Compare commits

..

107 Commits

Author SHA1 Message Date
embeddedt
292a6aeab3
Fix optimize_surface_rules breaking mods that provide custom BiomeManagers 2026-06-11 20:01:31 -04:00
embeddedt
7fbfcf1a92
Remove error when missing_block_entities sees null BE
Blocks may legitimately not have a block entity for some states
2026-06-07 21:50:44 -04:00
embeddedt
1bcb28a1ad
Allow feature level requirement to be set at package level 2026-06-07 19:43:28 -04:00
embeddedt
d51b0f60a2
Fix an instance of vanilla leaking a BufferBuilder 2026-06-07 19:19:25 -04:00
embeddedt
ab9880159e
Add experimental KubeJS memory usage optimization 2026-06-06 21:17:34 -04:00
embeddedt
0f94634361
Remove the item stack reference thread 2026-06-06 21:04:09 -04:00
embeddedt
f1492cc829
Allow ZipPackIndex to work with any byte channel 2026-06-04 20:57:13 -04:00
embeddedt
0ecee529d7
Fix Forge calling getResource on every loot table unnecessarily 2026-06-03 18:05:56 -04:00
embeddedt
e9bfd96dd9
Fix Forge pack finder being injected multiple times into pack repository 2026-05-28 22:33:03 -04:00
embeddedt
fb9dcf77c6
Improve ZipPackIndex 2026-05-28 22:20:28 -04:00
embeddedt
33851c1cb6
Fix ImposterProtoChunk leaking live block entities to worldgen 2026-05-24 23:09:52 -04:00
embeddedt
494203ef5a
Fix potential crash during worldgen with release_protochunks enabled
The crash can occur if a protochunk next to a FULL chunk is dropped,
and then later re-requested. If it was not persisted to disk for any
reason, it starts regeneration from scratch. At FEATURES stage, it may
try to place blocks into the adjacent LevelChunk already in the world.

The fix is to prevent this situation from even happening by pinning
protochunks directly next to FULL chunks, and preventing them from
unloading.
2026-05-24 19:45:24 -04:00
embeddedt
74f76f7305
Improvements to ZipPackIndex
- Allow it to work on channels that don't support mapping
- Skip indexing folders that are not part of a pack type
2026-05-23 21:44:14 -04:00
embeddedt
62dbbea083
Optimize ZIP resource packs significantly 2026-05-23 21:28:19 -04:00
embeddedt
538c52bc2a
Run stronghold gen on dedicated thread pool 2026-05-23 17:00:08 -04:00
embeddedt
b62eb1845b
Avoid blocking chunk generation on concentric rings calculation where possible 2026-05-23 16:43:56 -04:00
embeddedt
7c45564979
Fix potential stronghold cache corruption if player exits world too quickly 2026-05-23 16:32:56 -04:00
embeddedt
f8d2425242
Improve accuracy of possible biomes check 2026-05-23 12:50:48 -04:00
embeddedt
50cedfc699
Fix stability level being impossible to override 2026-05-23 12:50:33 -04:00
embeddedt
f4f596ca0c
Fix mixin failing at runtime due to missing AT 2026-05-23 12:50:21 -04:00
embeddedt
85aab426c5
Fix mixin AP complaints 2026-05-23 12:01:33 -04:00
embeddedt
29ff5f152e
Log the state of each mixin at DEBUG level 2026-05-23 11:58:36 -04:00
embeddedt
8213a720a3
Optimize TerraBlender using extended surface biome context
Supersedes TerraBlenderFix
2026-05-23 11:56:45 -04:00
embeddedt
afe3e09a27
Add feature level system for mixins 2026-05-23 11:51:11 -04:00
embeddedt
ae20fa17c9
Fix random CMEs from NightConfigWatchThrottler 2026-05-18 10:05:23 -04:00
embeddedt
a6c03e9928
Rewrite biome condition optimizer inspired by 26.2 changes
Thanks to https://codeberg.org/ZenXArch for making me aware of the
simpler vanilla approach to achieve the same thing
2026-05-16 13:59:01 -04:00
embeddedt
864c751aea
Remove stream in hot path of capability provider construction 2026-05-15 21:14:26 -04:00
embeddedt
f931d5c442
Fix isOptionEnabled being invoked in hot path during capability provider creation
Fixes #664
2026-05-15 21:04:40 -04:00
embeddedt
55cec86e5f
Disable mixin.perf.faster_ingredients with Prefab installed
Prefab relies on the nullity of `Ingredient.itemStacks` matching
vanilla, which is not true with this option enabled

aa5386c78b/Shared/src/com/prefab/recipe/ConditionedShapedRecipe.java (L166)

Fixes #660
2026-05-07 21:48:04 -04:00
embeddedt
4ec8ef753a
Fix scripts not detecting 26.1 branch 2026-05-06 18:19:17 -04:00
embeddedt
3f22e23565
Further optimize OptimizedBiomeLookupSequenceRule 2026-05-06 18:18:14 -04:00
embeddedt
a73dd5ef6a
Update bug report template 2026-05-05 20:27:55 -04:00
embeddedt
653a477060
Fix crash when mods use null attributes
Fixes #658
2026-05-05 20:23:06 -04:00
embeddedt
44113d2536
Improve efficiency of surface rule optimizer when rules are complex 2026-05-05 19:41:28 -04:00
embeddedt
1165d3bdd1
Fix Crash Assistant treating a mixin audit as a crash 2026-04-29 18:47:02 -04:00
embeddedt
c73cdc49a4
Replace CapabilityProvider mixin with ASM transformer
Works around this Mixin bug: https://github.com/FabricMC/Mixin/issues/146

Since CapabilityProvider is the parent of many commonly targeted classes
like Level, ItemStack, etc., this breaks mods

Fixes #650
2026-04-28 18:59:09 -04:00
embeddedt
4e3ecf9b6d
Disable mixin.perf.release_protochunks when Moonrise is present
Fixes #652
2026-04-27 19:52:08 -04:00
embeddedt
a40363c1fb
Improve issue comment workflow [skip ci] 2026-04-22 19:42:45 -04:00
embeddedt
46dd5ecddd
Comment on issues when fix is released
Fixes #649
2026-04-22 19:36:59 -04:00
embeddedt
b765bcb51f
Improve compatibility with mods that inject into ModelBaker.bake
Fixes #646
2026-04-22 19:27:07 -04:00
Mustafa
26bd7116a1
Change log level from warn to debug for successfully created missing block entities
Closes #648
2026-04-22 19:05:18 -04:00
Mustafa
4d2f0da1fc
Reduce log level of mixin.perf.spam_thread_dump to info
Closes #647
2026-04-22 18:48:47 -04:00
embeddedt
c2f585da95
Fix rare crash from HandshakeHandler in 5.27.0+
The existing Forge logic can concurrently modify sentMessages from two threads,
since handleIndexedMessage runs on the Netty thread, while tickServer is on the
server thread. Ticking the handler faster made the race condition significantly
more likely to manifest.
2026-04-14 22:22:06 -04:00
Evoloxi
327c3cd9ff
Fall back to interfaces when resolving capability fields (#643) 2026-04-13 20:32:01 -04:00
embeddedt
c64ca2e54b
Fix potential crash with mods that inject custom surface building logic
Fixes #638
2026-04-12 16:36:44 -04:00
embeddedt
85955ebf75
Ensure integrated server is ticked at least once before player connects
Fixes #639
2026-04-12 16:02:54 -04:00
embeddedt
d749205427
Adjust dynamic_languages for better mod compatibility 2026-04-11 14:39:36 -04:00
embeddedt
438ceb1984
Move auditing to happen later in launch 2026-04-11 14:19:22 -04:00
embeddedt
5acb5115b9
Add mixin audit to CI 2026-04-11 14:14:53 -04:00
embeddedt
37dc9e60eb
Do not intern AttributeSuppliers after launch 2026-04-11 14:04:37 -04:00
embeddedt
c2191df359
Release 5.27.0 & enable continuous deployment 2026-04-10 21:07:34 -04:00
embeddedt
d08da1b3c8
Disable release_protochunks when C2ME is installed 2026-03-29 19:46:04 -04:00
embeddedt
36f425b8cd
Fix excessive recursion from mailbox 2026-03-28 22:07:59 -04:00
embeddedt
dc3c379049
Fix ChunkBiomeLookup leaking a worldgen region 2026-03-28 21:45:59 -04:00
embeddedt
4ff7d4c554
Allow a single low-priority worker thread when cause_lag_by_disabling_threads is enabled
On a system with few cores, we should still benefit from using one low-priority
background thread for worldgen, because it avoids the server thread stopping
to handle it itself. The thread will be blocked
from progressing while higher-priority work (e.g. rendering or server ticking)
is in progress.
2026-03-28 21:45:14 -04:00
embeddedt
db13f39b30
Implement dynamic language loading 2026-03-28 20:55:27 -04:00
embeddedt
5a9c49f8d4
Add option to reduce memory usage of entity models 2026-03-28 20:02:30 -04:00
embeddedt
8ee85f2c16
Remove duplicate list held by DebugLevelSource 2026-03-28 19:31:24 -04:00
embeddedt
2081b63b56
Fix looking up private static final Capability fields 2026-03-27 22:38:18 -04:00
embeddedt
94f1fbf4db
Rewrite AttachCapabilitiesEvent hoisting to not rely on phases 2026-03-27 21:18:38 -04:00
embeddedt
ab8a8068e0
Avoid synchronizing layer list in LivingEntityRenderer 2026-03-26 22:58:18 -04:00
embeddedt
79d2b28d5b
Fix Forge handshake taking extremely long time with many payloads 2026-03-19 21:25:37 -04:00
embeddedt
18dc488ab9
Avoid spinning in Minecraft.doWorldLoad 2026-03-19 20:36:07 -04:00
embeddedt
a9340b2642
Rewrite and improve mixin.perf.cache_strongholds 2026-03-19 20:11:11 -04:00
embeddedt
670e06816b
Reduce work done while waiting for singleplayer client to initiate connection 2026-03-16 22:15:44 -04:00
embeddedt
53349cbd1a
Remove skip_redundant_saves 2026-03-16 22:14:35 -04:00
embeddedt
1794c81b61
Optimize sequence rules that check many biome conditions in a row 2026-03-15 15:24:54 -04:00
embeddedt
dbe9acb3d8
Heavily optimize the BlockColumn impl used during surface rule evaluation 2026-03-14 22:05:36 -04:00
embeddedt
22915a91a1
Implement a significantly more optimized biome lookup for surface rules 2026-03-14 19:44:42 -04:00
embeddedt
1289897004
Add worldgen benchmarking harness 2026-03-14 18:46:32 -04:00
embeddedt
9692da12b4
Add idle timer to prevent chunks from suspending too quickly 2026-03-14 15:59:52 -04:00
embeddedt
e34a99b38c
Simplify chunk unload logic & fix events not being fired when INACCESSIBLE chunks are unloaded 2026-03-14 14:59:45 -04:00
embeddedt
f79eae8b83
Make integrated server treat game as paused while singleplayer client is still loading 2026-03-14 10:44:04 -04:00
embeddedt
38288d5e6a
Automatically free contents of ChunkHolders only used for worldgen when generation finishes 2026-03-13 22:26:51 -04:00
embeddedt
2050516bf1
Do not cache supported glyphs in lazy provider 2026-03-13 19:53:33 -04:00
embeddedt
02f486ebf4
Avoid loading multiple copies of a lazy glyph provider 2026-03-13 19:36:15 -04:00
embeddedt
9edce9ad91
Dynamically load/unload Unihex font data 2026-03-06 20:52:26 -05:00
embeddedt
ac8d93d5b9
Ensure exceptions thrown in chunk load events are not dropped 2026-03-06 09:00:28 -05:00
embeddedt
bee4536c1a
Tweak full chunk promotion to reduce opportunities for deadlocks 2026-03-05 21:09:33 -05:00
embeddedt
da2206168b
Port AP to Java 17 2026-03-04 19:18:01 -05:00
embeddedt
17f930ea6f
WIP chunk saving optimization 2026-03-04 18:41:28 -05:00
embeddedt
f23348c6cb
Clear unneeded ObjectHolderRefs 2026-03-01 19:28:52 -05:00
embeddedt
21cbcb0e04
Strip signatures from jar manifests at startup to save memory 2026-03-01 17:52:13 -05:00
embeddedt
925c7526ee
Reduce memory usage of ImposterProtoChunks 2026-03-01 15:46:52 -05:00
embeddedt
30e3deb8e2
Avoid unnecessary chunkloads when remove_spawn_chunks is enabled 2026-03-01 15:18:13 -05:00
embeddedt
ee34dcf96e
Drastically simplify and document chunk system memory usage patch 2026-02-28 16:42:42 -05:00
embeddedt
49d800ff27
Avoid calling LazyOptional.isPresent() if possible 2026-02-27 22:19:04 -05:00
embeddedt
15f30b532c
Reduce generated class size slightly 2026-02-27 21:30:35 -05:00
embeddedt
df06010846
Fix superclass capability types being ignored sometimes 2026-02-27 20:53:40 -05:00
embeddedt
696b344ef5
Fix missed detection of certain cap equality checks 2026-02-27 20:35:58 -05:00
embeddedt
e63d99763e
Avoid initializing lazy capability providers for compatibility checks where possible 2026-02-27 19:29:16 -05:00
embeddedt
60850610f9
Group capability providers of known types together when possible 2026-02-27 19:11:24 -05:00
embeddedt
e16179b797
Emit more debug info to the generated dispatcher classes 2026-02-27 19:08:06 -05:00
embeddedt
784b914a43
Optimize runs of ICapabilityProvider calls into hash lookups 2026-02-26 22:26:57 -05:00
embeddedt
b9933b1158
Add bytecode analysis to filter ICapabilityProvider impls where possible
Currently disabled by default till more testing is completed
2026-02-26 21:45:31 -05:00
embeddedt
878b3798f3
Detect mods causing CMEs with the client resource reload listener list
Related: #512
2026-02-05 21:10:39 -05:00
embeddedt
bc0e9a09fc
Prevent model locations added in RegisterAdditional from being early baked 2026-02-02 21:29:14 -05:00
embeddedt
8c34c0de50
Dump stats on permanently loaded baked models to debug log 2026-02-02 20:50:21 -05:00
embeddedt
5a93bc6109
Use identityHashCode for attribute 2026-01-25 21:31:06 -05:00
embeddedt
8125da7882
Avoid propagating unbaked model load errors to higher-level code
Related: #625
2026-01-25 21:28:23 -05:00
embeddedt
d699187006
Fix AttachCapabilitiesEvent dispatch being very slow
EventBus strikes again...
2026-01-25 20:38:18 -05:00
embeddedt
cff29149db
Intern map keys in BlockStateData 2026-01-25 19:41:29 -05:00
embeddedt
3926f27d33
Optimize memory usage of entity attribute templates 2026-01-25 19:27:27 -05:00
embeddedt
9bc5f06a19
Ensure correct order of properties in generated ModelResourceLocation variant strings
Related: https://github.com/malte0811/FerriteCore/issues/219
2026-01-24 10:41:23 -05:00
embeddedt
a70f76a34d
Document the reason for lack of optimization 2026-01-23 20:20:48 -05:00
embeddedt
4dcdf09a01
Do not convert ModFileScanData annotation values to immutable lists
Related: #627
2026-01-23 20:19:28 -05:00
embeddedt
f26d35070e
Remove changelog step from release workflow [skip ci] 2026-01-22 20:01:26 -05:00
256 changed files with 10494 additions and 2311 deletions

View File

@ -4,51 +4,75 @@ body:
- type: markdown
attributes:
value: >-
**Note: This issue tracker is not intended for support requests!** If you need help with crashes or other issues, then
you should [ask on our Discord server](https://discord.gg/rN9Y7caguP) instead. Unless you are certain that you
have found a defect, and you are able to point to where the problem is, you should not open an issue.
<br><br>
Additionally, please make sure you have done the following:
**Need help?** Ask on [Discord](https://discord.gg/rN9Y7caguP) instead of opening an issue.
- **Have you ensured that all of your mods (including ModernFix) are up-to-date?** The latest version of ModernFix
can always be found [on Modrinth](https://modrinth.com/mod/modernfix).
- **Have you used the [search tool](https://github.com/embeddedt/ModernFix/issues) to check whether your issue
has already been reported?** If it has been, then consider adding more information to the existing issue instead.
- **Have you determined the minimum set of instructions to reproduce the issue?** If your problem only occurs
with other mods installed, then you should narrow down exactly which mods are causing the issue. Please do not
provide your entire list of mods to us and expect that we will be able to figure out the problem.
**Issues that do not meet the requirements below (or are otherwise impossible to address with the given info) will be closed without investigation.**
- type: checkboxes
id: confirmations
attributes:
label: Checklist
options:
- label: I am reporting a defect, not asking for help
required: true
- label: I have searched existing issues and this has not been reported
required: true
- label: I have reduced my mod list to the minimum required to reproduce this issue (see below)
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: >-
Use this section to describe the issue you are experiencing in as much depth as possible. The description should
explain what behavior you were expecting, and why you believe the issue to be a bug. If the issue you are reporting
only occurs with specific mods installed, then provide the name and version of each mod.
Describe the issue in detail. Be sure to include what you expected to happen and what actually happened.
validations:
required: true
- type: textarea
id: minimal-mods
attributes:
label: Minimal Mod List
description: >-
List ONLY the mods required to reproduce this issue. Maintainers have debugging tools that help them
locate problems quickly, but these generally don't work well in modpacks or large mod sets.
A minimal list should typically contain fewer than 10 mods.
**Hint:** If you have any screenshots, videos, or other information that you feel is necessary to
explain the issue, you can attach them here.
Reports with large mod lists will likely be closed without investigation, unless the problem is very clear.
If you don't know which mods are causing your problem, use binary search:
1. Remove half your mods
2. Test if the issue still occurs
3. If yes, remove half again. If no, restore the last removed half and repeat from step 1.
4. Repeat until only the necessary mods remain
placeholder: "- ModernFix 5.x.x\n- SomeMod 1.2.3"
validations:
required: true
- type: textarea
id: description-reproduction-steps
attributes:
label: Reproduction Steps
description: >-
Provide as much information as possible on how to reproduce this bug. Make sure your instructions are as clear and
concise as possible, because other people will need to be able to follow your guide in order to re-create the issue.
**Hint:** A common way to fill this section out is to write a step-by-step guide.
Provide clear steps to reproduce the bug. Each step should be a single concrete action.
Maintainers are busy and need to be able to quickly replicate your problem. Your reproduction steps should be
clear enough for someone who is unfamiliar with your mods to follow in 5 minutes or less (not counting time
to launch the game).
Providing vague steps is likely to result in the issue being closed.
placeholder: "1. \n2. \n3. "
validations:
required: true
- type: textarea
id: log-file
id: diagnostic-info
attributes:
label: Log File
label: Diagnostic Info
description: >-
**Hint:** You can usually find the log files within the folder `.minecraft/logs`. Most often, you will want the `latest.log`
file, since that file belongs to the last played session of the game.
placeholder: >-
Drag-and-drop the log file here.
Drag and drop `latest.log` from `.minecraft/logs/` for the session where the issue occurred.
Do not paste log text inline. Issues without a valid `latest.log` will be closed.
If a crash occurred, also attach the relevant file from `.minecraft/crash-reports/`.
validations:
required: true

View File

@ -11,6 +11,11 @@ on:
jobs:
build:
runs-on: ubuntu-22.04
permissions:
issues: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: true
steps:
- name: Checkout Repository
uses: actions/checkout@v4
@ -22,13 +27,108 @@ jobs:
distribution: 'temurin'
java-version: 21
check-latest: true
- name: Check if release branch
id: check_branch
if: github.event_name == 'push'
run: |
if [[ "${{ github.ref }}" =~ ^refs/heads/[0-9]+\. ]]; then
echo "is_release=true" >> $GITHUB_OUTPUT
else
echo "is_release=false" >> $GITHUB_OUTPUT
fi
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-read-only: ${{ !startsWith(github.ref, 'refs/heads/1.') }}
cache-read-only: ${{ steps.check_branch.outputs.is_release != 'true' }}
gradle-home-cache-cleanup: true
- name: Remove tags for release on other versions
if: steps.check_branch.outputs.is_release == 'true'
run: ./scripts/tagcleaner.sh
- name: Build ModernFix using Gradle
run: ./gradlew build
- name: Run mixin audit
run: timeout 60 xvfb-run ./gradlew runAuditClient
- name: Publish mod to CurseForge & Modrinth
if: steps.check_branch.outputs.is_release == 'true'
run: ./gradlew publishMods copyJarToBin
env:
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
- name: Capture mod version
if: steps.check_branch.outputs.is_release == 'true'
run: |
echo "MOD_VERSION=$(./gradlew properties -q | grep '^version:' | awk '{print $2}')" >> $GITHUB_ENV
echo "MC_VERSION=$(grep '^minecraft_version=' gradle.properties | cut -d= -f2)" >> $GITHUB_ENV
- name: Comment on fixed issues
if: steps.check_branch.outputs.is_release == 'true'
uses: actions/github-script@v7
with:
script: |
const { execSync } = require('child_process');
const branch = context.ref.replace('refs/heads/', '');
const { data: runs } = await github.rest.actions.listWorkflowRuns({
owner: context.repo.owner,
repo: context.repo.repo,
workflow_id: 'gradle.yml',
branch,
status: 'success',
per_page: 1
});
const logArgs = runs.workflow_runs.length > 0
? `${runs.workflow_runs[0].head_sha}..${context.sha}`
: `-1 ${context.sha}`;
const log = execSync(`git log ${logArgs} --format=%s%n%b`, { encoding: 'utf8' });
const issueNumbers = new Set();
const pattern = /(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)\s+#(\d+)/gi;
let match;
while ((match = pattern.exec(log)) !== null) {
issueNumbers.add(parseInt(match[1]));
}
if (issueNumbers.size === 0) {
console.log('No fixed issues found in commits');
return;
}
const MARKER = '<!-- modernfix-fix-tracker -->';
const modVersion = process.env.MOD_VERSION;
const mcVersion = process.env.MC_VERSION;
const newLine = `- ${modVersion} for Minecraft ${mcVersion}`;
for (const issueNumber of issueNumbers) {
try {
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
per_page: 100
});
const existing = comments.find(c => c.body.includes(MARKER));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body: existing.body + `\n${newLine}`
});
console.log(`Updated comment on issue #${issueNumber}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: issueNumber,
body: `${MARKER}\nThe fix for this issue has been released in the following versions of ModernFix:\n${newLine}`
});
console.log(`Created comment on issue #${issueNumber}`);
}
} catch (e) {
console.log(`Could not comment on #${issueNumber}: ${e.message}`);
}
}
- name: Upload Artifacts to GitHub
uses: actions/upload-artifact@v4
with:

View File

@ -1,41 +0,0 @@
name: Release ModernFix Artifacts
on:
release:
types:
- published
jobs:
release:
if: github.repository_owner == 'embeddedt'
runs-on: ubuntu-22.04
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: 21
check-latest: true
- name: Remove tags for release on other versions
run: ./scripts/tagcleaner.sh
- name: Build and publish mod to CurseForge & Modrinth
run: ./gradlew publishMods copyJarToBin
env:
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
- name: Upload assets to GitHub
uses: AButler/upload-release-assets@v3.0
with:
files: 'bin/*'
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Add changelog to release
uses: irongut/EditRelease@v1.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
id: ${{ github.event.release.id }}
replacebody: true
files: "CHANGELOG.md"

View File

@ -30,7 +30,7 @@ dependencies {
}
tasks.withType(JavaCompile) {
options.release = 21
options.release = 17
}
shadowJar {

View File

@ -90,24 +90,19 @@ public class ClientMixinValidator {
}
private boolean targetsClient(Object classTarget) {
return switch (classTarget) {
case TypeElement te ->
isClientMarked(te);
case TypeMirror tm -> {
var el = types.asElement(tm);
yield el != null ? targetsClient(el) : warn("TypeMirror of " + tm);
}
// If you're using a dollar sign in class names you are insane
case String s -> {
var te =
elemUtils.getTypeElement(toSourceString(s.split("\\$")[0]));
yield te != null ? targetsClient(te) : warn(s);
}
default ->
throw new IllegalArgumentException("Unhandled type: "
if (classTarget instanceof TypeElement te) {
return isClientMarked(te);
} else if (classTarget instanceof TypeMirror tm) {
var el = types.asElement(tm);
return el != null ? targetsClient(el) : warn("TypeMirror of " + tm);
} else if (classTarget instanceof String s) {
var te = elemUtils.getTypeElement(toSourceString(s.split("\\$")[0]));
return te != null ? targetsClient(te) : warn(s);
} else {
throw new IllegalArgumentException("Unhandled type: "
+ classTarget.getClass() + "\n" + "Stringified contents: "
+ classTarget.toString());
};
}
}
private boolean isClientMarked(TypeElement te) {
@ -166,7 +161,6 @@ public class ClientMixinValidator {
clzsses = wrappedClzss.stream()
.map(AnnotationValue::getValue)
.filter(o -> o instanceof TypeMirror)
.map(TypeMirror.class::cast)
.collect(Collectors.toSet());

View File

@ -25,7 +25,7 @@ public record MixinConfig(
InjectorOptions injectors, OverwriteOptions overwrites
) {
public MixinConfig(String packageName, List<String> commonMixins, List<String> clientMixins) {
this(true, "0.8", packageName, "org.embeddedt.modernfix.core.ModernFixMixinPlugin", "JAVA_21",
this(true, "0.8", packageName, "org.embeddedt.modernfix.core.ModernFixMixinPlugin", "JAVA_17",
commonMixins, clientMixins, InjectorOptions.DEFAULT, OverwriteOptions.DEFAULT);
}
public record InjectorOptions(int defaultRequire) {

View File

@ -0,0 +1,9 @@
package org.embeddedt.modernfix.annotation;
public enum FeatureLevel {
GA, BETA;
public boolean isAtLeast(FeatureLevel required) {
return this.ordinal() >= required.ordinal();
}
}

View File

@ -0,0 +1,12 @@
package org.embeddedt.modernfix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface RequiresFeatureLevel {
FeatureLevel value() default FeatureLevel.GA;
}

View File

@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
@Target({ElementType.TYPE, ElementType.PACKAGE})
public @interface RequiresMod {
String value() default "";
}

View File

@ -1,7 +1,5 @@
plugins {
id("net.neoforged.moddev") version("2.0.134")
id("org.ajoberstar.grgit") version("5.2.0")
id("com.palantir.git-version") version("1.0.0")
id("net.neoforged.moddev.legacyforge") version("2.0.134")
id("me.modmuss50.mod-publish-plugin") version("1.1.0")
}
@ -9,48 +7,20 @@ val minecraft_version = rootProject.properties["minecraft_version"].toString()
group = "org.embeddedt"
val versionDetails: groovy.lang.Closure<com.palantir.gradle.gitversion.VersionDetails> by extra
// extract base version from tag, generate other metadata ourselves
val details = versionDetails()
var plusIndex = details.lastTag.indexOf("+")
if (plusIndex == -1) {
plusIndex = details.lastTag.length
val gitVersion = providers.of(GitVersionSource::class) {
parameters {
minecraftVersion.set(minecraft_version)
projectDir.set(rootProject.layout.projectDirectory)
}
}
var baseVersion = details.lastTag.substring(0, plusIndex)
version = gitVersion.get()
val dirtyMarker = if (grgit.status().isClean) "" else ".dirty"
base.archivesName = "modernfix-forge"
val commitHashMarker =
if (details.commitDistance > 0)
"." + details.gitHash.substring(0, minOf(4, details.gitHash.length))
else
""
var preMarker =
if (details.commitDistance > 0 || !details.isCleanTag)
"-beta.${details.commitDistance}"
else
""
if (preMarker.isNotEmpty()) {
// bump to next patch release
val versionParts = baseVersion.split(".")
baseVersion =
"${versionParts[0]}.${versionParts[1]}.${versionParts[2].toInt() + 1}"
}
val versionString =
"${baseVersion}${preMarker}+mc${minecraft_version}${commitHashMarker}${dirtyMarker}"
version = versionString
base.archivesName = "modernfix-neoforge"
neoForge {
legacyForge {
enable {
version = rootProject.properties["forge_version"].toString()
forgeVersion = rootProject.properties["forge_version"].toString()
isDisableRecompilation = System.getenv("CI") == "true"
}
@ -68,6 +38,10 @@ neoForge {
create("server") {
server()
}
create("auditClient") {
client()
jvmArguments.addAll("-Dmodernfix.auditAndExit=true", "-Djava.awt.headless=true")
}
}
mods {
@ -77,21 +51,22 @@ neoForge {
}
}
mixin {
add(sourceSets.main.get(), "modernfix.refmap.json")
config("modernfix-modernfix.mixins.json")
}
tasks.named<Jar>("jar") {
manifest.attributes(mapOf(
"MixinConfigs" to "modernfix-modernfix.mixins.json",
"Specification-Version" to "1",
"Implementation-Title" to project.name,
"Implementation-Version" to version
))
}
// We must force the Java 21 compiler to be used because our AP requires Java 21
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
val curSourceCompatLevel = JavaVersion.VERSION_21
val curSourceCompatLevel = JavaVersion.VERSION_17
sourceCompatibility = curSourceCompatLevel
targetCompatibility = curSourceCompatLevel
}
@ -137,16 +112,24 @@ dependencies {
"additionalRuntimeClasspath"(project(":annotations"))
annotationProcessor(project(path = ":annotation-processor", configuration = "shadow"))
val mixinextrasVersion = rootProject.properties["mixinextras_version"].toString()
implementation("io.github.llamalad7:mixinextras-common:${mixinextrasVersion}")
annotationProcessor("net.fabricmc:sponge-mixin:0.12.5+mixin.0.8.5")
annotationProcessor("io.github.llamalad7:mixinextras-common:${mixinextrasVersion}")
implementation("io.github.llamalad7:mixinextras-forge:${mixinextrasVersion}")
"jarJar"("io.github.llamalad7:mixinextras-forge:${mixinextrasVersion}")
val jei_version = rootProject.properties["jei_version"].toString()
compileOnly("mezz.jei:jei-${minecraft_version}-neoforge:${jei_version}")
compileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}")
compileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}")
compileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}")
compileOnly("curse.maven:supermartijncore-454372:4455391")
compileOnly("curse.maven:patchouli-306770:6164575")
compileOnly("curse.maven:cofhcore-69162:5374122")
compileOnly("curse.maven:resourcefullib-570073:5659871")
compileOnly("curse.maven:kubejs-238086:5853326")
modCompileOnly("mezz.jei:jei-${minecraft_version}-forge:${jei_version}")
modCompileOnly("curse.maven:spark-361579:${rootProject.properties["spark_version"].toString()}")
modCompileOnly("curse.maven:ctm-267602:${rootProject.properties["ctm_version"].toString()}")
modCompileOnly("curse.maven:ldlib-626676:${rootProject.properties["ldlib_version"].toString()}")
modCompileOnly("curse.maven:supermartijncore-454372:4455391")
modCompileOnly("curse.maven:patchouli-306770:6164575")
modCompileOnly("curse.maven:cofhcore-69162:5374122")
modCompileOnly("curse.maven:resourcefullib-570073:5659871")
modCompileOnly("curse.maven:kubejs-238086:5853326")
modCompileOnly("curse.maven:terrablender-563928:6290448")
}
tasks.named<Jar>("jar") {
@ -178,12 +161,12 @@ tasks.named<ProcessResources>("processResources") {
inputs.property("version", project.version)
filesMatching("META-INF/neoforge.mods.toml") {
filesMatching("META-INF/mods.toml") {
expand("version" to project.version)
}
}
val finalJarTask = "jar"
val finalJarTask = "reobfJar"
tasks.register<Copy>("copyJarNameConsistent") {
from(tasks.named<Jar>(finalJarTask).get().outputs.files)
@ -207,7 +190,7 @@ publishMods {
changelog = "Please check the [GitHub wiki](https://github.com/embeddedt/ModernFix/wiki/Changelog) for major changes."
type = STABLE
modLoaders.add("neoforge")
modLoaders.add("forge")
curseforge {
projectId = "790626"

View File

@ -1,67 +0,0 @@
plugins {
id "architectury-plugin" version "3.4-SNAPSHOT"
id "dev.architectury.loom" version "1.9-SNAPSHOT" apply false
id "maven-publish"
id 'com.matthewprenger.cursegradle' version '1.4.0' apply false
id 'com.palantir.git-version' version '1.0.0'
id 'org.ajoberstar.grgit' version '5.2.0'
id 'se.bjurr.gitchangelog.git-changelog-gradle-plugin' version '1.79.0'
id "com.modrinth.minotaur" version "2.+" apply false
id("com.diffplug.spotless") version "6.25.0" apply false
id 'modernfix.common-conventions' apply false
}
architectury {
minecraft = rootProject.minecraft_version
}
ext.archives_base_name = 'modernfix'
apply plugin: 'modernfix.common-conventions'
tasks.withType(JavaCompile).configureEach {
// ensure that the encoding is set to UTF-8, no matter what the system default is
// this fixes some edge cases with special characters not displaying correctly
// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html
// If Javadoc is generated, this must be specified in that task too.
options.encoding = "UTF-8"
// The Minecraft launcher currently installs Java 8 for users, so your mod probably wants to target Java 8 too
// JDK 9 introduced a new way of specifying this that will make sure no newer classes or methods are used.
// We'll use that if it's available, but otherwise we'll use the older option.
def targetVersion = 8
/*
if (JavaVersion.current().isJava9Compatible()) {
options.release = targetVersion
}
*/
}
tasks.register('generateChangelog', se.bjurr.gitchangelog.plugin.gradle.GitChangelogTask) {
def details = versionDetails();
def theVersionRef
if (details.commitDistance > 0) {
theVersionRef = details.lastTag;
} else {
def secondLastTagCmd = "git describe --abbrev=0 " + details.lastTag + "^"
def secondLastTag = secondLastTagCmd.execute().text.trim()
theVersionRef = secondLastTag;
}
fromRef = theVersionRef
file = new File("${rootDir}/CHANGELOG.md");
templateContent = new File("${rootDir}/gradle/changelog.mustache").getText('UTF-8').replace("[[modernFixVersionRef]]", theVersionRef);
toCommit = "HEAD";
}
tasks.register('checkCleanTag') {
doLast {
def details = versionDetails()
if (!details.isCleanTag || versionDetails().commitDistance != 0) {
throw new GradleException('Not a clean tree.')
}
}
}
println "ModernFix: " + version

View File

@ -0,0 +1,7 @@
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}

View File

@ -0,0 +1,61 @@
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.ValueSource
import org.gradle.api.provider.ValueSourceParameters
import org.gradle.process.ExecOperations
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
abstract class GitVersionSource : ValueSource<String, GitVersionSource.Parameters> {
interface Parameters : ValueSourceParameters {
val minecraftVersion: Property<String>
val projectDir: DirectoryProperty
}
@get:Inject
abstract val execOperations: ExecOperations
override fun obtain(): String {
val minecraftVersion = parameters.minecraftVersion.get()
val workDir = parameters.projectDir.get().asFile
val releaseLine = workDir.resolve("release_line.txt").readText().trim()
val patch = try {
// Find the most recent first-parent commit that touched release_line.txt
val lineStartCommit = git(workDir,
"log", "--first-parent",
"-n", "1",
"--format=%H",
"--",
"release_line.txt"
).trim()
if (lineStartCommit.isEmpty()) {
// count all first-parent commits as a safe fallback
git(workDir, "rev-list", "--count", "--first-parent", "HEAD")
.trim().toIntOrNull() ?: 0
} else {
git(workDir, "rev-list", "--count", "--first-parent", "$lineStartCommit..HEAD")
.trim().toIntOrNull() ?: 0
}
} catch (_: Exception) {
// Git is unavailable or this is not a git repository
999
}
return "$releaseLine.$patch+mc$minecraftVersion"
}
private fun git(workDir: File, vararg args: String): String {
val output = ByteArrayOutputStream()
execOperations.exec {
commandLine("git", *args)
standardOutput = output
workingDir(workDir)
}
return output.toString(Charsets.UTF_8)
}
}

View File

@ -5,29 +5,28 @@ junit_version=5.10.0-M1
mixinextras_version=0.4.1
mod_id=modernfix
minecraft_version=1.21.1
enabled_platforms=neoforge
forge_version=21.1.111
parchment_version=2024.11.17
parchment_mc_version=1.21.1
minecraft_version=1.20.1
enabled_platforms=forge
forge_version=1.20.1-47.4.0
parchment_version=2023.07.09
refined_storage_version=4392788
jei_version=19.21.2.313
rei_version=13.0.678
ctm_version=5587515
ldlib_version=5782845
kubejs_version=2101.7.1-build.181
rhino_version=2101.2.7-build.74
supported_minecraft_versions=1.21.1
jei_version=15.8.0.11
rei_version=11.0.597
ctm_version=5983309
ldlib_version=5927130
kubejs_version=2001.6.5-build.16
rhino_version=2001.2.3-build.10
supported_minecraft_versions=1.20.1
fabric_loader_version=0.16.10
fabric_api_version=0.102.1+1.21.1
fabric_api_version=0.86.0+1.20.1
continuity_version=3.0.0-beta.4+1.20.2
continuity_version=3.0.0-beta.2+1.19.3
modmenu_version=11.0.3
modmenu_version=7.0.0-beta.2
diagonal_fences_version=4558828
spark_version=6225208
spark_version=4587310
use_fabric_api_at_runtime=true

View File

@ -1 +0,0 @@
loom.platform=neoforge

1
release_line.txt Normal file
View File

@ -0,0 +1 @@
5.27

View File

@ -1,2 +1,2 @@
#!/bin/bash
git ls-remote --heads origin | awk '{print $2}' | grep -E '^refs/heads/1\.' | sed 's:.*/::' | sort -V | grep -E '^1\.[0-9]*(\.[0-9]*)?$'
git ls-remote --heads origin | awk '{print $2}' | grep -E '^refs/heads/[0-9]+\.' | sed 's:.*/::' | sort -V | grep -E '^[0-9]+\.[0-9]*(\.[0-9]*)?$'

View File

@ -5,7 +5,7 @@ import re
def get_valid_mixin_options():
all_mixin_options = set()
# gather all mixins in mixin folders
for platform in [ "common", "fabric", "forge" ]:
for platform in [ "common", "forge" ]:
base_path = f"{platform}/src/main/java/org/embeddedt/modernfix/{platform}/mixin"
for root, dirs, files in os.walk(base_path):
for file in files:

View File

@ -2,6 +2,7 @@ package org.embeddedt.modernfix;
import net.minecraft.SharedConstants;
import net.minecraft.Util;
import net.minecraft.client.Minecraft;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.ServerLevel;
@ -12,6 +13,7 @@ import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.resources.ReloadExecutor;
import org.embeddedt.modernfix.util.ClassInfoManager;
import org.spongepowered.asm.mixin.MixinEnvironment;
import java.lang.management.ManagementFactory;
import java.util.concurrent.ExecutorService;
@ -45,6 +47,17 @@ public class ModernFix {
return resourceReloadService;
}
public static void runAuditIfRequested() {
boolean auditAndExit = Boolean.getBoolean("modernfix.auditAndExit");
if (auditAndExit || Boolean.getBoolean("modernfix.auditMixinsAtStart")) {
MixinEnvironment.getCurrentEnvironment().audit();
if (auditAndExit) {
// Prevents Crash Assistant from treating mixin audit as a crash
Minecraft.getInstance().stop();
System.exit(0);
}
}
}
public ModernFix() {
INSTANCE = this;

View File

@ -7,12 +7,14 @@ import org.embeddedt.modernfix.api.constants.IntegrationConstants;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.searchtree.JEIBackedSearchTree;
import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry;
import org.embeddedt.modernfix.spark.SparkLaunchProfiler;
import org.embeddedt.modernfix.util.ClassInfoManager;
import org.embeddedt.modernfix.world.IntegratedWatchdog;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;
public class ModernFixClient {
@ -38,6 +40,7 @@ public class ModernFixClient {
if(ModernFixMixinPlugin.instance.isOptionEnabled("feature.branding.F3Screen")) {
brandingString = ModernFix.NAME + " " + ModernFixPlatformHooks.INSTANCE.getVersionString();
}
SearchTreeProviderRegistry.register(JEIBackedSearchTree.PROVIDER);
for(String className : ModernFixPlatformHooks.INSTANCE.getCustomModOptions().get(IntegrationConstants.CLIENT_INTEGRATION_CLASS)) {
try {
CLIENT_INTEGRATIONS.add((ModernFixClientIntegration)Class.forName(className).getDeclaredConstructor().newInstance());

View File

@ -1,10 +1,11 @@
package org.embeddedt.modernfix.api.entrypoint;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import java.util.function.Function;
/**
* Implement this interface in a mod class and add it to "modernfix:integration_v1" in your mod metadata file
@ -20,10 +21,49 @@ public interface ModernFixClientIntegration {
default void onDynamicResourcesStatusChange(boolean enabled) {
}
/**
* Called to allow mods to observe the loading of an unbaked model and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
default UnbakedModel onUnbakedModelLoad(ResourceLocation location, UnbakedModel originalModel, ModelBakery bakery) {
return originalModel;
}
/**
* Called to allow mods to observe the use of an unbaked model at bake time and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
default UnbakedModel onUnbakedModelPreBake(ResourceLocation location, UnbakedModel originalModel, ModelBakery bakery) {
return originalModel;
}
/**
* Called to allow mods to observe the loading of a baked model and either make changes to it or wrap it with their
* own instance.
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
* with dynamic resources on
* @return the model which should actually be loaded for this resource location
*/
@Deprecated
default BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery) {
return originalModel;
}
/**
* Called to allow mods to observe the loading of a baked model and either make changes to it or wrap it with their
* own instance.
*
* @param location the ResourceLocation of the model (this may be a ModelResourceLocation)
* @param originalModel the original model
* @param bakery the model bakery - do not touch internal fields as they probably don't behave the way you expect
@ -31,7 +71,7 @@ public interface ModernFixClientIntegration {
* @param textureGetter function to retrieve textures for this model
* @return the model which should actually be loaded for this resource location
*/
default BakedModel onBakedModelLoad(ModelResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter textureGetter) {
return originalModel;
default BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
return onBakedModelLoad(location, baseModel, originalModel, state, bakery);
}
}

View File

@ -1,18 +1,23 @@
package org.embeddedt.modernfix.api.helpers;
import com.google.common.collect.ImmutableList;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers;
import org.embeddedt.modernfix.util.DynamicMap;
import org.jetbrains.annotations.Nullable;
import java.util.Map;
import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Function;
@SuppressWarnings("unused")
public final class ModelHelpers {
@ -23,7 +28,7 @@ public final class ModelHelpers {
* @return a list of all blockstates related to the model
*/
public static ImmutableList<BlockState> getBlockStateForLocation(ModelResourceLocation location) {
Optional<Block> blockOpt = BuiltInRegistries.BLOCK.getOptional(location.id());
Optional<Block> blockOpt = BuiltInRegistries.BLOCK.getOptional(new ResourceLocation(location.getNamespace(), location.getPath()));
if(blockOpt.isPresent())
return ModelBakeryHelpers.getBlockStatesForMRL(blockOpt.get().getStateDefinition(), location);
else
@ -47,7 +52,7 @@ public final class ModelHelpers {
* @return a fake map of the top-level models
*/
public static Map<ResourceLocation, BakedModel> createFakeTopLevelMap(BiFunction<ResourceLocation, ModelState, BakedModel> modelGetter) {
return new DynamicMap<>(ResourceLocation.class, location -> modelGetter.apply(location, BlockModelRotation.X0_Y0));
return new DynamicMap<>(location -> modelGetter.apply(location, BlockModelRotation.X0_Y0));
}
/**
@ -56,8 +61,6 @@ public final class ModelHelpers {
* @return an appropriate ModelBaker
*/
public static ModelBaker adaptBakery(ModelBakery bakery) {
throw new UnsupportedOperationException("TODO");
/*
return new ModelBaker() {
@Override
public UnbakedModel getModel(ResourceLocation resourceLocation) {
@ -69,8 +72,16 @@ public final class ModelHelpers {
public BakedModel bake(ResourceLocation resourceLocation, ModelState modelState) {
return ((IExtendedModelBakery)bakery).bakeDefault(resourceLocation, modelState);
}
};
*/
@Override
public @Nullable BakedModel bake(ResourceLocation location, ModelState state, Function<Material, TextureAtlasSprite> sprites) {
throw new UnsupportedOperationException();
}
@Override
public Function<Material, TextureAtlasSprite> getModelTextureGetter() {
return Material::sprite;
}
};
}
}

View File

@ -0,0 +1,185 @@
package org.embeddedt.modernfix.benchmark;
import com.google.common.util.concurrent.MoreExecutors;
import com.mojang.datafixers.util.Either;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.server.level.*;
import net.minecraft.util.Unit;
import net.minecraft.world.entity.ai.village.poi.PoiManager;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.*;
import net.minecraft.world.level.chunk.storage.ChunkSerializer;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.embeddedt.modernfix.ModernFix;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.Function;
public class WorldgenBenchmark {
private static final TicketType<Unit> BENCHMARK_TICKET =
TicketType.create("modernfix_benchmark", (a, b) -> 0);
private static final List<ChunkStatus> ALL_STATUSES = ChunkStatus.getStatusList().stream()
.filter(s -> s.getIndex() > ChunkStatus.EMPTY.getIndex()
&& s.getIndex() < ChunkStatus.INITIALIZE_LIGHT.getIndex())
.toList();
private static final int REQUIRED_LOAD_RADIUS = ALL_STATUSES.stream().mapToInt(ChunkStatus::getRange).max().orElse(0);
public static String run(ServerLevel level, ChunkPos center, int testRadius, int iterations, ChunkStatus startStatus, ChunkStatus stopStatus) {
int startIndex = ALL_STATUSES.indexOf(startStatus);
if (startIndex < 0) {
throw new IllegalArgumentException("Invalid start status: " + startStatus);
}
int stopIndex = ALL_STATUSES.indexOf(stopStatus);
if (stopIndex < 0) {
throw new IllegalArgumentException("Invalid stop status:" + stopStatus);
}
List<ChunkStatus> setupStatuses = ALL_STATUSES.subList(0, startIndex);
List<ChunkStatus> timedStatuses = ALL_STATUSES.subList(startIndex, stopIndex + 1);
Context ctx = new Context(level, center, testRadius);
long[] timings = new long[timedStatuses.size()];
int testDiameter = 2 * testRadius + 1;
int numPositions = testDiameter * testDiameter;
ChunkPos[] testPositions = new ChunkPos[numPositions];
CompoundTag[] snapshots = new CompoundTag[numPositions];
ChunkAccess[][] neighborArrays = new ChunkAccess[numPositions][];
int idx = 0;
for (int tz = -testRadius; tz <= testRadius; tz++) {
for (int tx = -testRadius; tx <= testRadius; tx++) {
ChunkPos testPos = new ChunkPos(center.x + tx, center.z + tz);
testPositions[idx] = testPos;
neighborArrays[idx] = ctx.buildNeighborArray(testPos);
ProtoChunk setupProto = ctx.newProtoChunk(testPos);
neighborArrays[idx][ctx.centerIndex] = setupProto;
for (ChunkStatus status : setupStatuses) {
status.generate(ctx.executor, level, ctx.generator, ctx.templates,
ctx.lightEngine, ctx.noopPromotion, Arrays.asList(neighborArrays[idx])).join();
}
snapshots[idx] = ChunkSerializer.write(level, setupProto);
idx++;
ModernFix.LOGGER.info("worldgen benchmark setup progress: {}/{}", idx, numPositions);
}
}
ModernFix.LOGGER.info("worldgen benchmark setup complete");
for (int iter = 0; iter < iterations; iter++) {
ModernFix.LOGGER.info("worldgen benchmark iteration: {}/{}", iter + 1, iterations);
for (int p = 0; p < numPositions; p++) {
ProtoChunk restored = ChunkSerializer.read(
level, ctx.poiManager, testPositions[p], snapshots[p]);
neighborArrays[p][ctx.centerIndex] = restored;
List<ChunkAccess> neighborList = Arrays.asList(neighborArrays[p]);
for (int s = 0; s < timedStatuses.size(); s++) {
long t0 = System.nanoTime();
timedStatuses.get(s).generate(ctx.executor, level, ctx.generator,
ctx.templates, ctx.lightEngine, ctx.noopPromotion, neighborList).join();
timings[s] += System.nanoTime() - t0;
}
}
}
ModernFix.LOGGER.info("worldgen benchmark done");
ctx.cleanup();
return formatTimings(timedStatuses, timings, testRadius, iterations);
}
private static String formatTimings(List<ChunkStatus> statuses, long[] timings, int testRadius, int iterations) {
int totalChunks = (2 * testRadius + 1) * (2 * testRadius + 1) * iterations;
StringBuilder sb = new StringBuilder();
long total = 0;
for (int i = 0; i < timings.length; i++) {
total += timings[i];
String name = BuiltInRegistries.CHUNK_STATUS.getKey(statuses.get(i)).getPath();
sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n",
name, timings[i] / 1e6, timings[i] / 1e6 / totalChunks));
}
sb.append(String.format(" %-22s %8.1f ms (%6.2f ms/chunk)\n",
"TOTAL", total / 1e6, total / 1e6 / totalChunks));
return sb.toString();
}
private static class Context {
final ServerLevel level;
final ServerChunkCache chunkSource;
final ChunkPos center;
final int loadRadius;
final int loadDiameter;
final ChunkAccess[] realChunks;
final int neighborDiameter;
final int centerIndex;
final Executor executor;
final ChunkGenerator generator;
final ThreadedLevelLightEngine lightEngine;
final StructureTemplateManager templates;
final PoiManager poiManager;
final Function<ChunkAccess, CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> noopPromotion;
private final net.minecraft.core.Registry<Biome> biomeRegistry;
Context(ServerLevel level, ChunkPos center, int testRadius) {
this.level = level;
this.chunkSource = level.getChunkSource();
this.center = center;
this.loadRadius = testRadius + REQUIRED_LOAD_RADIUS;
this.loadDiameter = 2 * loadRadius + 1;
this.neighborDiameter = 2 * REQUIRED_LOAD_RADIUS + 1;
this.centerIndex = neighborDiameter * neighborDiameter / 2;
this.executor = MoreExecutors.directExecutor();
this.generator = chunkSource.getGenerator();
this.lightEngine = chunkSource.getLightEngine();
this.templates = level.getStructureManager();
this.poiManager = chunkSource.getPoiManager();
this.noopPromotion = chunk -> CompletableFuture.completedFuture(Either.left(chunk));
this.biomeRegistry = level.registryAccess().registryOrThrow(Registries.BIOME);
chunkSource.addRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE);
realChunks = new ChunkAccess[loadDiameter * loadDiameter];
for (int dz = -loadRadius; dz <= loadRadius; dz++) {
for (int dx = -loadRadius; dx <= loadRadius; dx++) {
LevelChunk real = level.getChunk(center.x + dx, center.z + dz);
realChunks[(dz + loadRadius) * loadDiameter + (dx + loadRadius)] =
new ImposterProtoChunk(real, false);
}
}
}
ProtoChunk newProtoChunk(ChunkPos pos) {
return new ProtoChunk(pos, UpgradeData.EMPTY, level, biomeRegistry, null);
}
ChunkAccess[] buildNeighborArray(ChunkPos testPos) {
int count = neighborDiameter * neighborDiameter;
ChunkAccess[] array = new ChunkAccess[count];
int baseX = (testPos.x - REQUIRED_LOAD_RADIUS) - (center.x - loadRadius);
int baseZ = (testPos.z - REQUIRED_LOAD_RADIUS) - (center.z - loadRadius);
for (int dz = 0; dz < neighborDiameter; dz++) {
System.arraycopy(realChunks, (baseZ + dz) * loadDiameter + baseX,
array, dz * neighborDiameter, neighborDiameter);
}
return array;
}
void cleanup() {
chunkSource.removeRegionTicket(BENCHMARK_TICKET, center, loadRadius, Unit.INSTANCE);
}
}
}

View File

@ -0,0 +1,7 @@
package org.embeddedt.modernfix.chunk;
import net.minecraft.world.level.chunk.Palette;
public interface ExtendedPalettedContainer<T> {
Palette<T> mfix$getPalette();
}

View File

@ -3,14 +3,58 @@ package org.embeddedt.modernfix.command;
import com.mojang.brigadier.CommandDispatcher;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager;
import org.embeddedt.modernfix.structure.CachingStructureManager;
import java.io.InputStream;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static net.minecraft.commands.Commands.literal;
public class ModernFixCommands {
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
dispatcher.register(literal("modernfix")
.then(literal("upgradeStructures")
.requires(source -> source.hasPermission(3))
.executes(context -> {
ServerLevel level = context.getSource().getLevel();
if(level == null) {
context.getSource().sendFailure(Component.literal("Couldn't find server level"));
return 0;
}
ResourceManager manager = level.getServer().resources.resourceManager();
Map<ResourceLocation, Resource> structures = manager.listResources("structures", p -> p.getPath().endsWith(".nbt"));
int upgradedNum = 0;
Pattern pathPattern = Pattern.compile("^structures/(.*)\\.nbt$");
for(Map.Entry<ResourceLocation, Resource> entry : structures.entrySet()) {
upgradedNum++;
ResourceLocation found = entry.getKey();
Matcher matcher = pathPattern.matcher(found.getPath());
if(!matcher.matches())
continue;
ResourceLocation structureLocation = new ResourceLocation(found.getNamespace(), matcher.group(1));
try(InputStream resource = entry.getValue().open()) {
CachingStructureManager.readStructureTag(structureLocation, level.getServer().getFixerUpper(), resource);
Component msg = Component.literal("checked " + structureLocation + " (" + upgradedNum + "/" + structures.size() + ")");
context.getSource().sendSuccess(() -> msg, false);
} catch(Throwable e) {
ModernFix.LOGGER.error("Couldn't upgrade structure " + found, e);
context.getSource().sendFailure(Component.literal("error reading " + structureLocation + " (" + upgradedNum + "/" + structures.size() + ")"));
}
}
context.getSource().sendSuccess(() -> Component.literal("All structures upgraded"), false);
return 1;
}))
.then(literal("mcfunctions").requires(source -> source.hasPermission(3))
.executes(context -> {
ServerLevel level = context.getSource().getLevel();

View File

@ -0,0 +1,56 @@
package org.embeddedt.modernfix.common.mixin.bugfix.buffer_builder_leak;
import com.mojang.blaze3d.vertex.BufferBuilder;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.render.UnsafeBufferHelper;
import org.spongepowered.asm.mixin.Dynamic;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.nio.ByteBuffer;
@Mixin(value = BufferBuilder.class, priority = 1500)
@ClientOnlyMixin
public class BufferBuilderMixin {
@Shadow private ByteBuffer buffer;
private static boolean leakReported = false;
private boolean mfix$shouldFree = true;
@Dynamic
@Inject(method = "flywheel$injectForRender", at = @At("RETURN"), remap = false, require = 0)
private void preventFree(CallbackInfo ci) {
mfix$shouldFree = false;
}
/**
* Ensure UnsafeBufferHelper is classloaded early, to avoid Forge's event transformer showing an error in the log.
*/
@Inject(method = "<clinit>", at = @At(value = "RETURN"))
private static void initUnsafeBufferHelper(CallbackInfo ci) {
UnsafeBufferHelper.init();
}
@Override
protected void finalize() throws Throwable {
try {
ByteBuffer buf = buffer;
// can be null if a mod already tried to free the buffer
if(buf != null && mfix$shouldFree) {
if(!leakReported) {
leakReported = true;
ModernFix.LOGGER.warn("One or more BufferBuilders have been leaked, ModernFix will attempt to correct this.");
}
UnsafeBufferHelper.free(buf);
buffer = null;
}
} finally {
super.finalize();
}
}
}

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.bugfix.buffer_builder_leak;
import com.mojang.blaze3d.vertex.BufferBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap;
import net.minecraft.client.renderer.RenderBuffers;
import net.minecraft.client.renderer.RenderType;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(RenderBuffers.class)
@ClientOnlyMixin
public class RenderBuffersMixin {
/**
* @author embeddedt
* @reason put() may be called for multiple instances of the same render type (e.g. signSheet and hangingSignSheet
* in 1.20.1). This leaks the previous BufferBuilder if one is already in the map.
*/
@Inject(method = "put", at = @At("HEAD"), cancellable = true)
private static void mfix$preventBufferLeak(Object2ObjectLinkedOpenHashMap<RenderType, BufferBuilder> mapBuilders, RenderType renderType, CallbackInfo ci) {
if (mapBuilders.containsKey(renderType)) {
ci.cancel();
}
}
}

View File

@ -0,0 +1,163 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.datafixers.util.Either;
import net.minecraft.CrashReport;
import net.minecraft.ReportedException;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.embeddedt.modernfix.ModernFix;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Field;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.Executor;
import java.util.function.Function;
@Mixin(ChunkMap.class)
public abstract class ChunkMapLoadMixin {
@Shadow
@Nullable
protected abstract ChunkHolder getVisibleChunkIfPresent(long l);
@Shadow
@Final
private BlockableEventLoop<Runnable> mainThreadExecutor;
@Unique
private static final ThreadLocal<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> MFIX_SURROGATE_FUTURE = new ThreadLocal<>();
@Unique
private final ConcurrentLinkedQueue<Throwable> mfix$promotionExceptions = new ConcurrentLinkedQueue<>();
/**
* @author embeddedt
* @reason This redirect makes several changes to how full chunk promotion works. First of all, promotion runs
* directly in the context of the main thread executor, rather than going through the priority sorter.
* This change allows attempts to load other chunks from within the promotion lambda to succeed (important
* for bad EntityJoinLevelEvent implementations to not deadlock the game). Second, it slightly alters the
* semantics of protoChunkToFullChunk so that the FULL chunk future will be completed before postload
* callbacks finish running. This change allows attempts to load the _same_ chunk in the promotion lambda to
* succeed, as otherwise the future would block waiting for itself to complete.
*
* <p>This is a cleaner version of a similar trick used in ModernFix versions for 1.16, which deferred specifically
* entity addition to happen outside the futures.
*/
@Redirect(method = "protoChunkToFullChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0))
private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> createSurrogateFuture(
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> previousFuture,
Function<? super Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>, ? extends Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> fn,
Executor executor) {
var surrogate = new CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>();
// Unlike vanilla, we execute the promotion lambda in mainThreadExecutor, rather than within the context
// of the task sorter. Doing this avoids deadlocking the sorter if a blocking chunk load is attempted
// during chunk promotion. We still initially compose the future through the sorter's executor to stop promotion
// from running earlier than it would in vanilla.
previousFuture.thenComposeAsync(CompletableFuture::completedFuture, executor).thenApplyAsync(either -> {
// running on thread that executes lambda body
MFIX_SURROGATE_FUTURE.set(surrogate);
try {
return fn.apply(either);
} finally {
MFIX_SURROGATE_FUTURE.remove();
}
}, this.mainThreadExecutor).whenComplete((either, throwable) -> {
if (throwable != null) {
if (!surrogate.isDone()) {
surrogate.completeExceptionally(throwable);
} else {
// The chunk has already become visible at FULL status, so we
// track the exception ourselves and manually rethrow it at the right point
// to trigger a server crash
this.mfix$promotionExceptions.add(throwable);
}
} else {
surrogate.complete(either);
}
});
// Return the surrogate
return surrogate;
}
/**
* @author embeddedt
* @reason Complete the surrogate future as soon as basic promotion is done, and before we start loading entities
* & block entities. This allows EntityJoinLevelEvent to read the current chunk.
*/
@Inject(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
private void completeSurrogateFuture(ChunkHolder holder, ChunkAccess p_214856_, CallbackInfoReturnable<ChunkAccess> cir,
@Local(ordinal = 0) LevelChunk levelChunk) {
var future = MFIX_SURROGATE_FUTURE.get();
if (future != null) {
future.complete(Either.left(levelChunk));
}
}
@Inject(method = "tick()V", at = @At("HEAD"))
private void reportDeferredPromotionException(CallbackInfo ci) {
var throwable = this.mfix$promotionExceptions.poll();
if (throwable == null) {
return;
}
if (throwable instanceof ReportedException e) {
throw e;
} else {
throw new ReportedException(CrashReport.forThrowable(throwable, "Exception during promotion of chunk to FULL status"));
}
}
// we also preserve the legacy currentlyLoading field to keep Forge parity
private static final Field currentlyLoadingField = ObfuscationReflectionHelper.findField(ChunkHolder.class, "currentlyLoading");
private static void setCurrentlyLoading(ChunkHolder holder, LevelChunk value) {
try {
currentlyLoadingField.set(holder, value);
} catch(ReflectiveOperationException e) {
e.printStackTrace();
}
}
/**
* Set currentlyLoading before calling runPostLoad and restore its old value afterwards. We track the old value
* to avoid conflicting with Forge if/when this feature is added.
*/
@WrapOperation(method = "lambda$protoChunkToFullChunk$34", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
private void setCurrentLoadingThenPostLoad(LevelChunk chunk, Operation<Void> operation) {
ChunkHolder holder = this.getVisibleChunkIfPresent(chunk.getPos().toLong());
if(holder != null) {
LevelChunk prevLoading = null;
try {
prevLoading = (LevelChunk)currentlyLoadingField.get(holder);
} catch(ReflectiveOperationException e) {
e.printStackTrace();
}
try {
setCurrentlyLoading(holder, chunk);
operation.call(chunk);
} finally {
setCurrentlyLoading(holder, prevLoading);
}
} else {
ModernFix.LOGGER.warn("Unable to find chunk holder for loading chunk");
operation.call(chunk);
}
}
}

View File

@ -1,7 +1,6 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.core.Holder;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.gameevent.GameEvent;
@ -18,8 +17,8 @@ public class EntityMixin {
* tries to raytrace blocks. To fix this, we skip firing the sculk event if the chunk the entity is within is not
* loaded.
*/
@WrapWithCondition(method = "addPassenger", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;gameEvent(Lnet/minecraft/core/Holder;Lnet/minecraft/world/entity/Entity;)V"))
private boolean onlyAddIfSelfChunkLoaded(Entity instance, Holder<GameEvent> gameEvent, Entity entity) {
@WrapWithCondition(method = "addPassenger", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/Entity;gameEvent(Lnet/minecraft/world/level/gameevent/GameEvent;Lnet/minecraft/world/entity/Entity;)V"))
private boolean onlyAddIfSelfChunkLoaded(Entity instance, GameEvent event, Entity entity) {
var chunkPos = instance.chunkPosition();
if (instance.level() instanceof ServerLevel serverLevel && serverLevel.getChunkSource().getChunkNow(chunkPos.x, chunkPos.z) == null) {
ModernFix.LOGGER.warn("Skipped emitting ENTITY_MOUNT game event for entity {} as it would cause deadlock", instance.toString());

View File

@ -0,0 +1,57 @@
package org.embeddedt.modernfix.common.mixin.bugfix.chunk_deadlock;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.Field;
@Mixin(value = ServerChunkCache.class, priority = 1100)
public abstract class ServerChunkCache_CurrentLoadingMixin {
@Shadow @Nullable protected abstract ChunkHolder getVisibleChunkIfPresent(long l);
private static final MethodHandle CURRENTLY_LOADING;
static {
try {
Field currentlyLoadingField = ObfuscationReflectionHelper.findField(ChunkHolder.class, "currentlyLoading");
currentlyLoadingField.setAccessible(true);
CURRENTLY_LOADING = MethodHandles.lookup().unreflectGetter(currentlyLoadingField);
} catch(Exception e) {
throw new RuntimeException("Failed to get currentlyLoading field", e);
}
}
/**
* Check the currentlyLoading field before going to the future chain, as was done in 1.16. In 1.18 upstream seems
* to have only applied this to getChunkNow().
*/
@Inject(method = "getChunk", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;getChunkFutureMainThread(IILnet/minecraft/world/level/chunk/ChunkStatus;Z)Ljava/util/concurrent/CompletableFuture;"), cancellable = true, require = 0)
private void checkCurrentlyLoading(int chunkX, int chunkZ, ChunkStatus requiredStatus, boolean load, CallbackInfoReturnable<ChunkAccess> cir) {
long i = ChunkPos.asLong(chunkX, chunkZ);
ChunkHolder holder = this.getVisibleChunkIfPresent(i);
if(holder != null) {
LevelChunk c;
try {
c = (LevelChunk)CURRENTLY_LOADING.invokeExact(holder);
} catch(Throwable e) {
e.printStackTrace();
c = null;
}
if(c != null)
cir.setReturnValue(c);
}
}
}

View File

@ -0,0 +1,31 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.tags.TagKey;
import net.minecraftforge.registries.tags.ITag;
import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Map;
@Mixin(targets = {"net/minecraftforge/registries/ForgeRegistryTagManager"})
public class ForgeRegistryTagManagerMixin<V> {
@Shadow private volatile Map<TagKey<V>, ITag<V>> tags;
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@WrapMethod(method = "getTag", remap = false)
private ITag<V> getTagSafe(@NotNull TagKey<V> name, Operation<ITag<V>> original) {
ITag<V> tag = this.tags.get(name);
if (tag == null) {
synchronized (this) {
tag = original.call(name);
}
}
return tag;
}
}

View File

@ -0,0 +1,40 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import net.minecraft.core.HolderSet;
import net.minecraft.core.MappedRegistry;
import net.minecraft.tags.TagKey;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.IdentityHashMap;
import java.util.Map;
@Mixin(value = MappedRegistry.class, priority = 500)
public abstract class MappedRegistryMixin<T> {
@Shadow private volatile Map<TagKey<T>, HolderSet.Named<T>> tags;
@Shadow protected abstract HolderSet.Named<T> createTag(TagKey<T> key);
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@Overwrite
public HolderSet.Named<T> getOrCreateTag(TagKey<T> key) {
HolderSet.Named<T> named = this.tags.get(key);
if (named == null) {
// synchronize and check again - this is the bugfix
synchronized (this) {
named = this.tags.get(key);
if (named == null) {
named = this.createTag(key);
Map<TagKey<T>, HolderSet.Named<T>> map = new IdentityHashMap<>(this.tags);
map.put(key, named);
this.tags = map;
}
}
}
return named;
}
}

View File

@ -0,0 +1,39 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import net.minecraft.core.HolderSet;
import net.minecraft.tags.TagKey;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.IdentityHashMap;
import java.util.Map;
@Mixin(targets = {"net/minecraftforge/registries/NamespacedWrapper"}, priority = 500)
public abstract class NamespacedWrapperMixin<T> {
@Shadow(aliases = {"tags"}) private volatile Map<TagKey<T>, HolderSet.Named<T>> tags;
@Shadow(aliases = {"createTag"}) protected abstract HolderSet.Named<T> m_211067_(TagKey<T> key);
/**
* @author embeddedt (issue found by Uncandango)
* @reason vanilla does not use the correct double-checked locking paradigm, which leads to race conditions
*/
@Overwrite
public HolderSet.Named<T> getOrCreateTag(TagKey<T> key) {
HolderSet.Named<T> named = this.tags.get(key);
if (named == null) {
// synchronize and check again - this is the bugfix
synchronized (this) {
named = this.tags.get(key);
if (named == null) {
named = this.m_211067_(key);
Map<TagKey<T>, HolderSet.Named<T>> map = new IdentityHashMap<>(this.tags);
map.put(key, named);
this.tags = map;
}
}
}
return named;
}
}

View File

@ -0,0 +1,49 @@
package org.embeddedt.modernfix.common.mixin.bugfix.concurrency;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.client.Minecraft;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.PreparableReloadListener;
import net.minecraft.server.packs.resources.ReloadableResourceManager;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoadingStage;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(ReloadableResourceManager.class)
@ClientOnlyMixin
public abstract class ReloadableResourceManagerMixin {
@Shadow
@Final
private PackType type;
@Shadow
public abstract void registerReloadListener(PreparableReloadListener listener);
/**
* @author embeddedt
* @reason complain loudly when reload listeners are being registered too late in a way that would cause
* concurrency issues, and prevent them from crashing the game
*/
@WrapMethod(method = "registerReloadListener")
private void checkCallingThread(PreparableReloadListener listener, Operation<Void> original) {
if (ModernFixForge.registryEventsFired && this.type == PackType.CLIENT_RESOURCES
&& (Object)this == Minecraft.getInstance().getResourceManager()
&& !Minecraft.getInstance().isSameThread()) {
ModernFix.LOGGER.error("A mod is calling registerReloadListener at the wrong time. This will cause random concurrency crashes when ModernFix is not installed. Please report this to them. If you are a modder, refer to https://github.com/embeddedt/ModernFix/wiki/registerReloadListener-called-on-wrong-thread for more information.", new Exception("registerReloadListener called on wrong thread"));
// Defer the call onto the main client thread. There is a decent chance the mod's listener will be
// ignored in this case, but it is more predictable than allowing them to randomly crash the game.
Minecraft.getInstance().tell(() -> this.registerReloadListener(listener));
return;
}
original.call(listener);
}
}

View File

@ -2,9 +2,9 @@ package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.neoforged.neoforge.client.event.RenderLivingEvent;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.minecraftforge.client.event.RenderLivingEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -13,17 +13,18 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(LivingEntityRenderer.class)
@ClientOnlyMixin
public class LivingEntityRendererMixin {
@Redirect(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) {
@Redirect(method = "render(Lnet/minecraft/world/entity/LivingEntity;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z", ordinal = 0))
private boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderLivingEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).getPoseStack().size();
instance.post(event);
if (((RenderLivingEvent.Pre)event).isCanceled()) {
if (instance.post(event)) {
// Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose();
}
return true;
} else {
return false;
}
return event;
}
}

View File

@ -2,9 +2,9 @@ package org.embeddedt.modernfix.common.mixin.bugfix.entity_pose_stack;
import com.mojang.blaze3d.vertex.PoseStack;
import net.minecraft.client.renderer.entity.player.PlayerRenderer;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.neoforge.client.event.RenderPlayerEvent;
import net.minecraftforge.client.event.RenderPlayerEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -13,17 +13,18 @@ import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(PlayerRenderer.class)
@ClientOnlyMixin
public class PlayerRendererMixin {
@Redirect(method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/bus/api/IEventBus;post(Lnet/neoforged/bus/api/Event;)Lnet/neoforged/bus/api/Event;", ordinal = 0))
private Event fireCheckingPoseStack(IEventBus instance, Event event) {
@Redirect(method = "render(Lnet/minecraft/client/player/AbstractClientPlayer;FFLcom/mojang/blaze3d/vertex/PoseStack;Lnet/minecraft/client/renderer/MultiBufferSource;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z", ordinal = 0))
private boolean fireCheckingPoseStack(IEventBus instance, Event event) {
PoseStack stack = ((RenderPlayerEvent)event).getPoseStack();
int size = ((PoseStackAccessor)stack).getPoseStack().size();
instance.post(event);
if (((RenderPlayerEvent.Pre)event).isCanceled()) {
if (instance.post(event)) {
// Pop the stack if someone pushed it in the event
while (((PoseStackAccessor)stack).getPoseStack().size() > size) {
stack.popPose();
}
return true;
} else {
return false;
}
return event;
}
}

View File

@ -0,0 +1,36 @@
package org.embeddedt.modernfix.common.mixin.bugfix.forge_vehicle_packets;
import net.minecraft.network.protocol.game.ServerboundMoveVehiclePacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.network.ServerGamePacketListenerImpl;
import net.minecraft.world.phys.Vec3;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(ServerGamePacketListenerImpl.class)
public class ServerGamePacketListenerImplMixin {
@Shadow public ServerPlayer player;
@Redirect(method = "handleMoveVehicle", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;absMoveTo(DDDFF)V"), require = 0)
private void movePlayerUsingPositionRider(ServerPlayer player, double x, double y, double z, float yRot, float xRot, ServerboundMoveVehiclePacket packet) {
if(player == this.player) {
// use positionRider
Vec3 oldPos = this.player.position();
yRot = this.player.getYRot();
xRot = this.player.getXRot();
float yHeadRot = this.player.getYHeadRot();
this.player.getRootVehicle().positionRider(this.player);
// keep old rotation
this.player.setYRot(yRot);
this.player.setXRot(xRot);
this.player.setYHeadRot(yHeadRot);
// save old position
this.player.xo = oldPos.x;
this.player.yo = oldPos.y;
this.player.zo = oldPos.z;
} else
player.absMoveTo(x, y, z, yRot, xRot);
}
}

View File

@ -86,11 +86,9 @@ public abstract class LevelChunkMixin extends ChunkAccess {
}
BlockEntity blockEntity = this.getBlockEntity(pos.immutable(), LevelChunk.EntityCreationType.IMMEDIATE);
String blockName = state.getBlock().toString();
if (blockEntity != null) {
ModernFix.LOGGER.warn("Created missing block entity for {} at {}", blockName, pos.toShortString());
} else {
ModernFix.LOGGER.error("Block entity is missing for {} at {}, but could not be created", blockName, pos.toShortString());
if (blockEntity != null && ModernFix.LOGGER.isDebugEnabled()) {
String blockName = state.getBlock().toString();
ModernFix.LOGGER.debug("Created missing block entity for {} at {}", blockName, pos.toShortString());
}
}
}

View File

@ -0,0 +1,55 @@
package org.embeddedt.modernfix.common.mixin.bugfix.model_data_manager_cme;
import net.minecraft.client.Minecraft;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraftforge.client.model.data.ModelDataManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
/**
* Fix several concurrency issues in the default ModelDataManager.
*/
@Mixin(ModelDataManager.class)
@ClientOnlyMixin
public abstract class ModelDataManagerMixin {
@Shadow protected abstract void refreshAt(ChunkPos chunk);
@Shadow @Final private Map<ChunkPos, Set<BlockPos>> needModelDataRefresh;
/**
* Make the set of positions to refresh a real concurrent hash set rather than relying on synchronizedSet,
* because the returned iterator won't be thread-safe otherwise. See https://github.com/AppliedEnergistics/Applied-Energistics-2/issues/7511
*/
@ModifyArg(method = "requestRefresh", at = @At(value = "INVOKE", target = "Ljava/util/Map;computeIfAbsent(Ljava/lang/Object;Ljava/util/function/Function;)Ljava/lang/Object;", ordinal = 0), index = 1, remap = false)
private Function<ChunkPos, Set<BlockPos>> changeTypeOfSetUsed(Function<ChunkPos, Set<BlockPos>> mappingFunction) {
return pos -> ConcurrentHashMap.newKeySet();
}
@Redirect(method = "getAt(Lnet/minecraft/world/level/ChunkPos;)Ljava/util/Map;", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/model/data/ModelDataManager;refreshAt(Lnet/minecraft/world/level/ChunkPos;)V"), remap = false)
private void onlyRefreshOnMainThread(ModelDataManager instance, ChunkPos pos) {
// Only refresh model data on the main thread. This prevents calling getBlockEntity from worker threads
// which could cause weird CMEs or other behavior.
// Avoid the loop if no model data needs to be refreshed, to prevent unnecessary allocation.
if(Minecraft.getInstance().isSameThread() && !needModelDataRefresh.isEmpty()) {
// Refresh the given chunk, and all its neighbors. This is less efficient than the default code
// but we have no choice since we need to not do refreshing on workers, and blocks might
// try to access model data in neighboring chunks.
for(int x = -1; x <= 1; x++) {
for(int z = -1; z <= 1; z++) {
refreshAt(new ChunkPos(pos.x + x, pos.z + z));
}
}
}
}
}

View File

@ -0,0 +1,52 @@
package org.embeddedt.modernfix.common.mixin.bugfix.paper_chunk_patches;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.datafixers.util.Either;
import net.minecraft.server.level.*;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin {
@Shadow @Final private BlockableEventLoop<Runnable> mainThreadExecutor;
/* https://github.com/PaperMC/Paper/blob/ver/1.17.1/patches/server/0752-Fix-chunks-refusing-to-unload-at-low-TPS.patch */
@ModifyArg(method = "prepareAccessibleChunk", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenApplyAsync(Ljava/util/function/Function;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 1)
private Executor useMainThreadExecutor(Executor executor) {
return this.mainThreadExecutor;
}
/**
* @author embeddedt
* @reason 1.17+ uses getNow to check if the parent future is ready, and calls scheduleChunkGeneration as soon as
* it is found to not be ready. In the latter scenario, a massive number of extra CompletableFutures are allocated
* even if they are not actually necessary if the future is waited for. To prevent this, if the parent future
* is not done, we wait for it to complete and then retry schedule(). This will either detect an adequate
* status and return a loading future, or re-enter this injector with the parent future completed, in which case
* we proceed to schedule generation as originally requested.
*/
@WrapOperation(method = "schedule", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ChunkMap;scheduleChunkGeneration(Lnet/minecraft/server/level/ChunkHolder;Lnet/minecraft/world/level/chunk/ChunkStatus;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> mfix$avoidSchedulingGenerationPrematurely(ChunkMap map, ChunkHolder holder, ChunkStatus status, Operation<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> original) {
if (!status.hasLoadDependencies()) {
var parentFuture = holder.getOrScheduleFuture(status.getParent(), map);
if (!parentFuture.isDone()) {
return parentFuture.thenComposeAsync(
either -> map.schedule(holder, status),
this.mainThreadExecutor
);
}
}
return original.call(map, holder, status);
}
}

View File

@ -1,9 +1,15 @@
package org.embeddedt.modernfix.common.mixin.bugfix.recipe_book_type_desync;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.stats.RecipeBookSettings;
import net.minecraft.world.inventory.RecipeBookType;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.packet.NetworkUtils;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@ -30,14 +36,12 @@ public class RecipeBookSettingsMixin {
}
mfix$maxVanillaOrdinal = ord;
}
/*
@Redirect(method = "read(Lnet/minecraft/network/FriendlyByteBuf;)Lnet/minecraft/stats/RecipeBookSettings;", at = @At(value = "INVOKE", target = "Lnet/minecraft/network/FriendlyByteBuf;readBoolean()Z"))
private static boolean useDefaultBooleanIfVanilla(FriendlyByteBuf buf, @Local(ordinal = 0) RecipeBookType type) {
if(type.ordinal() >= (mfix$maxVanillaOrdinal + 1)) {
if(type.ordinal() >= (mfix$maxVanillaOrdinal + 1) && NetworkUtils.isCurrentlyVanilla) {
ModernFix.LOGGER.warn("Not reading recipe book data for type '{}' as we are using vanilla connection", type.name());
return false; // skip actually reading buffer
}
return buf.readBoolean();
}
*/
}

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.bugfix.registry_ops_cme;
import net.minecraft.core.Registry;
import net.minecraft.resources.RegistryOps;
import net.minecraft.resources.ResourceKey;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
@Mixin(targets = {"net/minecraft/resources/RegistryOps$1"})
public class RegistryOpsMemoizedMixin {
@Shadow @Final @Mutable
private Map<ResourceKey<? extends Registry<?>>, Optional<? extends RegistryOps.RegistryInfo<?>>> lookups;
@Inject(method = "<init>", at = @At("RETURN"))
private void useConcurrentMap(RegistryOps.RegistryInfoLookup registryInfoLookup, CallbackInfo ci) {
this.lookups = new ConcurrentHashMap<>(this.lookups);
}
}

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.common.mixin.bugfix.removed_dimensions;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
@Mixin(LevelStorageSource.class)
public class LevelStorageSourceMixin {
@ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lcom/mojang/serialization/DataResult;getOrThrow(ZLjava/util/function/Consumer;)Ljava/lang/Object;", ordinal = 0), index = 0)
private static boolean alwaysAllowPartialDimensions(boolean flag) {
return true;
}
}

View File

@ -24,7 +24,7 @@ public class MinecraftMixin {
/**
* To mitigate the effect of leaked client worlds, clear most of the data structures that waste memory.
*/
@Inject(method = "disconnect(Lnet/minecraft/client/gui/screens/Screen;Z)V", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Minecraft;level:Lnet/minecraft/client/multiplayer/ClientLevel;"))
@Inject(method = "clearLevel(Lnet/minecraft/client/gui/screens/Screen;)V", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/Minecraft;level:Lnet/minecraft/client/multiplayer/ClientLevel;"))
private void clearLevelDataForLeaks(CallbackInfo ci) {
if(this.level != null) {
try {

View File

@ -1,8 +1,11 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.server.Bootstrap;
import org.embeddedt.modernfix.util.TimeFormatter;
import net.minecraftforge.network.NetworkConstants;
import org.embeddedt.modernfix.forge.classloading.ManifestCompactor;
import org.slf4j.Logger;
import org.embeddedt.modernfix.forge.load.ModWorkManagerQueue;
import org.embeddedt.modernfix.util.TimeFormatter;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -22,6 +25,14 @@ public class BootstrapMixin {
private static void doModernFixBootstrap(CallbackInfo ci) {
if(!isBootstrapped) {
LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L));
ModWorkManagerQueue.replace();
ManifestCompactor.compactManifests();
}
}
/* for https://github.com/MinecraftForge/MinecraftForge/issues/9505 */
@Inject(method = "bootStrap", at = @At("RETURN"))
private static void doClassloadHack(CallbackInfo ci) {
NetworkConstants.init();
}
}

View File

@ -0,0 +1,31 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraftforge.forgespi.language.IModInfo;
import net.minecraftforge.logging.CrashReportAnalyser;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
@Mixin(CrashReportAnalyser.class)
public class CrashReportAnalyserMixin {
@Shadow @Final private static Map<IModInfo, String[]> SUSPECTED_MODS;
/**
* @author embeddedt
* @reason Remove ModernFix from the list of suspected mods when a crash happens. Otherwise, we get blamed
* for "registry object not present" crashes if users don't interpret the crash before reporting
* it.
*
* It seems unlikely ModernFix will simultaneously cause a crash while it's not obvious it caused it.
*/
@Inject(method = "buildSuspectedModsSection", at = @At("HEAD"), require = 0, remap = false)
private static void removeOurselvesFromSuspectedMods(StringBuilder stringBuilder, CallbackInfo ci) {
SUSPECTED_MODS.keySet().removeIf(iModInfo -> iModInfo.getModId().equals("modernfix"));
}
}

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraftforge.registries.GameData;
import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = GameData.class, remap = false)
public class GameDataMixin {
@Inject(method = "postRegisterEvents", at = @At("RETURN"))
private static void markPosted(CallbackInfo ci) {
ModernFixForge.registryEventsFired = true;
}
}

View File

@ -4,7 +4,7 @@ import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import net.minecraft.Util;
import net.minecraft.server.MinecraftServer;
import org.embeddedt.modernfix.duck.ITimeTrackingServer;
import org.embeddedt.modernfix.neoforge.load.MinecraftServerReloadTracker;
import org.embeddedt.modernfix.forge.load.MinecraftServerReloadTracker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.network.Connection;
import net.minecraftforge.network.NetworkHooks;
import org.embeddedt.modernfix.forge.packet.NetworkUtils;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(NetworkHooks.class)
public abstract class NetworkHooksMixin {
@Shadow public static boolean isVanillaConnection(Connection manager) {
throw new AssertionError();
}
@Inject(method = "handleClientLoginSuccess", at = @At("RETURN"), remap = false)
private static void setVanillaGlobalFlag(Connection manager, CallbackInfo ci) {
NetworkUtils.isCurrentlyVanilla = isVanillaConnection(manager);
}
}

View File

@ -0,0 +1,18 @@
package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.world.level.chunk.Palette;
import net.minecraft.world.level.chunk.PalettedContainer;
import org.embeddedt.modernfix.chunk.ExtendedPalettedContainer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(PalettedContainer.class)
public class PalettedContainerMixin<T> implements ExtendedPalettedContainer<T> {
@Shadow
private volatile PalettedContainer.Data<T> data;
@Override
public Palette<T> mfix$getPalette() {
return this.data.palette();
}
}

View File

@ -3,7 +3,7 @@ package org.embeddedt.modernfix.common.mixin.core;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.server.WorldLoader;
import org.embeddedt.modernfix.neoforge.load.MinecraftServerReloadTracker;
import org.embeddedt.modernfix.forge.load.MinecraftServerReloadTracker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.mixin.devenv;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.registries.ForgeRegistry;
import net.minecraftforge.registries.GameData;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(GameData.class)
public class GameDataMixin {
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/registries/ForgeRegistry;dump(Lnet/minecraft/resources/ResourceLocation;)V", remap = false))
private static void noDump(ForgeRegistry<?> reg, ResourceLocation id) {
}
}

View File

@ -1,9 +1,9 @@
package org.embeddedt.modernfix.common.mixin.feature.branding;
import com.google.common.collect.ImmutableList;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModList;
import net.neoforged.neoforge.internal.BrandingControl;
import net.minecraftforge.internal.BrandingControl;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@ -14,7 +14,7 @@ import java.util.Optional;
@Mixin(value = BrandingControl.class, remap = false, priority = 1100)
public class BrandingControlMixin {
@Inject(method = "computeBranding", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModList;get()Lnet/neoforged/fml/ModList;"), locals = LocalCapture.CAPTURE_FAILHARD, require = 0)
@Inject(method = "computeBranding", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModList;get()Lnet/minecraftforge/fml/ModList;"), locals = LocalCapture.CAPTURE_FAILHARD, require = 0)
private static void addModernFixBranding(CallbackInfo ci, ImmutableList.Builder<String> builder) {
Optional<? extends ModContainer> mfContainer = ModList.get().getModContainerById("modernfix");
if(mfContainer.isPresent())

View File

@ -1,6 +1,6 @@
package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads;
import net.minecraft.client.renderer.chunk.SectionRenderDispatcher;
import net.minecraft.client.renderer.chunk.ChunkRenderDispatcher;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -11,7 +11,7 @@ import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Mixin(SectionRenderDispatcher.class)
@Mixin(ChunkRenderDispatcher.class)
@ClientOnlyMixin
public class ChunkRenderDispatcherMixin {
private static final Executor MFIX_CHUNK_BUILD_EXECUTOR = new ThreadPoolExecutor(1, computeNumThreads(), 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>());

View File

@ -1,7 +1,7 @@
package org.embeddedt.modernfix.common.mixin.feature.cause_lag_by_disabling_threads;
import net.minecraft.Util;
import org.embeddedt.modernfix.util.DirectExecutorService;
import org.embeddedt.modernfix.util.SingleThreadedWorkerService;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
@ -12,5 +12,5 @@ import java.util.concurrent.ExecutorService;
@Mixin(Util.class)
public class UtilMixin {
@Shadow @Final @Mutable
private static final ExecutorService BACKGROUND_EXECUTOR = new DirectExecutorService();
private static final ExecutorService BACKGROUND_EXECUTOR = new SingleThreadedWorkerService();
}

View File

@ -5,8 +5,7 @@ import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.functions.CommandFunction;
import net.minecraft.commands.CommandFunction;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.ServerFunctionManager;
import org.embeddedt.modernfix.duck.IProfilingServerFunctionManager;
@ -30,22 +29,22 @@ public class ServerFunctionManagerMixin implements IProfilingServerFunctionManag
private final Map<ResourceLocation, Stopwatch> mfix$functionWatches = new Object2ObjectOpenHashMap<>();
@Inject(method = "executeTagFunctions", at = @At("HEAD"))
private void resetWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
private void resetWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
mfix$functionWatches.values().forEach(Stopwatch::reset);
}
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V"))
private void startWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction<CommandSourceStack> function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)I"))
private void startWatch(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Local(ordinal = 0) CommandFunction function, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
watchRef.set(null);
if (identifier == TICK_FUNCTION_TAG) {
var watch = mfix$functionWatches.computeIfAbsent(function.id(), i -> Stopwatch.createUnstarted());
var watch = mfix$functionWatches.computeIfAbsent(function.getId(), i -> Stopwatch.createUnstarted());
watch.start();
watchRef.set(watch);
}
}
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/functions/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)V", shift = At.Shift.AFTER))
private void stopWatch(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
@Inject(method = "executeTagFunctions", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ServerFunctionManager;execute(Lnet/minecraft/commands/CommandFunction;Lnet/minecraft/commands/CommandSourceStack;)I", shift = At.Shift.AFTER))
private void stopWatch(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci, @Share("stopwatch") LocalRef<Stopwatch> watchRef) {
var watch = watchRef.get();
if (watch != null && watch.isRunning()) {
watch.stop();
@ -53,7 +52,7 @@ public class ServerFunctionManagerMixin implements IProfilingServerFunctionManag
}
@Inject(method = "executeTagFunctions", at = @At("RETURN"))
private void pruneUnusedWatches(Collection<CommandFunction<CommandSourceStack>> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
private void pruneUnusedWatches(Collection<CommandFunction> functionObjects, ResourceLocation identifier, CallbackInfo ci) {
mfix$functionWatches.values().removeIf(watch -> watch.elapsed().isZero());
}

View File

@ -5,7 +5,7 @@ import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@Mixin(targets = "net/neoforged/neoforge/event/AddReloadListenerEvent$WrappedStateAwareListener")
@Mixin(targets = "net/minecraftforge/event/AddReloadListenerEvent$WrappedStateAwareListener")
public abstract class AddReloadListenerEventWrapperMixin implements PreparableReloadListener {
@Shadow @Final private PreparableReloadListener wrapped;

View File

@ -1,46 +1,52 @@
package org.embeddedt.modernfix.common.mixin.feature.registry_event_progress;
import net.neoforged.bus.api.Event;
import net.neoforged.bus.api.EventPriority;
import net.neoforged.fml.ModList;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.ModLoadingContext;
import net.neoforged.fml.event.IModBusEvent;
import net.neoforged.fml.loading.progress.StartupNotificationManager;
import net.neoforged.neoforge.registries.GameData;
import net.neoforged.neoforge.registries.RegisterEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.StartupMessageManager;
import net.minecraftforge.fml.event.IModBusEvent;
import net.minecraftforge.registries.GameData;
import net.minecraftforge.registries.RegisterEvent;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.forge.util.AsyncLoadingScreen;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = GameData.class, remap = false)
@ClientOnlyMixin
public class GameDataMixin {
@Redirect(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEventWrapContainerInModOrder(Lnet/neoforged/bus/api/Event;)V"))
private static <T extends Event & IModBusEvent> void postWithProgressBar(T event) {
if(ModLoader.hasErrors()) {
return;
}
RegisterEvent registryEvent = (RegisterEvent)event;
// We control phases ourselves so we can make a separate progress bar for each phase.
String registryName = registryEvent.getRegistryKey().location().toString();
for(EventPriority phase : EventPriority.values()) {
// FIXME need to use prepend rather than append for it to be visible for now
var pb = StartupNotificationManager.prependProgressBar(registryName, ModList.get().size());
try {
ModList.get().forEachModInOrder(mc -> {
ModLoadingContext.get().setActiveContainer(mc);
pb.label(pb.name() + " - " + mc.getModInfo().getDisplayName());
pb.increment();
mc.acceptEvent(phase, event);
ModLoadingContext.get().setActiveContainer(null);
});
} finally {
pb.complete();
}
}
private static AsyncLoadingScreen mfix$asyncScreen;
@Inject(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Ljava/util/Set;iterator()Ljava/util/Iterator;", ordinal = 0))
private static void createAsyncScreen(CallbackInfo ci) {
mfix$asyncScreen = new AsyncLoadingScreen();
}
@Inject(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Ljava/lang/RuntimeException;getSuppressed()[Ljava/lang/Throwable;", ordinal = 0))
private static void closeAsyncScreen(CallbackInfo ci) {
mfix$asyncScreen.close();
mfix$asyncScreen = null;
}
@Redirect(method = "postRegisterEvents", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModLoader;postEventWrapContainerInModOrder(Lnet/minecraftforge/eventbus/api/Event;)V"))
private static <T extends Event & IModBusEvent> void swapThreadAndPost(ModLoader loader, T event) {
RegisterEvent registryEvent = (RegisterEvent)event;
var pb = StartupMessageManager.addProgressBar(registryEvent.getRegistryKey().location().toString(), ModList.get().size());
try {
loader.postEventWithWrapInModOrder(event, (mc, e) -> {
ModLoadingContext.get().setActiveContainer(mc);
pb.label(pb.name() + " - " + mc.getModInfo().getDisplayName());
pb.increment();
}, (mc, e) -> {
ModLoadingContext.get().setActiveContainer(null);
});
} finally {
pb.complete();
}
}
}

View File

@ -0,0 +1,36 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import org.embeddedt.modernfix.entity.AttributeInstanceTemplates;
import org.embeddedt.modernfix.forge.init.ModernFixForge;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
@Mixin(AttributeSupplier.Builder.class)
public class AttributeSupplierBuilderMixin {
@Shadow
@Final
private Map<Attribute, AttributeInstance> builder;
/**
* @author embeddedt
* @reason canonicalize identical AttributeInstance templates, many entities are created with the same values
*/
@Inject(method = "build", at = @At(value = "NEW", target = "(Ljava/util/Map;)Lnet/minecraft/world/entity/ai/attributes/AttributeSupplier;"))
private void deduplicateInstances(CallbackInfoReturnable<AttributeSupplier> cir) {
// The interning has overhead, so we only apply it early during the launch, when mods are normally
// registering the custom attribute suppliers.
if (ModernFixForge.registryEventsFired) {
return;
}
this.builder.replaceAll((a, i) -> AttributeInstanceTemplates.intern(i));
}
}

View File

@ -0,0 +1,33 @@
package org.embeddedt.modernfix.common.mixin.perf.attribute_supplier_dedup;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
import net.minecraft.world.entity.ai.attributes.AttributeSupplier;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Map;
@Mixin(AttributeSupplier.class)
public class AttributeSupplierMixin {
@Shadow
@Final
@Mutable
private Map<Attribute, AttributeInstance> instances;
/**
* @author embeddedt
* @reason more compact than ImmutableMap due to less wrapper objects, and we do not
* care about insertion order in this context
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void useCompactJavaMap(Map<Attribute, AttributeInstance> instances, CallbackInfo ci) {
this.instances = new Object2ObjectOpenHashMap<>(this.instances);
}
}

View File

@ -0,0 +1,52 @@
package org.embeddedt.modernfix.common.mixin.perf.blast_search_trees;
import net.minecraft.client.KeyMapping;
import net.minecraft.client.Minecraft;
import net.minecraft.client.searchtree.SearchRegistry;
import net.minecraft.world.item.ItemStack;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.searchtree.RecipeBookSearchTree;
import org.embeddedt.modernfix.searchtree.SearchTreeProviderRegistry;
import org.lwjgl.glfw.GLFW;
import org.lwjgl.glfw.GLFWErrorCallback;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.List;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public abstract class MinecraftMixin {
@Shadow @Final private SearchRegistry searchRegistry;
@Shadow public abstract <T> void populateSearchTree(SearchRegistry.Key<T> key, List<T> list);
@Inject(method = "createSearchTrees", at = @At("HEAD"), cancellable = true)
private void replaceSearchTrees(CallbackInfo ci) {
SearchTreeProviderRegistry.Provider provider = SearchTreeProviderRegistry.getSearchTreeProvider();
if(provider == null)
return;
ModernFix.LOGGER.info("Replacing search trees with '{}' provider", provider.getName());
SearchRegistry.TreeBuilderSupplier<ItemStack> nameSupplier = list -> provider.getSearchTree(false);
SearchRegistry.TreeBuilderSupplier<ItemStack> tagSupplier = list -> provider.getSearchTree(true);
this.searchRegistry.register(SearchRegistry.CREATIVE_NAMES, nameSupplier);
this.searchRegistry.register(SearchRegistry.CREATIVE_TAGS, tagSupplier);
this.searchRegistry.register(SearchRegistry.RECIPE_COLLECTIONS, list -> new RecipeBookSearchTree(provider.getSearchTree(false), list));
ModernFixPlatformHooks.INSTANCE.registerCreativeSearchTrees(this.searchRegistry, nameSupplier, tagSupplier, this::populateSearchTree);
// grab components for all key mappings in order to prevent them from being loaded off-thread later
// this populates the LazyLoadedValues
// we also need to suppress GLFW errors to prevent crashes if a key is missing
GLFWErrorCallback oldCb = GLFW.glfwSetErrorCallback(null);
for(KeyMapping mapping : KeyMapping.ALL.values()) {
mapping.getTranslatedKeyMessage();
}
GLFW.glfwSetErrorCallback(oldCb);
ci.cancel();
}
}

View File

@ -3,6 +3,7 @@ package org.embeddedt.modernfix.common.mixin.perf.cache_profile_texture_url;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.mojang.authlib.minecraft.MinecraftProfileTexture;
import net.minecraft.client.resources.SkinManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
@ -12,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.Redirect;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
@Mixin(targets = {"net/minecraft/client/resources/SkinManager$TextureCache" })
@Mixin(SkinManager.class)
@ClientOnlyMixin
public class SkinManagerMixin {
@Unique
@ -21,7 +22,7 @@ public class SkinManagerMixin {
.concurrencyLevel(1)
.build();
@Redirect(method = { "getOrLoad", "registerTexture" },
@Redirect(method = "registerTexture(Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;Lcom/mojang/authlib/minecraft/MinecraftProfileTexture$Type;Lnet/minecraft/client/resources/SkinManager$SkinTextureCallback;)Lnet/minecraft/resources/ResourceLocation;",
at = @At(value = "INVOKE", target = "Lcom/mojang/authlib/minecraft/MinecraftProfileTexture;getHash()Ljava/lang/String;", remap = false))
private String useCachedHash(MinecraftProfileTexture texture) {
// avoid lambda allocation for common case

View File

@ -1,68 +1,198 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.Util;
import net.minecraft.core.Holder;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.nbt.*;
import net.minecraft.resources.RegistryOps;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.biome.BiomeSource;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.StructureSet;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.duck.IChunkGenerator;
import org.embeddedt.modernfix.duck.IServerLevel;
import org.embeddedt.modernfix.world.StrongholdLocationCache;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import org.spongepowered.asm.mixin.injection.Redirect;
import java.lang.ref.WeakReference;
import java.lang.ref.SoftReference;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Mixin(ChunkGeneratorStructureState.class)
public class ChunkGeneratorMixin implements IChunkGenerator {
private WeakReference<ServerLevel> mfix$serverLevel;
@Shadow
@Final
private long concentricRingsSeed;
@Shadow
@Final
private BiomeSource biomeSource;
private Path mfix$dimensionPath;
private MinecraftServer mfix$server;
private SoftReference<Map<String, List<ChunkPos>>> mfix$cachedPositions = new SoftReference<>(null);
private static final String CACHE_FILENAME = "mfix_stronghold_cache_v2.nbt";
@Override
public void mfix$setAssociatedServerLevel(ServerLevel level) {
mfix$serverLevel = new WeakReference<>(level);
public void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server) {
this.mfix$dimensionPath = cachePath;
this.mfix$server = server;
}
@Inject(method = "generateRingPositions", at = @At("HEAD"), cancellable = true)
private void useCachedDataIfAvailable(Holder<StructureSet> structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable<CompletableFuture<List<ChunkPos>>> cir) {
if(placement.count() == 0)
return;
ServerLevel level = searchLevel();
if(level == null)
return;
StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache();
List<ChunkPos> positions = cache.getChunkPosList();
if(positions.isEmpty())
return;
ModernFix.LOGGER.debug("Loaded stronghold cache for dimension {} with {} positions", level.dimension().location(), positions.size());
cir.setReturnValue(CompletableFuture.completedFuture(positions));
@WrapMethod(method = "generateRingPositions")
private CompletableFuture<List<ChunkPos>> modernfix$cacheRingPositions(Holder<StructureSet> structureSet,
ConcentricRingsStructurePlacement placement,
Operation<CompletableFuture<List<ChunkPos>>> original,
@Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
if (this.mfix$server == null || this.mfix$dimensionPath == null) {
return original.call(structureSet, placement);
}
String cacheKey = mfix$makeCacheKey(placement);
// Try reading from cache
List<ChunkPos> cached = mfix$readFromCache(cacheKey);
if (cached != null) {
ModernFix.LOGGER.debug("Using cached stronghold positions for {}", cacheKey);
return CompletableFuture.completedFuture(List.copyOf(cached));
}
var server = this.mfix$server;
ExecutorService strongholdPool = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors() - 2));
threadPoolRef.set(strongholdPool);
try {
return original.call(structureSet, placement).thenApplyAsync(positions -> {
// Skip write if server exited before we finished
if (server.isRunning()) {
mfix$writeToCache(cacheKey, positions);
}
return positions;
}, Util.ioPool());
} finally {
strongholdPool.shutdown();
}
}
private ServerLevel searchLevel() {
if(mfix$serverLevel != null)
return mfix$serverLevel.get();
else
/**
* @author embeddedt
* @reason Ring position calculation is often not required for initial chunk generation, but the tasks still occupy
* CPU time on the main worker pool and prevent higher priority work from progressing. To fix this we use a
* dedicated pool.
*/
@Redirect(method = "generateRingPositions", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;"))
private ExecutorService useDedicatedService(@Share("threadPool") LocalRef<ExecutorService> threadPoolRef) {
return threadPoolRef.get();
}
private String mfix$makeCacheKey(ConcentricRingsStructurePlacement placement) {
RegistryOps<Tag> ops = RegistryOps.create(NbtOps.INSTANCE, this.mfix$server.registryAccess());
String placementKey = ConcentricRingsStructurePlacement.CODEC.encodeStart(ops, placement)
.result().map(Tag::toString).orElse(null);
String biomeSourceKey = BiomeSource.CODEC.encodeStart(ops, this.biomeSource)
.result().map(Tag::toString).orElse(null);
if (placementKey == null || biomeSourceKey == null) {
ModernFix.LOGGER.warn("Failed to create cache key for concentric structure placement");
return null;
}
String data = placementKey + ";biomes=" + biomeSourceKey + ";seed=" + this.concentricRingsSeed;
try {
byte[] hash = MessageDigest.getInstance("SHA-256").digest(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(64);
for (byte b : hash) {
sb.append(String.format("%02x", b));
}
return sb.toString();
} catch (NoSuchAlgorithmException e) {
return null;
}
}
@Inject(method = "generateRingPositions", at = @At("RETURN"), cancellable = true)
private void saveCachedData(Holder<StructureSet> structureSet, ConcentricRingsStructurePlacement placement, CallbackInfoReturnable<CompletableFuture<List<ChunkPos>>> cir) {
cir.setReturnValue(cir.getReturnValue().thenApplyAsync(list -> {
if(list.size() == 0)
return list;
ServerLevel level = searchLevel();
if(level != null) {
StrongholdLocationCache cache = ((IServerLevel)level).mfix$getStrongholdCache();
cache.setChunkPosList(list);
ModernFix.LOGGER.debug("Saved stronghold cache for dimension {}", level.dimension().location());
private synchronized List<ChunkPos> mfix$readFromCache(String cacheKey) {
Map<String, List<ChunkPos>> cache = mfix$getOrLoadCache();
return cache.get(cacheKey);
}
private synchronized void mfix$writeToCache(String cacheKey, List<ChunkPos> positions) {
Map<String, List<ChunkPos>> cache = mfix$getOrLoadCache();
cache.put(cacheKey, List.copyOf(positions));
mfix$cachedPositions = new SoftReference<>(cache);
mfix$saveCacheFile(cache);
}
private Map<String, List<ChunkPos>> mfix$getOrLoadCache() {
Map<String, List<ChunkPos>> cache = mfix$cachedPositions.get();
if (cache != null) {
return cache;
}
cache = mfix$loadCacheFile();
mfix$cachedPositions = new SoftReference<>(cache);
return cache;
}
private Map<String, List<ChunkPos>> mfix$loadCacheFile() {
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
if (!Files.exists(file)) {
return new HashMap<>();
}
try {
CompoundTag root = NbtIo.readCompressed(file.toFile());
Map<String, List<ChunkPos>> result = new HashMap<>();
for (String key : root.getAllKeys()) {
if (root.contains(key, Tag.TAG_INT_ARRAY)) {
int[] data = root.getIntArray(key);
if (data.length >= 2 && data.length % 2 == 0) {
List<ChunkPos> positions = new ArrayList<>(data.length / 2);
for (int i = 0; i < data.length; i += 2) {
positions.add(new ChunkPos(data[i], data[i + 1]));
}
result.put(key, positions);
}
}
}
return list;
}, Util.backgroundExecutor()));
return result;
} catch (Exception e) {
ModernFix.LOGGER.warn("Failed to read stronghold cache, will recompute", e);
return new HashMap<>();
}
}
private void mfix$saveCacheFile(Map<String, List<ChunkPos>> cache) {
CompoundTag root = new CompoundTag();
for (var entry : cache.entrySet()) {
List<ChunkPos> positions = entry.getValue();
int[] data = new int[positions.size() * 2];
for (int i = 0; i < positions.size(); i++) {
ChunkPos pos = positions.get(i);
data[i * 2] = pos.x;
data[i * 2 + 1] = pos.z;
}
root.putIntArray(entry.getKey(), data);
}
Path file = mfix$dimensionPath.resolve(CACHE_FILENAME);
try {
NbtIo.writeCompressed(root, file.toFile());
} catch (Exception e) {
ModernFix.LOGGER.warn("Failed to write stronghold cache", e);
}
}
}

View File

@ -0,0 +1,123 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.levelgen.structure.placement.ConcentricRingsStructurePlacement;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ConcentricRingsStructurePlacement.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class ConcentricRingsStructurePlacementMixin {
@Shadow @Final private int distance;
@Shadow @Final private int spread;
@Shadow @Final private int count;
/**
* Maximum per-axis section displacement from the initial ring chunk after biome snapping.
*
* Vanilla calls findBiomeHorizontal with radius=112 blocks. In quart space this is ±28,
* and converting the selected quart back to section coordinates yields at most ±7 chunks
* per axis from the original (initialX, initialZ).
*/
@Unique private static final int MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS = 7;
/**
* Worst-case Euclidean error introduced by rounding:
* initialX/Z = round(cos(angle) * dist), round(sin(angle) * dist).
*/
@Unique private static final double MFIX_MAX_ROUNDING_ERROR = Math.sqrt(2.0) * 0.5;
/**
* Worst-case Euclidean biome-snap displacement when each axis can move by at most 7 chunks.
*/
@Unique private static final double MFIX_MAX_BIOME_SNAP_ERROR = MFIX_MAX_BIOME_SNAP_SECTIONS_PER_AXIS * Math.sqrt(2.0);
/**
* Total conservative positional slack (rounding + biome snap) applied to radial bounds.
*/
@Unique private static final double MFIX_MAX_POSITION_ERROR = MFIX_MAX_ROUNDING_ERROR + MFIX_MAX_BIOME_SNAP_ERROR;
/** Squared chunk-distance below which no ring position can ever land. */
@Unique private long mfix$innerRadiusSq;
/** Squared chunk-distance above which no ring position can ever land. */
@Unique private long mfix$outerRadiusSq;
/**
* Precomputes conservative radial bounds for vanilla's ring placement distance:
* {@code dist = 4*i + i*i1*6 + noise}, where {@code i=distance} and {@code i1=circle}.
*
* - Inner bound uses the minimum possible base term ({@code i1=0} => {@code 4*i}).
* - Outer bound uses the maximum reachable {@code i1} for this ({@code spread,count}) pair.
*
* Both bounds are expanded by {@link #MFIX_MAX_POSITION_ERROR} so we never reject a valid
* chunk produced by rounding and biome snapping.
*/
@Inject(
method = "<init>(Lnet/minecraft/core/Vec3i;Lnet/minecraft/world/level/levelgen/structure/placement/StructurePlacement$FrequencyReductionMethod;FILjava/util/Optional;IIILnet/minecraft/core/HolderSet;)V",
at = @At("RETURN")
)
private void mfix$computeRadiusBounds(CallbackInfo ci) {
double maxNoise = this.distance * 1.25; // (nextDouble() - 0.5) * (distance * 2.5)
// min(dist): 4*i + i*0*6 - maxNoise
double minDist = 4.0 * this.distance - maxNoise;
double safeInnerRadius = minDist - MFIX_MAX_POSITION_ERROR;
this.mfix$innerRadiusSq = (long)Math.max(0.0, Math.floor(safeInnerRadius * safeInnerRadius));
if (this.spread == 0) {
// Vanilla behavior becomes non-finite here (angle += / 0), so keep only inner rejection.
this.mfix$outerRadiusSq = Long.MAX_VALUE;
return;
}
int maxCircle = this.mfix$computeMaxCircleIndex();
// max(dist): 4*i + i*maxCircle*6 + maxNoise
double maxDist = 4.0 * this.distance + (double)this.distance * maxCircle * 6.0 + maxNoise;
double safeOuterRadius = maxDist + MFIX_MAX_POSITION_ERROR;
this.mfix$outerRadiusSq = (long)Math.ceil(safeOuterRadius * safeOuterRadius);
}
/**
* Computes the highest ring index ({@code circle}) that vanilla can reach for this placement.
*
* This mirrors the spread/total update logic in
* {@link net.minecraft.world.level.chunk.ChunkGeneratorStructureState#generateRingPositions},
* but only tracks deterministic loop state (no RNG).
*/
@Unique
private int mfix$computeMaxCircleIndex() {
int ringSpread = this.spread;
int total = 0;
int circle = 0;
while (total + ringSpread < this.count) {
total += ringSpread;
circle++;
ringSpread += 2 * ringSpread / (circle + 1);
ringSpread = Math.min(ringSpread, this.count - total);
}
return circle;
}
/**
* @author embeddedt, GPT-5.3-Codex
* @reason Avoid calling getRingPositionsFor() when we know the current chunk lies outside the region where
* concentric placement can even happen. This is particularly helpful when creating new worlds, because we can
* avoid blocking on the slow noise computations within the spawn region around (0, 0).
*/
@Inject(method = "isPlacementChunk", at = @At("HEAD"), cancellable = true)
private void mfix$earlyRejectByRadius(ChunkGeneratorStructureState structureState, int x, int z,
CallbackInfoReturnable<Boolean> cir) {
long distSq = (long)x * x + (long)z * z;
if (distSq < this.mfix$innerRadiusSq || distSq > this.mfix$outerRadiusSq) {
cir.setReturnValue(false);
}
}
}

View File

@ -1,61 +1,30 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_strongholds;
import net.minecraft.core.Holder;
import net.minecraft.core.RegistryAccess;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkGeneratorStructureState;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.DimensionDataStorage;
import net.minecraft.world.level.storage.WritableLevelData;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.embeddedt.modernfix.duck.IChunkGenerator;
import org.embeddedt.modernfix.duck.IServerLevel;
import org.embeddedt.modernfix.world.StrongholdLocationCache;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Supplier;
@Mixin(ServerLevel.class)
public abstract class ServerLevelMixin extends Level implements IServerLevel {
protected ServerLevelMixin(WritableLevelData arg, ResourceKey<Level> arg2, RegistryAccess arg3, Holder<DimensionType> arg4, Supplier<ProfilerFiller> supplier, boolean bl, boolean bl2, long l, int i) {
super(arg, arg2, arg3, arg4, supplier, bl, bl2, l, i);
}
@Shadow public abstract DimensionDataStorage getDataStorage();
@Shadow @Final private ServerChunkCache chunkSource;
private StrongholdLocationCache mfix$strongholdCache;
public class ServerLevelMixin {
/**
* Initialize the stronghold cache but don't force any structure generation yet.
* @author embeddedt
* @reason Make the dimension path accessible to ChunkGeneratorStructureState.
*/
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V"))
private void hookStrongholdCache(ChunkGeneratorStructureState generator) {
((IChunkGenerator)generator).mfix$setAssociatedServerLevel((ServerLevel)(Object)this);
}
/**
* Now start the stronghold generation process.
*/
@Inject(method = "<init>", at = @At("TAIL"))
private void ensureGeneration(CallbackInfo ci) {
mfix$strongholdCache = this.getDataStorage().computeIfAbsent(
StrongholdLocationCache.factory((ServerLevel)(Object)this),
StrongholdLocationCache.getFileId(this.dimensionTypeRegistration()));
this.chunkSource.getGeneratorState().ensureStructuresGenerated();
}
@Override
public StrongholdLocationCache mfix$getStrongholdCache() {
return mfix$strongholdCache;
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/ChunkGeneratorStructureState;ensureStructuresGenerated()V"))
private void setCachePath(ChunkGeneratorStructureState instance, Operation<Void> original,
@Local(ordinal = 0, argsOnly = true) LevelStorageSource.LevelStorageAccess levelStorageAccess,
@Local(ordinal = 0, argsOnly = true) ResourceKey<Level> dimension,
@Local(ordinal = 0, argsOnly = true) MinecraftServer server) {
((IChunkGenerator)instance).mfix$setStrongholdCachePath(levelStorageAccess.getDimensionPath(dimension), server);
original.call(instance);
}
}

View File

@ -0,0 +1,46 @@
package org.embeddedt.modernfix.common.mixin.perf.cache_upgraded_structures;
import com.mojang.datafixers.DataFixer;
import net.minecraft.core.HolderGetter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.structure.CachingStructureManager;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Optional;
@Mixin(StructureTemplateManager.class)
public class StructureManagerMixin {
@Shadow @Final private DataFixer fixerUpper;
@Shadow private ResourceManager resourceManager;
@Shadow @Final private HolderGetter<Block> blockLookup;
/**
* @author embeddedt
* @reason use our own manager to avoid needless DFU updates
*/
@Overwrite
private Optional<StructureTemplate> loadFromResource(ResourceLocation id) {
ResourceLocation arg = new ResourceLocation(id.getNamespace(), "structures/" + id.getPath() + ".nbt");
try(InputStream stream = this.resourceManager.open(arg)) {
return Optional.of(CachingStructureManager.readStructure(id, this.fixerUpper, stream, this.blockLookup));
} catch(FileNotFoundException e) {
return Optional.empty();
} catch(IOException e) {
ModernFix.LOGGER.error("Can't read structure", e);
return Optional.empty();
}
}
}

View File

@ -1,29 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.capability_list_compaction;
import com.llamalad7.mixinextras.sugar.Local;
import net.neoforged.neoforge.capabilities.BaseCapability;
import net.neoforged.neoforge.capabilities.CapabilityHooks;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import org.embeddedt.modernfix.neoforge.caps.CapProviderGetter;
import org.embeddedt.modernfix.neoforge.caps.ITrackingCapEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(value = CapabilityHooks.class, remap = false)
public class CapabilityHooksMixin {
// Must inject as late as possible to run after mixins that add their own capabilities
// (e.g. https://github.com/SuperMartijn642/Entangled/blob/37f2489d8badc3f52401088d8a6e25d2a63a045c/src/main/java/com/supermartijn642/entangled/mixin/neoforge/CapabilityHooksMixin.java)
@Inject(method = "init", at = @At(value = "RETURN"))
private static void deduplicateCaps(CallbackInfo ci, @Local(ordinal = 0) RegisterCapabilitiesEvent event) {
if(event instanceof ITrackingCapEvent) {
//var stopwatch = Stopwatch.createStarted();
for(BaseCapability<?, ?> cap : ((ITrackingCapEvent)event).mfix$getTrackedCaps()) {
CapProviderGetter.deduplicateCap(cap);
}
//stopwatch.stop();
//ModernFix.LOGGER.info("Deduplicated capability lists in {}", stopwatch);
}
}
}

View File

@ -1,51 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.capability_list_compaction;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.level.ItemLike;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.neoforged.neoforge.capabilities.BaseCapability;
import net.neoforged.neoforge.capabilities.BlockCapability;
import net.neoforged.neoforge.capabilities.EntityCapability;
import net.neoforged.neoforge.capabilities.IBlockCapabilityProvider;
import net.neoforged.neoforge.capabilities.ICapabilityProvider;
import net.neoforged.neoforge.capabilities.ItemCapability;
import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent;
import org.embeddedt.modernfix.neoforge.caps.ITrackingCapEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashSet;
import java.util.Set;
@Mixin(value = RegisterCapabilitiesEvent.class, remap = false)
public class RegisterCapabilitiesEventMixin implements ITrackingCapEvent {
private final Set<BaseCapability<?, ?>> mfix$trackedCapabilities = new HashSet<>();
@Inject(method = "registerBlock", at = @At("HEAD"))
private void trackBlockCap(BlockCapability<?, ?> capability, IBlockCapabilityProvider<?, ?> provider, Block[] blocks, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerBlockEntity", at = @At("HEAD"))
private void trackBlockEntityCap(BlockCapability<?, ?> capability, BlockEntityType<?> type, ICapabilityProvider<?, ?, ?> provider, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerItem", at = @At("HEAD"))
private void trackItemCap(ItemCapability<?, ?> capability, ICapabilityProvider<?, ?, ?> provider, ItemLike[] items, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Inject(method = "registerEntity", at = @At("HEAD"))
private void trackEntityCap(EntityCapability<?, ?> capability, EntityType<?> type, ICapabilityProvider<?, ?, ?> provider, CallbackInfo ci) {
mfix$trackedCapabilities.add(capability);
}
@Override
public Set<BaseCapability<?, ?>> mfix$getTrackedCaps() {
return mfix$trackedCapabilities;
}
}

View File

@ -1,7 +1,9 @@
package org.embeddedt.modernfix.common.mixin.perf.chunk_meshing;
import net.minecraft.client.renderer.chunk.SectionCompiler;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.chunk.RenderChunkRegion;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.state.BlockState;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.util.blockpos.SectionBlockPosIterator;
@ -9,7 +11,7 @@ import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@Mixin(value = SectionCompiler.class, priority = 2000)
@Mixin(targets = { "net/minecraft/client/renderer/chunk/ChunkRenderDispatcher$RenderChunk$RebuildTask"}, priority = 2000)
@ClientOnlyMixin
@RequiresMod("!fluidlogged")
public class RebuildTaskMixin {
@ -21,4 +23,13 @@ public class RebuildTaskMixin {
private Iterable<BlockPos> fastBetweenClosed(BlockPos firstPos, BlockPos secondPos) {
return () -> new SectionBlockPosIterator(firstPos);
}
/**
* @author embeddedt
* @reason RenderChunkRegion.getBlockState is expensive, avoid calling it multiple times for the same position
*/
@Redirect(method = "compile", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/chunk/RenderChunkRegion;getBlockState(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/state/BlockState;", ordinal = 1), require = 0)
private BlockState useExistingBlockState(RenderChunkRegion instance, BlockPos pos, @Local(ordinal = 0) BlockState state) {
return state;
}
}

View File

@ -0,0 +1,40 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_entity_models;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.model.geom.ModelPart;
import net.minecraft.client.model.geom.builders.CubeDefinition;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
@Mixin(CubeDefinition.class)
@ClientOnlyMixin
public class CubeDefinitionMixin {
@Unique
private static final ConcurrentHashMap<List<Object>, ModelPart.Cube> MFIX_CUBE_CACHE = new ConcurrentHashMap<>();
/**
* @author embeddedt
* @reason deduplicate creation of Cube objects
*/
@WrapOperation(method = "bake", at = @At(value = "NEW", target = "(IIFFFFFFFFFZFFLjava/util/Set;)Lnet/minecraft/client/model/geom/ModelPart$Cube;"))
private ModelPart.Cube modernfix$deduplicateCube(int texCoordU, int texCoordV, float originX, float originY, float originZ,
float dimensionX, float dimensionY, float dimensionZ, float gtowX,
float growY, float growZ, boolean mirror, float texScaleU,
float texScaleV, Set visibleFaces,
Operation<ModelPart.Cube> original) {
List<Object> cacheKey = List.of(texCoordU, texCoordV, originX, originY, originZ, dimensionX, dimensionY, dimensionZ, gtowX, growY, growZ, mirror, texScaleU, texScaleV, visibleFaces);
var cube = MFIX_CUBE_CACHE.get(cacheKey);
if (cube == null) {
cube = original.call((Object[])cacheKey.toArray());
MFIX_CUBE_CACHE.put(cacheKey, cube);
}
return cube;
}
}

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.lighting.ChunkSkyLightSources;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@Mixin(ChunkAccess.class)
public class ChunkAccessMixin {
@Shadow
@Final
@Mutable
protected LevelChunkSection[] sections;
@Shadow
protected ChunkSkyLightSources skyLightSources;
}

View File

@ -0,0 +1,22 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_imposterprotochunks;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import net.minecraft.world.level.chunk.LevelChunk;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ImposterProtoChunk.class)
public abstract class ImposterProtoChunkMixin extends ChunkAccessMixin {
/**
* @author embeddedt
* @reason ImposterProtoChunks allocate their own LevelChunkSection objects etc. which wastes quite
* a bit of memory
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceDuplicateObjects(LevelChunk wrapped, boolean allowWrites, CallbackInfo ci) {
this.sections = wrapped.getSections();
this.skyLightSources = wrapped.getSkyLightSources();
}
}

View File

@ -35,7 +35,7 @@ public class BlockStateDataMixin {
t = compactTag(ct);
}
t = TAG_INTERNER.addOrGet(t);
entries[i++] = Map.entry(key, t);
entries[i++] = Map.entry(key.intern(), t);
}
return new CompoundTag(Map.ofEntries(entries));
}

View File

@ -1,8 +1,7 @@
package org.embeddedt.modernfix.common.mixin.perf.compact_mojang_registries;
import com.mojang.serialization.Lifecycle;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.RegistrationInfo;
import net.minecraft.resources.ResourceKey;
import org.embeddedt.modernfix.annotation.IgnoreOutsideDev;
import org.embeddedt.modernfix.registry.LifecycleMap;
import org.spongepowered.asm.mixin.Final;
@ -21,10 +20,10 @@ public abstract class MappedRegistryMixin<T> {
@Shadow
@Final
@Mutable
private Map<ResourceKey<T>, RegistrationInfo> registrationInfos;
private Map<T, Lifecycle> lifecycles;
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceStorage(CallbackInfo ci) {
this.registrationInfos = new LifecycleMap<>();
this.lifecycles = new LifecycleMap<>();
}
}

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.compress_unihex_font;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.mojang.serialization.MapCodec;
import net.minecraft.client.gui.font.providers.GlyphProviderDefinition;
import net.minecraft.client.gui.font.providers.GlyphProviderType;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.render.font.LazyGlyphProvider;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(GlyphProviderType.class)
@ClientOnlyMixin
public class GlyphProviderTypeMixin {
@ModifyExpressionValue(method = "<clinit>", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/client/gui/font/providers/UnihexProvider$Definition;CODEC:Lcom/mojang/serialization/MapCodec;"))
private static MapCodec<? extends GlyphProviderDefinition> lazyUnihex(MapCodec<? extends GlyphProviderDefinition> codec) {
return LazyGlyphProvider.wrap(codec);
}
}

View File

@ -12,7 +12,7 @@ import java.util.concurrent.ExecutorService;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public class MinecraftMixin {
@Redirect(method = { "<init>", "reloadResourcePacks(ZLnet/minecraft/client/Minecraft$GameLoadCookie;)Ljava/util/concurrent/CompletableFuture;" }, at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;", ordinal = 0))
@Redirect(method = { "<init>", "reloadResourcePacks(Z)Ljava/util/concurrent/CompletableFuture;" }, at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;backgroundExecutor()Ljava/util/concurrent/ExecutorService;", ordinal = 0))
private ExecutorService getResourceReloadExecutor() {
return ModernFix.resourceReloadExecutor();
}

View File

@ -10,7 +10,7 @@ import java.util.concurrent.Executor;
@Mixin(MinecraftServer.class)
public class MinecraftServerMixin {
@ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ReloadableServerResources;loadResources(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/core/LayeredRegistryAccess;Lnet/minecraft/world/flag/FeatureFlagSet;Lnet/minecraft/commands/Commands$CommandSelection;ILjava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 5)
@ModifyArg(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/ReloadableServerResources;loadResources(Lnet/minecraft/server/packs/resources/ResourceManager;Lnet/minecraft/core/RegistryAccess$Frozen;Lnet/minecraft/world/flag/FeatureFlagSet;Lnet/minecraft/commands/Commands$CommandSelection;ILjava/util/concurrent/Executor;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"), index = 5)
private Executor getReloadExecutor(Executor asyncExecutor) {
return ModernFix.resourceReloadExecutor();
}

View File

@ -25,7 +25,7 @@ import java.util.Map;
*/
@Mixin(WallBlock.class)
public abstract class WallBlockMixin extends Block {
private static Map<ImmutableList<Float>, Pair<Map<Map<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>>> CACHE_BY_SHAPE_VALS = new HashMap<>();
private static Map<ImmutableList<Float>, Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>>> CACHE_BY_SHAPE_VALS = new HashMap<>();
public WallBlockMixin(Properties properties) {
super(properties);
@ -34,7 +34,7 @@ public abstract class WallBlockMixin extends Block {
@Inject(method = "makeShapes", at = @At("HEAD"), cancellable = true)
private synchronized void useCachedShapeMap(float f1, float f2, float f3, float f4, float f5, float f6, CallbackInfoReturnable<Map<BlockState, VoxelShape>> cir) {
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
Pair<Map<Map<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>> cache = CACHE_BY_SHAPE_VALS.get(key);
Pair<Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape>, StateDefinition<Block, BlockState>> cache = CACHE_BY_SHAPE_VALS.get(key);
// require the properties to be identical
if(cache == null || !cache.getSecond().getProperties().equals(this.stateDefinition.getProperties()))
return;
@ -55,7 +55,7 @@ public abstract class WallBlockMixin extends Block {
return;
ImmutableList<Float> key = ImmutableList.of(f1, f2, f3, f4, f5, f6);
if(!CACHE_BY_SHAPE_VALS.containsKey(key)) {
Map<Map<Property<?>, Comparable<?>>, VoxelShape> cacheByProperties = new HashMap<>();
Map<ImmutableMap<Property<?>, Comparable<?>>, VoxelShape> cacheByProperties = new HashMap<>();
Map<BlockState, VoxelShape> shapeMap = cir.getReturnValue();
for(Map.Entry<BlockState, VoxelShape> entry : shapeMap.entrySet()) {
cacheByProperties.put(entry.getKey().getValues(), entry.getValue());

View File

@ -0,0 +1,19 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.types.Type;
import net.minecraft.world.level.block.entity.BlockEntityType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Prevent fetchChoiceType calls from loading DFU early. Vanilla doesn't need the return values here.
*/
@Mixin(BlockEntityType.class)
public class BlockEntityTypeMixin {
@Redirect(method = "register", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;fetchChoiceType(Lcom/mojang/datafixers/DSL$TypeReference;Ljava/lang/String;)Lcom/mojang/datafixers/types/Type;"))
private static Type<?> skipSchemaCheck(DSL.TypeReference ref, String s) {
return null;
}
}

View File

@ -1,17 +1,33 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.mojang.datafixers.DataFixerBuilder;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.DataFixer;
import net.minecraft.util.datafix.DataFixers;
import org.embeddedt.modernfix.dfu.DFUBlaster;
import org.embeddedt.modernfix.dfu.LazyDataFixer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Set;
@Mixin(DataFixers.class)
public class DataFixersMixin {
@ModifyReturnValue(method = "createFixerUpper", at = @At("RETURN"))
private static DataFixerBuilder.Result setupMapBlasting(DataFixerBuilder.Result original) {
DFUBlaster.blastMaps();
return original;
public abstract class DataFixersMixin {
@Shadow protected static DataFixer createFixerUpper(Set<DSL.TypeReference> set) {
throw new AssertionError();
}
private static LazyDataFixer lazyDataFixer;
/**
* Avoid classloading the DFU logic until we actually need it.
*/
@Inject(method = "createFixerUpper", at = @At("HEAD"), cancellable = true)
private static void createLazyFixerUpper(Set<DSL.TypeReference> set, CallbackInfoReturnable<DataFixer> cir) {
if(lazyDataFixer == null) {
lazyDataFixer = new LazyDataFixer(() -> createFixerUpper(set));
cir.setReturnValue(lazyDataFixer);
}
}
}

View File

@ -0,0 +1,19 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_dfu;
import com.mojang.datafixers.DSL;
import com.mojang.datafixers.types.Type;
import net.minecraft.world.entity.EntityType;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
/**
* Prevent fetchChoiceType calls from loading DFU early. Vanilla doesn't need the return values here.
*/
@Mixin(EntityType.Builder.class)
public class EntityTypeBuilderMixin {
@Redirect(method = "build", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;fetchChoiceType(Lcom/mojang/datafixers/DSL$TypeReference;Ljava/lang/String;)Lcom/mojang/datafixers/types/Type;"))
private Type<?> skipSchemaCheck(DSL.TypeReference ref, String s) {
return null;
}
}

View File

@ -0,0 +1,53 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_languages;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.client.resources.language.ClientLanguage;
import net.minecraft.server.packs.resources.Resource;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamiclanguages.DynamicLanguageMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Modifies the language system to load/unload the contents of language entries based on GC pressure.
*/
@Mixin(value = ClientLanguage.class, priority = 2000)
@ClientOnlyMixin
public class ClientLanguageMixin {
/**
* @author embeddedt
* @reason collect the list of all known language resources
*/
@WrapOperation(method = "loadFrom", at = @At(value = "INVOKE",
target = "Lnet/minecraft/client/resources/language/ClientLanguage;appendFrom(Ljava/lang/String;Ljava/util/List;Ljava/util/Map;)V"))
private static void collectResources(String languageName, List<Resource> resources,
Map<String, String> destinationMap, Operation<Void> original,
@Share("usedResources") LocalRef<List<Resource>> usedResources) {
List<Resource> collected = usedResources.get();
if (collected == null) {
collected = new ArrayList<>();
usedResources.set(collected);
}
collected.addAll(resources);
original.call(languageName, resources, destinationMap);
}
/**
* @author embeddedt
* @reason figure out which keys are dynamically loaded and which are injected by mixins
*/
@ModifyArg(method = "loadFrom", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/language/ClientLanguage;<init>(Ljava/util/Map;Z)V"), index = 0)
private static Map<String, String> modifyLanguageMap(Map<String, String> storage, @Share("usedResources") LocalRef<List<Resource>> usedResources) {
List<Resource> collected = Objects.requireNonNullElse(usedResources.get(), List.of());
return DynamicLanguageMap.forVanillaData(storage, collected);
}
}

View File

@ -29,7 +29,7 @@ public class BlockModelShaperMixin {
@Inject(method = { "<init>", "replaceCache" }, at = @At("RETURN"))
private void replaceModelMap(CallbackInfo ci) {
// replace the backing map for mods which will access it
this.modelByStateCache = new DynamicOverridableMap<>(BlockState.class, state -> modelManager.getModel(ModelLocationCache.get(state)));
this.modelByStateCache = new DynamicOverridableMap<>(state -> modelManager.getModel(ModelLocationCache.get(state)));
// Clear the cached models on blockstate objects
for(Block block : BuiltInRegistries.BLOCK) {
for(BlockState state : block.getStateDefinition().getPossibleStates()) {

View File

@ -1,117 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntMaps;
import it.unimi.dsi.fastutil.objects.ReferenceObjectImmutablePair;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.renderer.block.model.BlockModelDefinition;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IBlockStateModelLoader;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.Iterator;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.function.BiConsumer;
import java.util.function.Predicate;
@Mixin(BlockStateModelLoader.class)
@ClientOnlyMixin
public abstract class BlockStateModelLoaderMixin implements IBlockStateModelLoader {
@Shadow protected abstract void loadBlockStateDefinitions(ResourceLocation resourceLocation, StateDefinition<Block, BlockState> stateDefinition);
@Shadow @Mutable @Final private Object2IntMap<BlockState> modelGroups;
private ImmutableList<BlockState> filteredStates;
@Inject(method = "<init>", at = @At("RETURN"))
private void makeModelGroupsSynchronized(Map map, ProfilerFiller profilerFiller, UnbakedModel unbakedModel, BlockColors blockColors, BiConsumer biConsumer, CallbackInfo ci) {
this.modelGroups = Object2IntMaps.synchronize(this.modelGroups);
}
@Override
public void loadSpecificBlock(ModelResourceLocation location) {
var optionalBlock = BuiltInRegistries.BLOCK.getOptional(location.id());
if(optionalBlock.isPresent()) {
// embeddedt note - filtering is currently disabled as it's quite inefficient to do vs. just loading
// the extra models and letting LRU deal with it
/*
try {
// Only filter states if we are in a world and not in the loading overlay
filteredStates = (Minecraft.getInstance().getOverlay() == null && Minecraft.getInstance().level != null) ? ModelBakeryHelpers.getBlockStatesForMRL(optionalBlock.get().getStateDefinition(), location) : null;
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception filtering states on {}", location, e);
filteredStates = null;
}
*/
try {
this.loadBlockStateDefinitions(location.id(), optionalBlock.get().getStateDefinition());
} finally {
filteredStates = null;
}
}
}
@Redirect(method = "loadAllBlockStates", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/DefaultedRegistry;iterator()Ljava/util/Iterator;"))
private Iterator<?> skipIteratingBlocks(DefaultedRegistry instance) {
return Collections.emptyIterator();
}
@Redirect(method = "loadBlockStateDefinitions", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
private ImmutableList<BlockState> getFilteredStates(StateDefinition<Block, BlockState> instance) {
return this.filteredStates != null ? this.filteredStates : instance.getPossibleStates();
}
// Add some caching around key hot paths
private final Cache<ReferenceObjectImmutablePair<BlockStateModelLoader.LoadedJson, ResourceLocation>, BlockModelDefinition> cachedBlockModelDefs = CacheBuilder.newBuilder()
.maximumSize(100)
.build();
private static final Cache<Pair<StateDefinition<Block, BlockState>, String>, Predicate<BlockState>> cachedBlockStatePredicates = CacheBuilder.newBuilder()
.maximumSize(100)
.build();
@WrapOperation(method = "loadBlockStateDefinitions", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader$LoadedJson;parse(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/renderer/block/model/BlockModelDefinition$Context;)Lnet/minecraft/client/renderer/block/model/BlockModelDefinition;"))
private BlockModelDefinition avoidMultipleParses(BlockStateModelLoader.LoadedJson instance, ResourceLocation blockStateId, BlockModelDefinition.Context context, Operation<BlockModelDefinition> original) {
try {
return cachedBlockModelDefs.get(ReferenceObjectImmutablePair.of(instance, blockStateId), () -> original.call(instance, blockStateId, context));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
@WrapMethod(method = "predicate")
private static Predicate<BlockState> memoizePredicate(StateDefinition<Block, BlockState> stateDefentition, String properties, Operation<Predicate<BlockState>> original) {
try {
return cachedBlockStatePredicates.get(Pair.of(stateDefentition, properties), () -> original.call(stateDefentition, properties));
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -3,16 +3,17 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.base.Stopwatch;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.neoforged.bus.api.Event;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.ModList;
import net.neoforged.fml.ModLoader;
import net.neoforged.fml.util.ObfuscationReflectionHelper;
import net.neoforged.neoforge.client.ClientHooks;
import net.neoforged.neoforge.client.event.ModelEvent;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.client.ForgeHooksClient;
import net.minecraftforge.client.event.ModelEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.fml.ModContainer;
import net.minecraftforge.fml.ModList;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.neoforge.dynresources.ModelBakeEventHelper;
import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Redirect;
@ -23,14 +24,14 @@ import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Mixin(ClientHooks.class)
@Mixin(ForgeHooksClient.class)
public class ForgeHooksClientMixin {
/**
* Generate a more realistic keySet that contains every item and block model location, to help with mod compat.
*/
@Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V"), remap = false)
private static void postNamespacedKeySetEvent(Event event) {
if(ModLoader.hasErrors())
@Redirect(method = "onModifyBakingResult", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/fml/ModLoader;postEvent(Lnet/minecraftforge/eventbus/api/Event;)V"), remap = false)
private static void postNamespacedKeySetEvent(ModLoader loader, Event event) {
if(!ModLoader.isLoadingStateValid())
return;
ModelEvent.ModifyBakingResult bakeEvent = ((ModelEvent.ModifyBakingResult)event);
Stopwatch globalTimer = Stopwatch.createStarted();
@ -41,8 +42,8 @@ public class ForgeHooksClientMixin {
Map<String, Stopwatch> times = new Object2ObjectOpenHashMap<>();
times.put("modernfix", selfTimer);
ModList.get().forEachModContainer((id, mc) -> {
Map<ModelResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id);
ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getTextureGetter(), bakeEvent.getModelBakery());
Map<ResourceLocation, BakedModel> newRegistry = helper.wrapRegistry(id);
ModelEvent.ModifyBakingResult postedEvent = new ModelEvent.ModifyBakingResult(newRegistry, bakeEvent.getModelBakery());
Stopwatch timer = times.computeIfAbsent(id, $ -> Stopwatch.createUnstarted());
timer.start();
try {
@ -62,5 +63,8 @@ public class ForgeHooksClientMixin {
ModernFix.LOGGER.warn(" {}: {}", entry.getKey(), entry.getValue().toString());
});
}
if (bakeEvent.getModels() instanceof DynamicBakedModelProvider dynamicProvider) {
dynamicProvider.dumpStats();
}
}
}

View File

@ -4,9 +4,11 @@ import net.minecraft.client.renderer.ItemModelShaper;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.neoforged.neoforge.client.model.RegistryAwareItemModelShaper;
import net.minecraftforge.client.model.ForgeItemModelShaper;
import net.minecraftforge.registries.ForgeRegistries;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamicresources.DynamicModelCache;
import org.embeddedt.modernfix.dynamicresources.ModelLocationCache;
@ -19,20 +21,20 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.HashMap;
import java.util.Map;
@Mixin(RegistryAwareItemModelShaper.class)
@Mixin(ForgeItemModelShaper.class)
@ClientOnlyMixin
public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Shadow(remap = false) @Final @Mutable private Map<Item, ModelResourceLocation> locations;
@Shadow(remap = false) @Final @Mutable private Map<Holder.Reference<Item>, ModelResourceLocation> locations;
private Map<Item, ModelResourceLocation> overrideLocations;
private Map<Holder.Reference<Item>, ModelResourceLocation> overrideLocations;
private final DynamicModelCache<Item> mfix$modelCache = new DynamicModelCache<>(k -> this.mfix$getModelSlow((Item)k), true);
private final DynamicModelCache<Holder.Reference<Item>> mfix$modelCache = new DynamicModelCache<>(k -> this.mfix$getModelSlow((Holder.Reference<Item>)k), true);
public ItemModelMesherForgeMixin(ModelManager arg) {
super(arg);
}
private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath("modernfix", "sentinel"), "sentinel");
private static final ModelResourceLocation SENTINEL = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel");
@Inject(method = "<init>", at = @At("RETURN"))
private void replaceLocationMap(CallbackInfo ci) {
@ -42,16 +44,16 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
}
@Unique
private ModelResourceLocation mfix$getLocationForge(Item item) {
private ModelResourceLocation mfix$getLocationForge(Holder.Reference<Item> item) {
ModelResourceLocation map = overrideLocations.getOrDefault(item, SENTINEL);
if(map == SENTINEL) {
/* generate the appropriate location from our cache */
map = ModelLocationCache.get(item);
map = ModelLocationCache.get(item.get());
}
return map;
}
private BakedModel mfix$getModelSlow(Item key) {
private BakedModel mfix$getModelSlow(Holder.Reference<Item> key) {
ModelResourceLocation map = mfix$getLocationForge(key);
return map == null ? null : getModelManager().getModel(map);
}
@ -64,7 +66,7 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Overwrite
@Override
public BakedModel getItemModel(Item item) {
return this.mfix$modelCache.get(item);
return this.mfix$modelCache.get(ForgeRegistries.ITEMS.getDelegateOrThrow(item));
}
/**
@ -75,7 +77,7 @@ public abstract class ItemModelMesherForgeMixin extends ItemModelShaper {
@Overwrite
@Override
public void register(Item item, ModelResourceLocation location) {
overrideLocations.put(item, location);
overrideLocations.put(ForgeRegistries.ITEMS.getDelegateOrThrow(item), location);
}
/**

View File

@ -33,7 +33,7 @@ public abstract class ItemModelShaperMixin {
super();
}
private static final ModelResourceLocation SENTINEL_VANILLA = new ModelResourceLocation(ResourceLocation.fromNamespaceAndPath("modernfix", "sentinel"), "sentinel");
private static final ModelResourceLocation SENTINEL_VANILLA = new ModelResourceLocation(new ResourceLocation("modernfix", "sentinel"), "sentinel");
private final DynamicModelCache<Item> mfix$itemModelCache = new DynamicModelCache<>(k -> this.mfix$getModelForItem((Item)k), true);

View File

@ -0,0 +1,34 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.block.model.ItemOverrides;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.function.Function;
@Mixin(ItemOverrides.class)
@ClientOnlyMixin
public class ItemOverridesForgeMixin {
/**
* @author embeddedt
* @reason servers insist on generating invalid item overrides that have missing models
*/
@WrapOperation(method = "bakeModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBaker;bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;"), remap = false)
private BakedModel bake(ModelBaker instance, ResourceLocation resourceLocation, ModelState modelState, Function<ResourceLocation, TextureAtlasSprite> spriteGetter, Operation<BakedModel> original) {
boolean prevState = ((IExtendedModelBaker)instance).throwOnMissingModel(false);
try {
return original.call(instance, resourceLocation, modelState, spriteGetter);
} finally {
((IExtendedModelBaker)instance).throwOnMissingModel(prevState);
}
}
}

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.block.model.ItemOverrides;
import net.minecraft.client.resources.model.ModelBaker;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ItemOverrides.class)
@ClientOnlyMixin
public class ItemOverridesMixin {
@WrapOperation(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBaker;getModel(Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/resources/model/UnbakedModel;"))
private UnbakedModel preventThrowForMissing(ModelBaker instance, ResourceLocation resourceLocation, Operation<UnbakedModel> original) {
boolean prevState = ((IExtendedModelBaker)instance).throwOnMissingModel(false);
try {
return original.call(instance, resourceLocation);
} finally {
((IExtendedModelBaker)instance).throwOnMissingModel(prevState);
}
}
}

View File

@ -1,22 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import net.minecraft.client.Minecraft;
import net.minecraft.client.resources.model.ModelManager;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public abstract class MinecraftMixin_ModelTicking {
@Shadow public abstract ModelManager getModelManager();
@Inject(method = "tick", at = @At(value = "RETURN"))
private void tickModels(CallbackInfo ci) {
((IExtendedModelManager)this.getModelManager()).mfix$tick();
}
}

View File

@ -1,85 +1,102 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.dynamicresources.ModelMissingException;
import org.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.function.Function;
@Mixin(ModelBakery.ModelBakerImpl.class)
@Mixin(value = ModelBakery.ModelBakerImpl.class, priority = 600)
@ClientOnlyMixin
public abstract class ModelBakerImplMixin {
@Shadow public abstract UnbakedModel getModel(ResourceLocation location);
public abstract class ModelBakerImplMixin implements IModelBakerImpl, IExtendedModelBaker {
private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Shadow(aliases = {"this$0","f_243927_"}) @Final private ModelBakery field_40571;
@Shadow(aliases = {"this$0"}) @Final private ModelBakery field_40571;
@Unique
private int mfix$getDepth = 0;
private boolean mfix$ignoreCache = false;
/**
* @author embeddedt
* @reason force parent resolution to happen before model gets baked
*/
@ModifyReturnValue(method = "getModel", at = @At("RETURN"))
private UnbakedModel resolveParents(UnbakedModel model) {
mfix$getDepth++;
if(mfix$getDepth == 1) {
try {
model.resolveParents(this::getModel);
} catch(Exception e) {
ModernFix.LOGGER.warn("Exception encountered resolving parents", e);
@Shadow @Final private Function<Material, TextureAtlasSprite> modelTextureGetter;
@Override
public void mfix$ignoreCache() {
mfix$ignoreCache = true;
}
private boolean throwIfMissing;
@Override
public boolean throwOnMissingModel(boolean flag) {
boolean old = throwIfMissing;
throwIfMissing = flag;
return old;
}
@Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
private void obtainModel(ResourceLocation arg, CallbackInfoReturnable<UnbakedModel> cir) {
if(debugDynamicModelLoading)
ModernFix.LOGGER.info("Baking {}", arg);
IExtendedModelBakery extendedBakery = (IExtendedModelBakery)this.field_40571;
if(arg instanceof ModelResourceLocation && arg != ModelBakery.MISSING_MODEL_LOCATION) {
// synchronized because we use topLevelModels
synchronized (this.field_40571) {
this.field_40571.loadTopLevel((ModelResourceLocation)arg);
cir.setReturnValue(this.field_40571.topLevelModels.getOrDefault(arg, extendedBakery.mfix$getUnbakedMissingModel()));
// avoid leaks
this.field_40571.topLevelModels.clear();
}
} else
cir.setReturnValue(this.field_40571.getModel(arg));
UnbakedModel toReplace = cir.getReturnValue();
if(true) {
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
toReplace = integration.onUnbakedModelPreBake(arg, toReplace, this.field_40571);
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception firing model pre-bake event for {}", arg, e);
}
}
}
mfix$getDepth--;
cir.setReturnValue(toReplace);
cir.getReturnValue().resolveParents(this.field_40571::getModel);
if(cir.getReturnValue() == extendedBakery.mfix$getUnbakedMissingModel()) {
if(arg != ModelBakery.MISSING_MODEL_LOCATION) {
if(debugDynamicModelLoading)
ModernFix.LOGGER.warn("Model {} not present", arg);
if(throwIfMissing)
throw new ModelMissingException();
}
}
}
@ModifyExpressionValue(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0), remap = false)
private Object ignoreCacheIfRequested(Object o) {
return mfix$ignoreCache ? null : o;
}
@WrapOperation(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/UnbakedModel;bake(Lnet/minecraft/client/resources/model/ModelBaker;Ljava/util/function/Function;Lnet/minecraft/client/resources/model/ModelState;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/client/resources/model/BakedModel;"))
private BakedModel callBakedModelIntegration(UnbakedModel unbakedModel, ModelBaker baker, Function<Material, TextureAtlasSprite> spriteGetter, ModelState state, ResourceLocation location, Operation<BakedModel> operation) {
BakedModel model = operation.call(unbakedModel, baker, spriteGetter, state, location);
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
model = integration.onBakedModelLoad(location, unbakedModel, model, state, this.field_40571, spriteGetter);
}
return model;
}
@WrapMethod(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;)Lnet/minecraft/client/resources/model/BakedModel;")
private BakedModel mfix$lockWhenBaking(ResourceLocation location, ModelState transform, Operation<BakedModel> original) {
var lock = ((IExtendedModelBakery)this.field_40571).mfix$getLock();
lock.lock();
try {
return original.call(location, transform);
} finally {
lock.unlock();
}
}
/**
* @author embeddedt
* @reason Handle dynamic model loading
*/
@Overwrite(remap = false)
public UnbakedModel getTopLevelModel(ModelResourceLocation location) {
IExtendedModelBakery bakery = (IExtendedModelBakery)this.field_40571;
UnbakedModel model = bakery.mfix$loadUnbakedModelDynamic(location);
return model == bakery.mfix$getMissingModel() ? null : model;
}
@WrapMethod(method = "bake(Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/client/resources/model/ModelState;Ljava/util/function/Function;)Lnet/minecraft/client/resources/model/BakedModel;", remap = false)
private BakedModel mfix$lockWhenBaking(ResourceLocation location, ModelState transform, Function<Material, TextureAtlasSprite> textureGetter, Operation<BakedModel> original) {
var lock = ((IExtendedModelBakery)this.field_40571).mfix$getLock();
lock.lock();
try {
return original.call(location, transform, textureGetter);
} finally {
lock.unlock();
}
}
}

View File

@ -1,274 +1,412 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.RemovalCause;
import com.google.common.cache.RemovalNotification;
import com.google.common.collect.ForwardingMap;
import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.BlockModelRotation;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.duck.IBlockStateModelLoader;
import org.embeddedt.modernfix.duck.IExtendedModelBaker;
import org.embeddedt.modernfix.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.util.DynamicOverridableMap;
import org.embeddedt.modernfix.util.LRUMap;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.dynamicresources.ModelBakeryHelpers;
import org.objectweb.asm.Opcodes;
import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Mutable;
import org.spongepowered.asm.mixin.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.*;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
@Mixin(ModelBakery.class)
/* low priority so that our injectors are added after other mods' */
@Mixin(value = ModelBakery.class, priority = 1100)
@ClientOnlyMixin
public abstract class ModelBakeryMixin implements IExtendedModelBakery {
@Unique
private BlockStateModelLoader dynamicLoader;
@Unique
private final ReentrantLock modelBakeryLock = new ReentrantLock();
private static final boolean debugDynamicModelLoading = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Unique
private ModelBakery.TextureGetter textureGetter;
@Shadow @Final @Mutable public Map<ResourceLocation, UnbakedModel> unbakedCache;
@Unique
private BakedModel bakedMissingModel;
@Shadow @Final public static ModelResourceLocation MISSING_MODEL_LOCATION;
@Shadow abstract UnbakedModel getModel(ResourceLocation resourceLocation);
@Shadow protected abstract BlockModel loadBlockModel(ResourceLocation location) throws IOException;
@Shadow @Final private UnbakedModel missingModel;
@Shadow @Final private Set<ResourceLocation> loadingStack;
@Unique
private static final boolean DEBUG_MODEL_LOADS = Boolean.getBoolean("modernfix.debugDynamicModelLoading");
@Shadow protected abstract void loadModel(ResourceLocation blockstateLocation) throws Exception;
/**
* Bake a model using the provided texture getter and location. The model is stored in {@link ModelBakeryMixin#bakedTopLevelModels}.
*/
@Shadow(aliases = "lambda$bakeModels$6") protected abstract void method_61072(ModelBakery.TextureGetter getter, ModelResourceLocation location, UnbakedModel model);
@Shadow @Final @Mutable
private Map<ResourceLocation, BakedModel> bakedTopLevelModels;
@Shadow @Mutable @Final private Map<ModelResourceLocation, BakedModel> bakedTopLevelModels;
@Shadow @Mutable @Final public Map<ModelResourceLocation, UnbakedModel> topLevelModels;
@Shadow @Mutable @Final private Map<ResourceLocation, UnbakedModel> unbakedCache;
@Shadow @Mutable @Final public Map<ModelBakery.BakedCacheKey, BakedModel> bakedCache;
@Shadow @Final @Mutable private Map<ModelBakery.BakedCacheKey, BakedModel> bakedCache;
@Shadow protected abstract void loadItemModelAndDependencies(ResourceLocation resourceLocation);
@Shadow @Final @Mutable private BlockColors blockColors;
@Shadow @Final private static Logger LOGGER;
@Shadow
public abstract void loadTopLevel(ModelResourceLocation modelResourceLocation);
@Shadow public abstract UnbakedModel getModel(ResourceLocation resourceLocation);
private Cache<ModelBakery.BakedCacheKey, BakedModel> loadedBakedModels;
private Cache<ResourceLocation, UnbakedModel> loadedModels;
private HashMap<ResourceLocation, UnbakedModel> smallLoadingCache = new HashMap<>();
private boolean ignoreModelLoad;
// disable fabric recursion
@SuppressWarnings("unused")
private boolean fabric_enableGetOrLoadModelGuard;
@Shadow @Final public static ModelResourceLocation MISSING_MODEL_VARIANT;
@Shadow protected abstract void registerModelAndLoadDependencies(ModelResourceLocation modelLocation, UnbakedModel model);
private final Map<ModelResourceLocation, BakedModel> mfix$emulatedBakedRegistry = new DynamicOverridableMap<>(ModelResourceLocation.class, this::loadBakedModelDynamic);
@Override
public UnbakedModel mfix$loadUnbakedModelDynamic(ModelResourceLocation location) {
if(location.equals(MISSING_MODEL_VARIANT)) {
return missingModel;
}
modelBakeryLock.lock();
try {
UnbakedModel existing = this.topLevelModels.get(location);
if (existing != null) {
return existing;
@Redirect(method = "<init>", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/client/resources/model/ModelBakery;blockColors:Lnet/minecraft/client/color/block/BlockColors;"))
private void replaceTopLevelBakedModels(ModelBakery bakery, BlockColors val) {
fabric_enableGetOrLoadModelGuard = false;
this.blockColors = val;
this.loadedBakedModels = CacheBuilder.newBuilder()
.expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
.maximumSize(ModelBakeryHelpers.MAX_BAKED_MODEL_COUNT)
.concurrencyLevel(8)
.removalListener(this::onModelRemoved)
.softValues()
.build();
this.loadedModels = CacheBuilder.newBuilder()
.expireAfterAccess(ModelBakeryHelpers.MAX_MODEL_LIFETIME_SECS, TimeUnit.SECONDS)
.maximumSize(ModelBakeryHelpers.MAX_UNBAKED_MODEL_COUNT)
.concurrencyLevel(8)
.removalListener(this::onModelRemoved)
.softValues()
.build();
this.bakedCache = loadedBakedModels.asMap();
ConcurrentMap<ResourceLocation, UnbakedModel> unbakedCacheBackingMap = loadedModels.asMap();
this.unbakedCache = new ForwardingMap<ResourceLocation, UnbakedModel>() {
@Override
protected Map<ResourceLocation, UnbakedModel> delegate() {
return unbakedCacheBackingMap;
}
if(DEBUG_MODEL_LOADS) {
ModernFix.LOGGER.info("Loading model {}", location);
@Override
public UnbakedModel put(ResourceLocation key, UnbakedModel value) {
smallLoadingCache.put(key, value);
return super.put(key, value);
}
if(location.variant().equals("inventory")) {
this.loadItemModelAndDependencies(location.id());
} else if (location.variant().equals("fabric_resource") || location.variant().equals("standalone")) {
UnbakedModel unbakedModel = this.getModel(location.id());
this.registerModelAndLoadDependencies(location, unbakedModel);
} else {
((IBlockStateModelLoader)dynamicLoader).loadSpecificBlock(location);
}
return this.topLevelModels.getOrDefault(location, this.missingModel);
} finally {
modelBakeryLock.unlock();
}
};
this.bakedTopLevelModels = new DynamicBakedModelProvider((ModelBakery)(Object)this, bakedCache);
}
@WrapMethod(method = "getModel")
private UnbakedModel mfix$lockWhenGettingModel(ResourceLocation modelLocation, Operation<UnbakedModel> original) {
modelBakeryLock.lock();
try {
return original.call(modelLocation);
} finally {
modelBakeryLock.unlock();
}
}
@Override
public UnbakedModel mfix$getMissingModel() {
return missingModel;
}
@Unique
private BakedModel loadBakedModelDynamic(ModelResourceLocation location) {
if(location.equals(MISSING_MODEL_VARIANT)) {
return bakedMissingModel;
}
BakedModel model;
modelBakeryLock.lock();
try {
model = bakedTopLevelModels.get(location);
if(model == null) {
UnbakedModel prototype = mfix$loadUnbakedModelDynamic(location);
if(prototype == missingModel) {
model = bakedMissingModel;
} else {
prototype.resolveParents(this::getModel);
if(DEBUG_MODEL_LOADS) {
ModernFix.LOGGER.info("Baking model {}", location);
}
this.method_61072(this.textureGetter, location, prototype);
model = bakedTopLevelModels.remove(location);
if(model == null) {
ModernFix.LOGGER.error("Failed to load model " + location);
model = bakedMissingModel;
}
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
model = integration.onBakedModelLoad(location, prototype, model, BlockModelRotation.X0_Y0, (ModelBakery)(Object)this, this.textureGetter);
} catch (RuntimeException e) {
ModernFix.LOGGER.error("Exception encountered running dynamic resources integration", e);
}
}
}
}
} finally {
modelBakeryLock.unlock();
}
return model;
}
@ModifyExpressionValue(method = "<init>", at = @At(value = "CONSTANT", args = "stringValue=missing_model"))
private String replaceBackingMaps(String original) {
this.unbakedCache = new LRUMap<>(this.unbakedCache);
this.bakedCache = new LRUMap<>(this.bakedCache);
this.topLevelModels = new LRUMap<>(this.topLevelModels);
this.bakedTopLevelModels = new LRUMap<>(this.bakedTopLevelModels);
@ModifyExpressionValue(method = "<init>", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, ordinal = 0, target = "Lnet/minecraft/client/resources/model/ModelBakery;STATIC_DEFINITIONS:Ljava/util/Map;"))
private Map<ResourceLocation, StateDefinition<Block, BlockState>> ignoreFutureModelLoads(Map<ResourceLocation, StateDefinition<Block, BlockState>> original) {
this.ignoreModelLoad = true;
return original;
}
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/BlockStateModelLoader;loadAllBlockStates()V"))
private void noInitialBlockStateLoad(BlockStateModelLoader instance, Operation<Void> original) {
dynamicLoader = instance;
original.call(instance);
}
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/DefaultedRegistry;keySet()Ljava/util/Set;"))
private Set<?> skipLoadingItems(DefaultedRegistry instance) {
return Collections.emptySet();
}
@Inject(method = "bakeModels", at = @At("HEAD"))
private void storeTextureGetterAndBakeMissing(ModelBakery.TextureGetter textureGetter, CallbackInfo ci) {
this.textureGetter = textureGetter;
this.method_61072(textureGetter, MISSING_MODEL_VARIANT, Objects.requireNonNull(this.topLevelModels.get(MISSING_MODEL_VARIANT)));
this.bakedMissingModel = this.bakedTopLevelModels.get(MISSING_MODEL_VARIANT);
}
private boolean inInitialLoad = true;
@Inject(method = "bakeModels", at = @At("RETURN"))
private void onInitialBakeFinish(ModelBakery.TextureGetter textureGetter, CallbackInfo ci) {
var permanentMRLs = new ObjectOpenHashSet<>(this.bakedTopLevelModels.keySet());
((LRUMap<ModelResourceLocation, BakedModel>)this.bakedTopLevelModels).setPermanentEntries(permanentMRLs);
ModernFix.LOGGER.info("Dynamic model bakery initial baking finished, with {} permanent top level baked models", this.bakedTopLevelModels.size());
}
@Inject(method = "<init>", at = @At("RETURN"))
private void onInitialLoadFinish(BlockColors blockColors, ProfilerFiller profilerFiller, Map map, Map map2, CallbackInfo ci) {
var permanentMRLs = new ObjectOpenHashSet<>(this.topLevelModels.keySet());
((LRUMap<ModelResourceLocation, UnbakedModel>)this.topLevelModels).setPermanentEntries(permanentMRLs);
ModernFix.LOGGER.info("Dynamic model bakery loading finished, with {} permanent top level models", this.topLevelModels.size());
}
@Unique
private int tickCount;
@Unique
private static final int MAXIMUM_CACHE_SIZE = 1000;
private void runCleanup() {
try {
((LRUMap<?, ?>)this.unbakedCache).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in unbaked cache", e);
}
try {
((LRUMap<?, ?>)this.bakedCache).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in baked cache", e);
}
try {
((LRUMap<?, ?>)this.topLevelModels).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in top level models", e);
}
try {
((LRUMap<?, ?>)this.bakedTopLevelModels).dropEntriesToMeetSize(MAXIMUM_CACHE_SIZE);
} catch(RuntimeException e) {
throw new IllegalStateException("Exception dropping entries in baked top level models", e);
}
}
@Override
public void mfix$finishLoading() {
inInitialLoad = false;
}
@Override
public void mfix$tick() {
if(inInitialLoad) {
private <K, V> void onModelRemoved(RemovalNotification<K, V> notification) {
if(!debugDynamicModelLoading)
return;
// If the entry was replaced (happens because of the Minecraft model loading code structure), or
// was explicitly removed, we don't really care.
var reason = notification.getCause();
if (reason == RemovalCause.REPLACED || reason == RemovalCause.EXPLICIT) {
return;
}
tickCount++;
if((tickCount % 200) == 0) {
if(modelBakeryLock.tryLock()) {
try {
runCleanup();
} finally {
modelBakeryLock.unlock();
}
}
Object k = notification.getKey();
if(k == null)
return;
ResourceLocation rl;
boolean baked = false;
if(k instanceof ResourceLocation) {
rl = (ResourceLocation)k;
} else {
rl = ((ModelBakery.BakedCacheKey)k).id();
baked = true;
}
/* can fire when a model is replaced */
if(!baked && this.loadedModels.getIfPresent(rl) != null)
return;
ModernFix.LOGGER.warn("Evicted {} model {}", baked ? "baked" : "unbaked", rl);
}
private UnbakedModel missingModel;
private Set<ResourceLocation> blockStateFiles;
private Set<ResourceLocation> modelFiles;
@ModifyArg(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0), index = 1)
private Object captureMissingModel(Object model) {
this.missingModel = (UnbakedModel)model;
this.blockStateFiles = new HashSet<>();
this.modelFiles = new HashSet<>();
return this.missingModel;
}
/**
* @author embeddedt
* @reason We provide a fake baked registry to the rest of Minecraft, that dynamically loads models.
* @reason don't actually load most models
*/
@Overwrite
public Map<ModelResourceLocation, BakedModel> getBakedTopLevelModels() {
return this.mfix$emulatedBakedRegistry;
@Redirect(method = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelBakery;loadTopLevel(Lnet/minecraft/client/resources/model/ModelResourceLocation;)V"))
private void addTopLevelFile(ModelBakery bakery, ModelResourceLocation location) {
if(location == MISSING_MODEL_LOCATION || !this.ignoreModelLoad) {
loadTopLevel(location);
}
}
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;forEach(Ljava/util/function/BiConsumer;)V", ordinal = 0))
private void fetchStaticDefinitions(Map<ResourceLocation, StateDefinition<Block, BlockState>> map, BiConsumer<ResourceLocation, StateDefinition<Block, BlockState>> func) {
/* no-op */
}
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;", ordinal = 0))
private ImmutableList<BlockState> fetchBlocks(StateDefinition<Block, BlockState> def) {
/* no-op */
return ImmutableList.of();
}
/**
* @author embeddedt
* @reason Prevent the models provided by RegisterAdditional from being tracked, otherwise the unbaked models will
* be loaded, baked, and added to permanent overrides unnecessarily.
* We still need to fire the event, because there is a decent chance mods rely on it to set up state for their
* model loaders, but we can ignore the return value.
*/
@WrapOperation(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/client/ForgeHooksClient;onRegisterAdditionalModels(Ljava/util/Set;)V"))
private void preventLoadOfAdditionalModels(Set<ResourceLocation> additionalModels, Operation<Void> original) {
original.call(additionalModels);
// Immediately clear the set
additionalModels.clear();
}
/**
* Make a copy of the top-level model list to avoid CME if more models get loaded here.
*/
@Redirect(method = "<init>", at = @At(value = "INVOKE", target = "Ljava/util/Map;values()Ljava/util/Collection;", ordinal = 0))
private Collection<?> copyTopLevelModelList(Map<?, ?> map) {
return new ArrayList<>(map.values());
}
private BiFunction<ResourceLocation, Material, TextureAtlasSprite> textureGetter;
@Inject(method = "bakeModels", at = @At("HEAD"))
private void captureGetter(BiFunction<ResourceLocation, Material, TextureAtlasSprite> getter, CallbackInfo ci) {
this.ignoreModelLoad = false;
textureGetter = getter;
DynamicBakedModelProvider.currentInstance = (DynamicBakedModelProvider)this.bakedTopLevelModels;
}
@Redirect(method = "bakeModels", at = @At(value = "INVOKE", target = "Ljava/util/Map;keySet()Ljava/util/Set;"))
private Set<ResourceLocation> skipBake(Map<ResourceLocation, UnbakedModel> instance) {
Set<ResourceLocation> modelSet = new HashSet<>(instance.keySet());
if(modelSet.size() > 0)
ModernFix.LOGGER.info("Early baking {} models", modelSet.size());
return modelSet;
}
/**
* Use the already loaded missing model instead of the cache entry (which will probably get evicted).
*/
@Redirect(method = "loadModel", at = @At(value = "INVOKE", target = "Ljava/util/Map;get(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 1))
private Object getMissingModel(Map map, Object rl) {
if(rl == MISSING_MODEL_LOCATION && map == unbakedCache)
return missingModel;
return unbakedCache.get(rl);
}
@ModifyVariable(method = "cacheAndQueueDependencies", at = @At("HEAD"), argsOnly = true)
private UnbakedModel fireUnbakedEvent(UnbakedModel model, ResourceLocation location) {
for(ModernFixClientIntegration integration : ModernFixClient.CLIENT_INTEGRATIONS) {
try {
model = integration.onUnbakedModelLoad(location, model, (ModelBakery)(Object)this);
} catch(RuntimeException e) {
ModernFix.LOGGER.error("Exception firing model load event for {}", location, e);
}
}
return model;
}
@Inject(method = "cacheAndQueueDependencies", at = @At("RETURN"))
private void addToSmallLoadingCache(ResourceLocation location, UnbakedModel model, CallbackInfo ci) {
smallLoadingCache.put(location, model);
}
private int mfix$nestedLoads = 0;
/**
* @author embeddedt
* @reason synchronize
*/
@Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
public void getOrLoadModelDynamic(ResourceLocation modelLocation, CallbackInfoReturnable<UnbakedModel> cir) {
if(modelLocation.equals(MISSING_MODEL_LOCATION)) {
cir.setReturnValue(missingModel);
return;
}
UnbakedModel existing = this.unbakedCache.get(modelLocation);
if (existing != null) {
cir.setReturnValue(existing);
} else {
synchronized(this) {
// CIT Resewn adds dependencies to loadingStack outside of getModel(), so we must silently
// ignore existing things in the stack for the outermost model
if (mfix$nestedLoads > 0 && this.loadingStack.contains(modelLocation)) {
throw new IllegalStateException("Circular reference while loading " + modelLocation);
} else {
this.loadingStack.add(modelLocation);
UnbakedModel iunbakedmodel = missingModel;
while(!this.loadingStack.isEmpty()) {
ResourceLocation resourcelocation = this.loadingStack.iterator().next();
mfix$nestedLoads++;
try {
existing = this.unbakedCache.get(resourcelocation);
if (existing == null) {
if(debugDynamicModelLoading)
LOGGER.info("Loading {}", resourcelocation);
this.loadModel(resourcelocation);
} else
smallLoadingCache.put(resourcelocation, existing);
} catch (ModelBakery.BlockStateDefinitionException var9) {
LOGGER.warn(var9.getMessage());
this.unbakedCache.put(resourcelocation, iunbakedmodel);
smallLoadingCache.put(resourcelocation, iunbakedmodel);
} catch (Exception var10) {
LOGGER.warn("Unable to load model: '{}' referenced from: {}: {}", resourcelocation, modelLocation, var10);
this.unbakedCache.put(resourcelocation, iunbakedmodel);
smallLoadingCache.put(resourcelocation, iunbakedmodel);
} finally {
mfix$nestedLoads--;
this.loadingStack.remove(resourcelocation);
}
}
// We have to get the result from the temporary cache used for a model load
// As in pathological cases (e.g. Pedestals on 1.19) unbakedCache can lose
// the model immediately
UnbakedModel result = smallLoadingCache.getOrDefault(modelLocation, iunbakedmodel);
try {
// required as some mods (e.g. EBE) call bake directly on the returned model, without resolving parents themselves
result.resolveParents(this::getModel);
} catch(RuntimeException ignored) {}
// We are done with loading, so clear this cache to allow GC of any unneeded models
if(mfix$nestedLoads == 0)
smallLoadingCache.clear();
cir.setReturnValue(result);
}
}
}
}
@Redirect(method = "loadModel", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
private ImmutableList<BlockState> loadOnlyRelevantBlockState(StateDefinition<Block, BlockState> stateDefinition, ResourceLocation location) {
var allStates = stateDefinition.getPossibleStates();
if(!(location instanceof ModelResourceLocation mrl)) {
return allStates;
}
// Load a batch of models at once in certain initialization phases to speed up the loading process.
// This is disabled when in-game as it will cause stutters when blocks are placed.
boolean shouldLoadBatch = (Minecraft.getInstance().getOverlay() != null || Minecraft.getInstance().level == null);
int batchSize = ModelBakeryHelpers.MAX_UNBAKED_MODEL_COUNT - 1000;
// If loading a batch and all the states are smaller than the max batch size, just use them
// This is hoisted above the computation of desiredStates for performance reasons
if (shouldLoadBatch && allStates.size() <= batchSize) {
return allStates;
}
var desiredStates = ModelBakeryHelpers.getBlockStatesForMRL(stateDefinition, mrl);
// If not loading a batch, load only the desired states
if (!shouldLoadBatch) {
return desiredStates;
}
// At this point we want to load a batch if possible, but loading every state is too much. If desiredStates
// is a single state (should almost always be the case), then we choose a sublist starting from it and extending
// batchSize entries (or less if the list ends). If it's multiple states, a single sublist may not include
// everything, so we bail.
if (desiredStates.size() != 1) {
return desiredStates;
}
var desiredState = desiredStates.get(0);
int indexInAllStates = allStates.indexOf(desiredState);
if (indexInAllStates == -1) {
return desiredStates;
}
return allStates.subList(indexInAllStates, Math.min(indexInAllStates + batchSize, allStates.size()));
}
@Override
public ReentrantLock mfix$getLock() {
return this.modelBakeryLock;
public BakedModel bakeDefault(ResourceLocation modelLocation, ModelState state) {
ModelBakery.BakedCacheKey key = new ModelBakery.BakedCacheKey(modelLocation, BlockModelRotation.X0_Y0.getRotation(), BlockModelRotation.X0_Y0.isUvLocked());
BakedModel m = loadedBakedModels.getIfPresent(key);
if(m != null)
return m;
ModelBakery self = (ModelBakery) (Object) this;
ModelBaker theBaker = self.new ModelBakerImpl(textureGetter, modelLocation);
((IExtendedModelBaker)theBaker).throwOnMissingModel(true);
synchronized(this) {
// We intentionally use the 2-arg overload for better mixin compatibility, because we use the baker's default
// texture getter anyway.
//noinspection deprecation
m = theBaker.bake(modelLocation, state);
}
if(m != null)
loadedBakedModels.put(key, m);
return m;
}
@Override
public ImmutableList<BlockState> getBlockStatesForMRL(StateDefinition<Block, BlockState> stateDefinition, ModelResourceLocation location) {
return loadOnlyRelevantBlockState(stateDefinition, location);
}
private BakedModel bakedMissingModel = null;
public void setBakedMissingModel(BakedModel m) {
bakedMissingModel = m;
}
public BakedModel getBakedMissingModel() {
return bakedMissingModel;
}
public UnbakedModel mfix$getUnbakedMissingModel() {
return missingModel;
}
@Override
public void mfix$clearModels() {
loadedModels.invalidateAll();
loadedBakedModels.invalidateAll();
}
}

View File

@ -4,34 +4,27 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.Maps;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.client.renderer.block.model.BlockModel;
import net.minecraft.client.resources.model.AtlasSet;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.BlockStateModelLoader;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelManager;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.Resource;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.util.GsonHelper;
import net.minecraft.util.profiling.ProfilerFiller;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.IExtendedModelBakery;
import org.embeddedt.modernfix.duck.IExtendedModelManager;
import org.embeddedt.modernfix.util.CacheUtil;
import org.embeddedt.modernfix.util.LambdaMap;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Coerce;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.io.BufferedReader;
import java.io.IOException;
@ -48,12 +41,9 @@ import java.util.stream.Collectors;
@Mixin(ModelManager.class)
@ClientOnlyMixin
public class ModelManagerMixin implements IExtendedModelManager {
public class ModelManagerMixin {
@Shadow private Map<ResourceLocation, BakedModel> bakedRegistry;
@Unique
private Runnable tickHandler = () -> {};
@Inject(method = "<init>", at = @At("RETURN"))
private void injectDummyBakedRegistry(CallbackInfo ci) {
if(this.bakedRegistry == null) {
@ -64,16 +54,16 @@ public class ModelManagerMixin implements IExtendedModelManager {
@ModifyArg(method = "loadBlockModels", at = @At(value = "INVOKE", target = "Ljava/util/concurrent/CompletableFuture;thenCompose(Ljava/util/function/Function;)Ljava/util/concurrent/CompletableFuture;", ordinal = 0), index = 0)
private static Function<Map<ResourceLocation, Resource>, ? extends CompletionStage<Map<ResourceLocation, BlockModel>>> deferBlockModelLoad(Function<Map<ResourceLocation, Resource>, ? extends CompletionStage<Map<ResourceLocation, BlockModel>>> fn, @Local(ordinal = 0, argsOnly = true) ResourceManager manager) {
return resourceMap -> {
var cache = CacheUtil.<ResourceLocation, BlockModel>simpleCacheForLambda(location -> loadSingleBlockModel(manager, location), 100L);
var fallbackModel = BlockModel.fromString(ModelBakery.MISSING_MODEL_MESH);
var cache = CacheUtil.<ResourceLocation, BlockModel>simpleCacheForLambda(location -> loadSingleBlockModel(manager, location, fallbackModel), 100L);
return CompletableFuture.completedFuture(Maps.asMap(Set.copyOf(resourceMap.keySet()), location -> cache.getUnchecked(location)));
};
}
@Redirect(method = "reload", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/resources/model/ModelManager;loadBlockStates(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/util/concurrent/Executor;)Ljava/util/concurrent/CompletableFuture;"))
private CompletableFuture<Map<ResourceLocation, List<BlockStateModelLoader.LoadedJson>>> deferBlockStateLoad(ResourceManager manager, Executor executor) {
var cache = CacheUtil.<ResourceLocation, List<BlockStateModelLoader.LoadedJson>>simpleCacheForLambda(location -> loadSingleBlockState(manager, location), 100L);
var blockStateKeys = Set.copyOf(BlockStateModelLoader.BLOCKSTATE_LISTER.listMatchingResourceStacks(manager).keySet());
return CompletableFuture.completedFuture(Maps.asMap(blockStateKeys, location -> cache.getUnchecked(location)));
private CompletableFuture<Map<ResourceLocation, List<ModelBakery.LoadedJson>>> deferBlockStateLoad(ResourceManager manager, Executor executor) {
var cache = CacheUtil.<ResourceLocation, List<ModelBakery.LoadedJson>>simpleCacheForLambda(location -> loadSingleBlockState(manager, location), 100L);
return CompletableFuture.completedFuture(new LambdaMap<>(location -> cache.getUnchecked(location)));
}
@Redirect(method = "loadModels", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/StateDefinition;getPossibleStates()Lcom/google/common/collect/ImmutableList;"))
@ -81,40 +71,27 @@ public class ModelManagerMixin implements IExtendedModelManager {
return ImmutableList.of();
}
private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location) {
private static BlockModel loadSingleBlockModel(ResourceManager manager, ResourceLocation location, BlockModel fallbackModel) {
return manager.getResource(location).map(resource -> {
try (BufferedReader reader = resource.openAsReader()) {
return BlockModel.fromStream(reader);
} catch(IOException e) {
ModernFix.LOGGER.error("Couldn't load model", e);
return null;
} catch (Exception e) {
// We must return some nonnull value to avoid breaking the map convention. The easiest solution
// is to just return a missing model template.
ModernFix.LOGGER.error("Couldn't load model {}, substituting missing", location, e);
return fallbackModel;
}
}).orElse(null);
}
private List<BlockStateModelLoader.LoadedJson> loadSingleBlockState(ResourceManager manager, ResourceLocation location) {
private List<ModelBakery.LoadedJson> loadSingleBlockState(ResourceManager manager, ResourceLocation location) {
return manager.getResourceStack(location).stream().map(resource -> {
try (BufferedReader reader = resource.openAsReader()) {
return new BlockStateModelLoader.LoadedJson(resource.sourcePackId(), GsonHelper.parse(reader));
return new ModelBakery.LoadedJson(resource.sourcePackId(), GsonHelper.parse(reader));
} catch(IOException e) {
ModernFix.LOGGER.error("Couldn't load blockstate", e);
return null;
}
}).filter(Objects::nonNull).collect(Collectors.toList());
}
@Inject(method = "loadModels", at = @At("RETURN"))
private void storeTicker(ProfilerFiller profilerFiller, Map<ResourceLocation, AtlasSet.StitchResult> map, ModelBakery modelBakery, CallbackInfoReturnable<?> cir) {
tickHandler = ((IExtendedModelBakery)modelBakery)::mfix$tick;
}
@Inject(method = "apply", at = @At("RETURN"))
private void freezeBakery(@Coerce Object reloadState, ProfilerFiller profilerFiller, CallbackInfo ci, @Local(ordinal = 0) ModelBakery bakery) {
((IExtendedModelBakery)bakery).mfix$finishLoading();
}
@Override
public void mfix$tick() {
tickHandler.run();
}
}

View File

@ -3,12 +3,15 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ctm;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;
import com.mojang.datafixers.util.Pair;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.*;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.embeddedt.modernfix.forge.dynresources.IModelBakerImpl;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@ -26,6 +29,7 @@ import team.chisel.ctm.client.util.TextureMetadataHandler;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.util.*;
import java.util.function.Function;
@Mixin(TextureMetadataHandler.class)
@RequiresMod("ctm")
@ -41,18 +45,18 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
ModernFixClient.CLIENT_INTEGRATIONS.add(this);
}
@Inject(method = { "onModelBake(Lnet/neoforged/neoforge/client/event/ModelEvent$BakingCompleted;)V", "onModelBake(Lnet/neoforged/neoforge/client/event/ModelEvent$ModifyBakingResult;)V" }, at = @At("HEAD"), cancellable = true, remap = false)
@Inject(method = { "onModelBake(Lnet/minecraftforge/client/event/ModelEvent$ModifyBakingResult;)V", "onModelBake(Lnet/minecraftforge/client/event/ModelEvent$BakingCompleted;)V" }, at = @At("HEAD"), cancellable = true, remap = false)
private void noIteration(CallbackInfo ci) {
ci.cancel();
}
@Override
public BakedModel onBakedModelLoad(ModelResourceLocation mrl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter getter) {
if (!(baked instanceof AbstractCTMBakedModel) && !baked.isCustomRenderer()) {
public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery) {
if (rl instanceof ModelResourceLocation && !(baked instanceof AbstractCTMBakedModel) && !baked.isCustomRenderer()) {
Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>();
dependencies.push(mrl.id());
seenModels.add(mrl.id());
dependencies.push(rl);
seenModels.add(rl);
boolean shouldWrap = false;
Set<Pair<String, String>> errors = new HashSet<>();
// Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles
@ -60,7 +64,7 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
ResourceLocation dep = dependencies.pop();
UnbakedModel model;
try {
model = dep == mrl.id() ? rootModel : bakery.getModel(dep);
model = dep == rl ? rootModel : bakery.getModel(dep);
} catch (Exception e) {
continue;
}
@ -88,27 +92,34 @@ public abstract class TextureMetadataHandlerMixin implements ModernFixClientInte
if (shouldWrap) {
try {
baked = wrap(rootModel, baked);
handleInit(mrl, baked, bakery, getter);
handleInit(rl, baked, bakery);
dependencies.clear();
} catch (IOException e) {
CTM.logger.error("Could not wrap model " + mrl + ". Aborting...", e);
CTM.logger.error("Could not wrap model " + rl + ". Aborting...", e);
}
}
}
return baked;
}
private void handleInit(ModelResourceLocation key, BakedModel wrappedModel, ModelBakery bakery, ModelBakery.TextureGetter spriteGetter) {
private void handleInit(ResourceLocation key, BakedModel wrappedModel, ModelBakery bakery) {
if(wrappedModel instanceof AbstractCTMBakedModel baked) {
IModelCTM var10 = baked.getModel();
if (var10 instanceof ModelCTM ctmModel) {
if (!ctmModel.isInitialized()) {
// Clear the baked cache as upstream CTM does
((CTMModelBakeryAccessor)bakery).mfix$getBakedCache().clear();
ModelBakery.ModelBakerImpl baker = bakery.new ModelBakerImpl(spriteGetter, key);
ctmModel.bake(baker, m -> spriteGetter.get(key, m), BlockModelRotation.X0_Y0);
Function<Material, TextureAtlasSprite> spriteGetter = (m) -> {
return Minecraft.getInstance().getModelManager().getAtlas(m.atlasLocation()).getSprite(m.texture());
};
ModelBakery.ModelBakerImpl baker = bakery.new ModelBakerImpl(($, m) -> {
return spriteGetter.apply(m);
}, key);
// bypass bakedCache so that dependent models get re-baked and thus retrieve their sprites again
((IModelBakerImpl)baker).mfix$ignoreCache();
ctmModel.bake(baker, spriteGetter, BlockModelRotation.X0_Y0, key);
}
}
}
}
}
}

View File

@ -2,9 +2,11 @@ package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.ldlib;
import com.lowdragmc.lowdraglib.LDLib;
import com.lowdragmc.lowdraglib.client.ClientProxy;
import com.lowdragmc.lowdraglib.client.model.custommodel.CustomBakedModel;
import com.lowdragmc.lowdraglib.client.forge.ClientProxyImpl;
import com.lowdragmc.lowdraglib.client.model.custommodel.LDLMetadataSection;
import com.lowdragmc.lowdraglib.client.model.forge.CustomBakedModelImpl;
import com.lowdragmc.lowdraglib.client.model.forge.LDLRendererModel;
import net.minecraft.client.renderer.texture.TextureAtlasSprite;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.Material;
import net.minecraft.client.resources.model.ModelBakery;
@ -27,8 +29,9 @@ import java.util.Deque;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
@Mixin(ClientProxy.class)
@Mixin(ClientProxyImpl.class)
@ClientOnlyMixin
@RequiresMod("ldlib")
public abstract class ClientProxyImplMixin implements ModernFixClientIntegration {
@ -43,11 +46,11 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
}
@Override
public BakedModel onBakedModelLoad(ModelResourceLocation mrl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, ModelBakery.TextureGetter textureGetter) {
public BakedModel onBakedModelLoad(ResourceLocation rl, UnbakedModel rootModel, BakedModel baked, ModelState state, ModelBakery bakery, Function<Material, TextureAtlasSprite> textureGetter) {
if (baked == null) {
return null;
}
if (rootModel != null) {
if (rl instanceof ModelResourceLocation && rootModel != null) {
if (baked instanceof LDLRendererModel) {
return baked;
}
@ -56,10 +59,9 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
}
Deque<ResourceLocation> dependencies = new ArrayDeque<>();
Set<ResourceLocation> seenModels = new HashSet<>();
ResourceLocation rl = mrl.id();
dependencies.push(rl);
seenModels.add(rl);
boolean shouldWrap = ClientProxy.WRAPPED_MODELS.getOrDefault(mrl, false);
boolean shouldWrap = ClientProxy.WRAPPED_MODELS.getOrDefault(rl, false);
// Breadth-first loop through dependencies, exiting as soon as a CTM texture is found, and skipping duplicates/cycles
while (!shouldWrap && !dependencies.isEmpty()) {
ResourceLocation dep = dependencies.pop();
@ -90,9 +92,9 @@ public abstract class ClientProxyImplMixin implements ModernFixClientIntegration
LDLib.LOGGER.error("Error loading baked dependency {} for baked {}. Skipping...", dep, rl, e);
}
}
ClientProxy.WRAPPED_MODELS.put(mrl, shouldWrap);
ClientProxy.WRAPPED_MODELS.put(rl, shouldWrap);
if (shouldWrap) {
return new CustomBakedModel<>(baked);
return new CustomBakedModelImpl(baked);
}
}
return baked;

View File

@ -0,0 +1,64 @@
package org.embeddedt.modernfix.common.mixin.perf.dynamic_resources.supermartijncore;
import com.supermartijn642.core.registry.ClientRegistrationHandler;
import com.supermartijn642.core.util.Pair;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.resources.model.BakedModel;
import net.minecraft.client.resources.model.ModelBakery;
import net.minecraft.client.resources.model.ModelState;
import net.minecraft.client.resources.model.UnbakedModel;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFixClient;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.api.entrypoint.ModernFixClientIntegration;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
@Mixin(ClientRegistrationHandler.class)
@RequiresMod("supermartijn642corelib")
@ClientOnlyMixin
public class ClientRegistrationHandlerMixin {
@Shadow(remap = false) @Final private List<Pair<Supplier<Stream<ResourceLocation>>, Function<BakedModel, BakedModel>>> modelOverwrites;
private Map<ResourceLocation, Function<BakedModel, BakedModel>> modelOverwritesByLocation = new Object2ObjectOpenHashMap<>();
@Redirect(method = "handleModelBakeEvent", at = @At(value = "FIELD", target = "Lcom/supermartijn642/core/registry/ClientRegistrationHandler;modelOverwrites:Ljava/util/List;"), remap = false)
private List<?> skipModelOverwrites(ClientRegistrationHandler h) {
modelOverwritesByLocation.clear();
for(Pair<Supplier<Stream<ResourceLocation>>, Function<BakedModel, BakedModel>> pair : this.modelOverwrites) {
Stream<ResourceLocation> locationStream = pair.left().get();
Function<BakedModel, BakedModel> swapper = pair.right();
locationStream.forEach(l -> {
modelOverwritesByLocation.put(l, swapper);
});
}
return Collections.emptyList();
}
@Inject(method = "<init>", at = @At("RETURN"))
private void registerDynBake(String modid, CallbackInfo ci) {
ModernFixClient.CLIENT_INTEGRATIONS.add(new ModernFixClientIntegration() {
@Override
public BakedModel onBakedModelLoad(ResourceLocation location, UnbakedModel baseModel, BakedModel originalModel, ModelState state, ModelBakery bakery) {
Function<BakedModel, BakedModel> replacer = modelOverwritesByLocation.get(location);
if(replacer != null)
return replacer.apply(originalModel);
else
return originalModel;
}
});
}
}

View File

@ -1,14 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import net.minecraft.core.component.DataComponents;
import net.minecraft.util.EncoderCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(DataComponents.class)
public interface DataComponentsAccessor {
@Accessor("ENCODER_CACHE")
static EncoderCache mfix$getCache() {
throw new AssertionError();
}
}

View File

@ -1,12 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import com.google.common.cache.LoadingCache;
import net.minecraft.util.EncoderCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(EncoderCache.class)
public interface EncoderCacheAccessor {
@Accessor("cache")
LoadingCache<?, ?> mfix$getCache();
}

View File

@ -1,27 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.encoder_cache_leak;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import net.minecraft.core.component.DataComponents;
import net.minecraft.server.ReloadableServerResources;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.concurrent.CompletableFuture;
@Mixin(ReloadableServerResources.class)
public class ReloadableServerResourcesMixin {
/**
* @author embeddedt
* @reason Some mods (e.g. KubeJS) may provide a custom DynamicOps instance during resource reload. This instance
* can end up being strongly retained by an EncoderCache.Key entry even after the reload finishes. The simplest
* fix is to invalidate all entries of the encoder cache after a server-side resource reload, which should not break
* mods, as the cache is not guaranteed to persist entries for any length of time due to using both a maximum size
* & soft values.
*/
@ModifyReturnValue(method = "loadResources", at = @At("RETURN"))
private static CompletableFuture<ReloadableServerResources> resetEncoderCache(CompletableFuture<ReloadableServerResources> future) {
return future.whenComplete((r, t) -> {
((EncoderCacheAccessor)DataComponentsAccessor.mfix$getCache()).mfix$getCache().invalidateAll();
});
}
}

View File

@ -0,0 +1,43 @@
package org.embeddedt.modernfix.common.mixin.perf.fast_forge_dummies;
import com.mojang.serialization.Lifecycle;
import net.minecraft.core.Holder;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.Registry;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import org.objectweb.asm.Opcodes;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Map;
@Mixin(targets = { "net/minecraftforge/registries/NamespacedWrapper" })
public abstract class NamespacedHolderHelperMixin<T> extends MappedRegistry<T> {
@Shadow(remap = false) private Map<ResourceLocation, Holder.Reference<T>> holdersByName;
public NamespacedHolderHelperMixin(ResourceKey<? extends Registry<T>> arg, Lifecycle lifecycle) {
super(arg, lifecycle);
}
@Inject(method = "freeze", at = @At(value = "FIELD", opcode = Opcodes.GETFIELD, target = "Lnet/minecraftforge/registries/NamespacedWrapper;holdersByName:Ljava/util/Map;", remap = false), cancellable = true)
private void fastDummyCheck(CallbackInfoReturnable<Registry<T>> cir) {
// Quickly iterate without making any streams, etc. to see if everything is fine
// Use the slow path (by returning without cancelling) when there is an error
for(Holder.Reference<T> ref : this.holdersByName.values()) {
if(!ref.isBound())
return;
}
if (this.unregisteredIntrusiveHolders != null) {
for(Holder.Reference<T> ref : this.unregisteredIntrusiveHolders.values()) {
if(ref.getType() == Holder.Reference.Type.INTRUSIVE && !ref.isBound())
return;
}
}
// Skip the creation of streams
cir.setReturnValue(this);
}
}

Some files were not shown because too many files have changed in this diff Show More