Compare commits

..

106 Commits

Author SHA1 Message Date
thirtyninerealms-cloud
667ac6c6ee
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
2026-06-14 17:30:06 +08:00
thirtyninerealms-cloud
2d760eecbb
Fix thread leak and graceful shutdown issue in NightConfigWatchThrottler
Problem:
- FileSystemWatchService threads accumulate over time (observed 17+ threads)
- Threads cannot be interrupted during container shutdown due to unhandled parkNanos()
- Container fails to stop gracefully, requiring force kill

Root cause:
- LockSupport.parkNanos() called without interruption handling
- No shutdown detection mechanism
- Threads continue polling file system even when JVM is terminating

Changes:
1. Add AtomicBoolean shutdown flag to prevent new watch iterations during shutdown
2. Add proper thread interruption handling with graceful fallback to empty iterator
3. Register shutdown hook to set flag on JVM exit

Testing:
- Verified threads no longer accumulate after multiple config reloads
- Container now responds to SIGTERM and stops within 5 seconds
- CPU usage returns to normal after shutdown sequence
2026-06-14 17:24:20 +08:00
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
106 changed files with 5421 additions and 637 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,34 +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 }}

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) {

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.legacyforge") version("2.0.134")
id("org.ajoberstar.grgit") version("5.2.0")
id("com.palantir.git-version") version("1.0.0")
id("me.modmuss50.mod-publish-plugin") version("1.1.0")
}
@ -9,42 +7,14 @@ 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)
val dirtyMarker = if (grgit.status().isClean) "" else ".dirty"
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
version = gitVersion.get()
base.archivesName = "modernfix-forge"
@ -68,6 +38,10 @@ legacyForge {
create("server") {
server()
}
create("auditClient") {
client()
jvmArguments.addAll("-Dmodernfix.auditAndExit=true", "-Djava.awt.headless=true")
}
}
mods {
@ -91,12 +65,7 @@ tasks.named<Jar>("jar") {
))
}
// 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_17
sourceCompatibility = curSourceCompatLevel
targetCompatibility = curSourceCompatLevel
@ -160,6 +129,7 @@ dependencies {
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") {

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)
}
}

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

@ -1,34 +0,0 @@
pluginManagement {
repositories {
gradlePluginPortal()
maven { url "https://maven.fabricmc.net/" }
maven { url "https://maven.architectury.dev/" }
maven { url "https://maven.minecraftforge.net/" }
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == "com.github.johnrengelman.shadow") {
useModule("gradle.plugin.com.github.johnrengelman:shadow:${shadow_version}")
}
}
}
}
include("annotation-processor")
include("annotations")
include("test_agent")
include("common")
def current_platforms = getProperty("enabled_platforms").tokenize(',')
current_platforms.each { it ->
def platform_name = it.trim()
include(platform_name)
if(hasProperty("modernfix.testmod.enable")) {
def testmodFolder = new File(platform_name + "/" + "testmod")
if (testmodFolder.isDirectory()) {
include(platform_name + ":testmod")
}
}
}
rootProject.name = 'modernfix'

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

@ -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

@ -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

@ -2,21 +2,128 @@ 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
@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");
@ -32,7 +139,7 @@ public abstract class ChunkMapLoadMixin {
* 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 = "*", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;runPostLoad()V"))
@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) {

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

@ -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

@ -1,22 +1,18 @@
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.server.level.progress.ChunkProgressListener;
import net.minecraft.util.thread.BlockableEventLoop;
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.levelgen.structure.templatesystem.StructureTemplateManager;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
@ -25,18 +21,6 @@ import java.util.concurrent.Executor;
public abstract class ChunkMapMixin {
@Shadow @Final private BlockableEventLoop<Runnable> mainThreadExecutor;
@Shadow @Final private ChunkMap.DistanceManager distanceManager;
@Shadow protected abstract CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> protoChunkToFullChunk(ChunkHolder arg);
@Shadow @Final private ServerLevel level;
@Shadow @Final private ThreadedLevelLightEngine lightEngine;
@Shadow @Final private ChunkProgressListener progressListener;
@Shadow protected abstract CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> scheduleChunkGeneration(ChunkHolder chunkHolder, ChunkStatus chunkStatus);
@Shadow @Final private StructureTemplateManager structureTemplateManager;
/* 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) {
@ -45,31 +29,24 @@ public abstract class ChunkMapMixin {
/**
* @author embeddedt
* @reason revert 1.17 chunk system changes, significantly reduces time and RAM needed to load chunks
* @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.
*/
@Inject(method = "schedule", at = @At("HEAD"), cancellable = true)
private void useLegacySchedulingLogic(ChunkHolder holder, ChunkStatus requiredStatus, CallbackInfoReturnable<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> cir) {
if(requiredStatus != ChunkStatus.EMPTY && !requiredStatus.hasLoadDependencies()) {
ChunkPos chunkpos = holder.getPos();
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future = holder.getOrScheduleFuture(requiredStatus.getParent(), (ChunkMap)(Object)this);
cir.setReturnValue(future.thenComposeAsync((either) -> {
Optional<ChunkAccess> optional = either.left();
if (requiredStatus == ChunkStatus.LIGHT) {
this.distanceManager.addTicket(TicketType.LIGHT, chunkpos, 33 + ChunkStatus.getDistance(ChunkStatus.LIGHT), chunkpos);
}
// from original method
if (optional.isPresent() && optional.get().getStatus().isOrAfter(requiredStatus)) {
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> completablefuture = requiredStatus.load(this.level, this.structureTemplateManager, this.lightEngine, (arg2) -> {
return this.protoChunkToFullChunk(holder);
}, (ChunkAccess)optional.get());
this.progressListener.onStatusChange(chunkpos, requiredStatus);
return completablefuture;
} else {
return this.scheduleChunkGeneration(holder, requiredStatus);
}
}, this.mainThreadExecutor).thenComposeAsync(CompletableFuture::completedFuture, this.mainThreadExecutor));
@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

@ -2,6 +2,7 @@ package org.embeddedt.modernfix.common.mixin.core;
import net.minecraft.server.Bootstrap;
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;
@ -25,6 +26,7 @@ public class BootstrapMixin {
if(!isBootstrapped) {
LOGGER.info("ModernFix reached bootstrap stage ({} after launch)", TimeFormatter.formatNanos(ManagementFactory.getRuntimeMXBean().getUptime() * 1000L * 1000L));
ModWorkManagerQueue.replace();
ManifestCompactor.compactManifests();
}
}

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

@ -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

@ -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

@ -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

@ -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::load,
StrongholdLocationCache::new,
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,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

@ -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

@ -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

@ -12,6 +12,7 @@ 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.dynamicresources.DynamicBakedModelProvider;
import org.embeddedt.modernfix.forge.dynresources.ModelBakeEventHelper;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@ -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

@ -7,6 +7,8 @@ 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.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.color.block.BlockColors;
import net.minecraft.client.renderer.block.model.BlockModel;
@ -187,6 +189,20 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery {
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.
*/
@ -256,7 +272,9 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery {
cir.setReturnValue(existing);
} else {
synchronized(this) {
if (this.loadingStack.contains(modelLocation)) {
// 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);
@ -356,7 +374,12 @@ public abstract class ModelBakeryMixin implements IExtendedModelBakery {
ModelBakery self = (ModelBakery) (Object) this;
ModelBaker theBaker = self.new ModelBakerImpl(textureGetter, modelLocation);
((IExtendedModelBaker)theBaker).throwOnMissingModel(true);
synchronized(this) { m = theBaker.bake(modelLocation, state, theBaker.getModelTextureGetter()); }
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;

View File

@ -54,7 +54,8 @@ public class ModelManagerMixin {
@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)));
};
}
@ -70,13 +71,15 @@ public class ModelManagerMixin {
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);
}

View File

@ -0,0 +1,59 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.EventPriority;
import org.embeddedt.modernfix.duck.IBatchingCapEvent;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer;
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 java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
@Mixin(AttachCapabilitiesEvent.class)
public abstract class AttachCapabilitiesEventMixin extends Event implements IBatchingCapEvent {
@Shadow @Final
private Map<ResourceLocation, ICapabilityProvider> caps;
@Unique
private final Map<ResourceLocation, EventPriority> mfix$phaseMap = new HashMap<>();
/**
* @author embeddedt
* @reason record the current dispatch phase so we can sort within phase groups later
*/
@WrapMethod(method = "addCapability", remap = false)
private void mfix$trackPhase(ResourceLocation key, ICapabilityProvider cap, Operation<Void> original) {
original.call(key, cap);
mfix$phaseMap.put(key, this.getPhase());
}
@Override
public void mfix$sortCaps() {
if (caps.size() < 2) {
return;
}
var entries = new ArrayList<>(caps.entrySet());
entries.sort(Comparator.comparingInt(e -> {
EventPriority phase = mfix$phaseMap.getOrDefault(e.getKey(), EventPriority.NORMAL);
var result = CapabilityAnalyzer.analyze(e.getValue().getClass());
// Primary: preserve phase ordering (HIGHEST=0 .. LOWEST=4)
// Secondary: Known/AlwaysEmpty before Indeterminate within each phase
int capKey = result instanceof CapabilityAnalysisResult.Indeterminate ? 1 : 0;
return phase.ordinal() * 2 + capKey;
}));
caps.clear();
entries.forEach(e -> caps.put(e.getKey(), e.getValue()));
mfix$phaseMap.clear();
}
}

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_capabilities.bytecode_analysis;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraftforge.event.ForgeEventFactory;
import net.minecraftforge.eventbus.api.Event;
import net.minecraftforge.eventbus.api.IEventBus;
import org.embeddedt.modernfix.duck.IBatchingCapEvent;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(value = ForgeEventFactory.class, remap = false)
public class ForgeEventFactoryMixin {
@WrapOperation(method = "gatherCapabilities(Lnet/minecraftforge/event/AttachCapabilitiesEvent;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;)Lnet/minecraftforge/common/capabilities/CapabilityDispatcher;", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/eventbus/api/IEventBus;post(Lnet/minecraftforge/eventbus/api/Event;)Z"))
private static boolean modernfix$sortAfterPost(IEventBus instance, Event event, Operation<Boolean> original) {
boolean result = original.call(instance, event);
((IBatchingCapEvent) event).mfix$sortCaps();
return result;
}
}

View File

@ -135,6 +135,7 @@ public abstract class IngredientMixin implements ExtendedIngredient {
return stacks;
}
}
IngredientItemStacksSoftReference.clearReferences();
ItemStack[] result = computeItemsArray();
this.mfix$cachedItemStacks = new IngredientItemStacksSoftReference((Ingredient)(Object)this, result);
return result;

View File

@ -0,0 +1,57 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.storage.loot.LootTable;
import net.minecraftforge.common.ForgeHooks;
import org.apache.commons.lang3.function.TriFunction;
import org.apache.logging.log4j.Logger;
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.Overwrite;
import org.spongepowered.asm.mixin.Shadow;
import java.util.Optional;
import static net.minecraftforge.common.ForgeHooks.loadLootTable;
@Mixin(value = ForgeHooks.class, remap = false)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class ForgeHooksMixin {
@Shadow
@Final
private static Logger LOGGER;
private static boolean mfix$isVanillaTable(JsonElement data) {
if (!(data instanceof JsonObject obj)) {
return false;
}
var vanillaMarker = obj.getAsJsonPrimitive("mfix$isVanillaTable");
if (vanillaMarker == null) {
return false;
}
return vanillaMarker.getAsBoolean();
}
/**
* @author embeddedt
* @reason avoid getResource() call per loot table by using injected marker
*/
@Overwrite
public static TriFunction<ResourceLocation, JsonElement, ResourceManager, Optional<LootTable>> getLootTableDeserializer(Gson gson, String directory) {
return (location, data, resourceManager) -> {
try {
boolean custom = !mfix$isVanillaTable(data);
return Optional.ofNullable(loadLootTable(gson, location, data, custom));
} catch (Exception exception) {
LOGGER.error("Couldn't parse element {}:{}", directory, location, exception);
return Optional.empty();
}
};
}
}

View File

@ -0,0 +1,41 @@
package org.embeddedt.modernfix.common.mixin.perf.faster_loot_loading;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.resources.FileToIdConverter;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.resources.ResourceManager;
import net.minecraft.world.level.storage.loot.LootDataManager;
import net.minecraft.world.level.storage.loot.LootDataType;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
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.Map;
@Mixin(LootDataManager.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class LootDataManagerMixin {
/**
* @author embeddedt
* @reason inject a marker for vanilla loot tables into the JSON so that we can retrieve it from the deserializer
*/
@Inject(method = "lambda$scheduleElementParse$5", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/packs/resources/SimpleJsonResourceReloadListener;scanDirectory(Lnet/minecraft/server/packs/resources/ResourceManager;Ljava/lang/String;Lcom/google/gson/Gson;Ljava/util/Map;)V", shift = At.Shift.AFTER))
private static void mfix$scanAndCapture(ResourceManager resourceManager, LootDataType lootDataType, Map map, CallbackInfo ci,
@Local(ordinal = 1) Map<ResourceLocation, JsonElement> lootTables) {
FileToIdConverter converter = FileToIdConverter.json(lootDataType.directory());
var lootTableResourceMap = converter.listMatchingResources(resourceManager);
for (var entry : lootTableResourceMap.entrySet()) {
if (lootTables.get(converter.fileToId(entry.getKey())) instanceof JsonObject obj) {
var resource = entry.getValue();
if (resource != null && !resource.isBuiltin()) {
obj.addProperty("mfix$isVanillaTable", true);
}
}
}
}
}

View File

@ -0,0 +1,69 @@
package org.embeddedt.modernfix.common.mixin.perf.fix_handshake_stall;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import net.minecraftforge.network.HandshakeHandler;
import net.minecraftforge.network.NetworkRegistry;
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.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.Collections;
import java.util.List;
@Mixin(value = HandshakeHandler.class, remap = false)
public class HandshakeHandlerMixin {
@Shadow
private int packetPosition;
@Shadow
private List<NetworkRegistry.LoginPayload> messageList;
@Shadow
private List<Integer> sentMessages;
/**
* @author embeddedt
* @reason we must synchronize sentMessages because it is modified from both the Netty thread and the
* server thread
*/
@Inject(method = "<init>", at = @At("RETURN"))
private void synchronizeSentMessages(CallbackInfo ci) {
this.sentMessages = Collections.synchronizedList(this.sentMessages);
}
/**
* @author embeddedt
* @reason Forge only sends one login payload per tick. It takes many seconds to send all the payloads at this rate.
* During this time, the game remains frozen on the chunk loading screen with almost zero CPU usage.
* To fix this, we re-tick the handshake handler until the packetPosition stops advancing or the handler indicates
* it no longer needs ticking.
*/
@WrapMethod(method = "tickServer")
private boolean modernfix$retick(Operation<Boolean> original) {
boolean isDoneTicking;
int prevPacketPosition;
do {
prevPacketPosition = this.packetPosition;
isDoneTicking = original.call();
} while(!isDoneTicking && this.packetPosition > prevPacketPosition);
return isDoneTicking;
}
/**
* @author embeddedt
* @reason The original HandshakeHandler has an off-by-one error in its completion check. We patch this to prevent
* our optimization from potentially triggering it more often due to the timing change.
*/
@WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Ljava/util/List;isEmpty()Z", ordinal = 0), slice = @Slice(from = @At(value = "INVOKE", target = "Ljava/util/List;removeIf(Ljava/util/function/Predicate;)Z", ordinal = 0)))
private boolean preventEarlyExit(List<?> instance, Operation<Boolean> original) {
if (instance != this.sentMessages) {
throw new AssertionError("Injector is misplaced");
}
return original.call(instance) && this.packetPosition >= this.messageList.size();
}
}

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_cap_retrieval;
import net.minecraftforge.event.AttachCapabilitiesEvent;
import net.minecraftforge.eventbus.api.Event;
import org.spongepowered.asm.mixin.Mixin;
@Mixin(AttachCapabilitiesEvent.class)
public abstract class AttachCapabilitiesEventMixin extends Event {
/**
* @author embeddedt
* @reason EventSubclassTransformer is supposed to inject an override returning a constant on the class to avoid the
* {@link net.minecraftforge.eventbus.api.EventListenerHelper#isCancelable(Class)} slow path.
* However, the false case is only done for direct subclasses of Event (the true case is done for
* any cancelable event). This works for normal events because they must subclass Event directly, or be a subclass
* of an event that does. However, AttachCapabilitiesEvent subclasses GenericEvent, which does not pass through
* the EventSubclassTransformer as it comes from the EventBus library (where transformers are not run) rather than
* Forge which is on the GAME layer. The transformer on AttachCapabilitiesEvent then does not add the override as
* it expects it to be present on GenericEvent already.
* <p>
* The simplest workaround to that whole mess is to just inject the override ourselves.
*/
@Override
public boolean isCancelable() {
return false;
}
}

View File

@ -0,0 +1,38 @@
package org.embeddedt.modernfix.common.mixin.perf.forge_registry_alloc;
import net.minecraft.world.level.levelgen.DebugLevelSource;
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;
import java.util.AbstractList;
import java.util.stream.Collector;
import java.util.stream.Stream;
@Mixin(DebugLevelSource.class)
public class DebugLevelSourceMixin {
/**
* @author embeddedt
* @reason Reuse the existing blockstate list held by Forge instead of making a new one
*/
@Redirect(method = "initValidStates", at = @At(value = "INVOKE", target = "Ljava/util/stream/Stream;collect(Ljava/util/stream/Collector;)Ljava/lang/Object;", ordinal = 0), remap = false)
private static Object getStateList(Stream<?> instance, Collector<?, ?, ?> arCollector) {
var idMapper = GameData.getBlockStateIDMap();
return new AbstractList<>() {
@Override
public int size() {
return idMapper.size();
}
@Override
public Object get(int index) {
var o = idMapper.byId(index);
if (o == null) {
throw new IndexOutOfBoundsException();
}
return o;
}
};
}
}

View File

@ -1,12 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.kubejs;
import com.google.gson.JsonElement;
import dev.latvian.mods.kubejs.recipe.RecipeJS;
import dev.latvian.mods.kubejs.recipe.RecipesEventJS;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.crafting.Recipe;
import net.minecraft.world.item.crafting.RecipeManager;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@ -49,4 +57,30 @@ public class RecipeEventJSMixin {
}
}
}
/**
* @author embeddedt
* @reason once datapackRecipeMap is iterated, it is never referenced again, so clear it to avoid retaining
* references to the JSON objects
*/
@Inject(method = "post", at = @At(value = "NEW", target = "()Ljava/util/concurrent/ConcurrentLinkedQueue;", ordinal = 0), remap = false)
private void modernfix$clearDatapackRecipeMap(RecipeManager recipeManager, Map<ResourceLocation, JsonElement> datapackRecipeMap, CallbackInfo ci) {
if (ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) {
datapackRecipeMap.clear();
}
}
/**
* @author embeddedt
* @reason As we start materializing the final recipe objects, null out the JSON references so we avoid having
* to keep both in memory at the same time
*/
@Inject(method = "createRecipe", at = @At("RETURN"), remap = false)
private void modernfix$clearJson(RecipeJS r, CallbackInfoReturnable<Recipe<?>> cir) {
if (!ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL.isAtLeast(FeatureLevel.BETA)) {
return;
}
r.json = null;
r.originalJson = null;
}
}

View File

@ -0,0 +1,16 @@
package org.embeddedt.modernfix.common.mixin.perf.object_holder_cleanup;
import net.minecraftforge.registries.GameData;
import org.embeddedt.modernfix.forge.registry.ObjectHolderClearer;
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 clearRedundantHolders(CallbackInfo ci) {
ObjectHolderClearer.removeRedundantHolders();
}
}

View File

@ -0,0 +1,62 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext;
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.List;
import java.util.Set;
@Mixin(SurfaceRules.BiomeConditionSource.class)
public class BiomeConditionSourceMixin {
@Shadow
@Final
public List<ResourceKey<Biome>> biomes;
/**
* @author Mojang, embeddedt
* @reason Hoist evaluation of the biome conditions where possible, in cases where we know the chunk contains
* no matching biomes, or only matching biomes
*/
@Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$Condition;", at = @At("HEAD"), cancellable = true)
private void mfix$optimizeCondition(SurfaceRules.Context p_context, CallbackInfoReturnable<SurfaceRules.Condition> cir) {
var possibleBiomes = ((ExtendedSurfaceContext)(Object)p_context).mfix$getPossibleBiomes();
if (possibleBiomes != null) {
if (mfix$guaranteedNoMatch(possibleBiomes)) {
cir.setReturnValue(() -> false);
} else if (mfix$alwaysMatches(possibleBiomes)) {
cir.setReturnValue(() -> true);
}
}
}
private boolean mfix$guaranteedNoMatch(Set<ResourceKey<Biome>> possibleBiomesInChunk) {
var testBiomes = this.biomes;
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < testBiomes.size(); i++) {
if (possibleBiomesInChunk.contains(testBiomes.get(i))) {
return false;
}
}
return true;
}
private boolean mfix$alwaysMatches(Set<ResourceKey<Biome>> possibleBiomesInChunk) {
var testBiomes = this.biomes;
// Check each of the biomes in the chunk and see if we'd always match it
for (var biome : possibleBiomesInChunk) {
if (!testBiomes.contains(biome)) {
// at least one biome would not match
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.world.level.biome.BiomeManager;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(BiomeManager.class)
public interface BiomeManagerAccessor {
@Accessor("biomeZoomSeed")
long mfix$getZoomSeed();
@Accessor("noiseBiomeSource")
BiomeManager.NoiseBiomeSource mfix$getBiomeSource();
}

View File

@ -0,0 +1,97 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import terrablender.worldgen.surface.NamespacedSurfaceRuleSource;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
@Mixin(NamespacedSurfaceRuleSource.class)
@RequiresMod("terrablender")
@RequiresFeatureLevel(FeatureLevel.BETA)
public class NamespacedSurfaceRuleSourceMixin {
@Shadow
@Final
private Map<String, SurfaceRules.RuleSource> sources;
@Shadow
@Final
private SurfaceRules.RuleSource base;
/**
* @author embeddedt
* @reason Avoid doing an expensive biome lookup per block in cases where we can prove all biomes will be from a
* single namespace. This achieves much of the benefit of TerraBlenderFix without the compatibility issues.
*/
@Inject(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At("HEAD"), cancellable = true, remap = false)
private void modernfix$fastApply(SurfaceRules.Context context, CallbackInfoReturnable<SurfaceRules.SurfaceRule> cir,
@Share("possibleNamespaces") LocalRef<Set<String>> possibleNamespacesRef) {
var possibleBiomes = ((ExtendedSurfaceContext)(Object)context).mfix$getPossibleBiomes();
if (possibleBiomes == null) {
return;
}
Set<String> namespaces = mfix$findNamespaces(possibleBiomes);
possibleNamespacesRef.set(namespaces);
if (namespaces.size() != 1) {
return;
}
String singleNamespace = namespaces.iterator().next();
// In a single namespace scenario, we can bypass the biome lookup and directly construct a sequence rule
SurfaceRules.RuleSource namespacedSource = this.sources.get(singleNamespace);
if (namespacedSource == null) {
// Sequence rule wrapper not required
cir.setReturnValue(this.base.apply(context));
} else {
cir.setReturnValue(new SurfaceRules.SequenceRule(ImmutableList.of(namespacedSource.apply(context), this.base.apply(context))));
}
}
/**
* @author embeddedt
* @reason Even if we have to fall back to the namespaced source, avoid compiling surface rules for namespaces that
* will never be hit in the given chunk.
*/
@ModifyArg(method = "apply(Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;)Lnet/minecraft/world/level/levelgen/SurfaceRules$SurfaceRule;", at = @At(value = "INVOKE", target = "Ljava/util/Set;forEach(Ljava/util/function/Consumer;)V"), remap = false)
private Consumer<Map.Entry<String, SurfaceRules.RuleSource>> mfix$filterConsumer(Consumer<Map.Entry<String, SurfaceRules.RuleSource>> originalConsumer,
@Share("possibleNamespaces") LocalRef<Set<String>> possibleNamespacesRef) {
var possibleNamespaces = possibleNamespacesRef.get();
if (possibleNamespaces == null) {
return originalConsumer;
}
return entry -> {
if(possibleNamespaces.contains(entry.getKey())) {
originalConsumer.accept(entry);
}
};
}
private static Set<String> mfix$findNamespaces(Set<ResourceKey<Biome>> possibleBiomes) {
if (possibleBiomes.size() == 1) {
return Set.of(possibleBiomes.iterator().next().location().getNamespace());
} else {
var namespaces = new ObjectArraySet<String>(4);
for (var key : possibleBiomes) {
namespaces.add(key.location().getNamespace());
}
return Set.copyOf(namespaces);
}
}
}

View File

@ -0,0 +1,68 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.level.WorldGenRegion;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.StructureManager;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.LevelChunkSection;
import net.minecraft.world.level.levelgen.NoiseBasedChunkGenerator;
import net.minecraft.world.level.levelgen.RandomState;
import org.embeddedt.modernfix.chunk.ExtendedPalettedContainer;
import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext;
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.NoSuchElementException;
import java.util.Set;
@Mixin(NoiseBasedChunkGenerator.class)
public class NoiseBasedChunkGeneratorMixin {
@SuppressWarnings("unchecked")
private static void mfix$accumulate(Set<ResourceKey<Biome>> chunkBiomes, LevelChunkSection section) {
var palette = ((ExtendedPalettedContainer<Holder<Biome>>)section.getBiomes()).mfix$getPalette();
if (palette.getSize() == 1) {
// No need to iterate the storage itself, as there can only be one value
chunkBiomes.add(palette.valueFor(0).unwrapKey().orElseThrow());
} else {
// Use getAll() rather than raw palette iteration. PalettedContainer.recreate() seeds the new
// palette with Biomes.PLAINS (the initial default), leaving a stale palette entry even after
// fillBiomesFromNoise replaces all cells with real biomes. getAll() only visits entries that
// are actually referenced in the backing storage, so stale entries are correctly excluded.
section.getBiomes().getAll(holder -> chunkBiomes.add(holder.unwrapKey().orElseThrow()));
}
}
private static Set<ResourceKey<Biome>> mfix$obtainBiomes(WorldGenRegion region, int chunkRadius) {
Set<ResourceKey<Biome>> chunkBiomes = new ReferenceOpenHashSet<>();
ChunkPos center = region.getCenter();
for (int z = center.z - chunkRadius; z <= center.z + chunkRadius; z++) {
for (int x = center.x - chunkRadius; x <= center.x + chunkRadius; x++) {
var chunk = region.getChunk(x, z);
for (var section : chunk.getSections()) {
mfix$accumulate(chunkBiomes, section);
}
}
}
return chunkBiomes;
}
/**
* @author embeddedt
* @reason scan for all biomes in the chunk and its neighbors before building surface
*/
@Inject(method = "buildSurface(Lnet/minecraft/server/level/WorldGenRegion;Lnet/minecraft/world/level/StructureManager;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;)V",
at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/NoiseBasedChunkGenerator;buildSurface(Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/StructureManager;Lnet/minecraft/world/level/biome/BiomeManager;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/blending/Blender;)V"))
private void mfix$findNearbyBiomes(WorldGenRegion level, StructureManager structureManager, RandomState random, ChunkAccess chunk, CallbackInfo ci) {
try {
ExtendedSurfaceContext.COMPUTED_POSSIBLE_BIOMES.set(mfix$obtainBiomes(level, 1));
} catch (NoSuchElementException ignored) {
// Catch in case a biome somehow does not have a key. In that case we just don't use the computed set
}
}
}

View File

@ -0,0 +1,27 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.levelgen.SurfaceRules;
import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import java.util.Set;
@Mixin(SurfaceRules.Context.class)
public class SurfaceRulesContextMixin implements ExtendedSurfaceContext {
@Nullable Set<ResourceKey<Biome>> mfix$possibleBiomes;
@Override
public void mfix$applyPossibleBiomes() {
// Copy into a field so we don't hit a thread local for each BiomeConditionSource instance
this.mfix$possibleBiomes = ExtendedSurfaceContext.COMPUTED_POSSIBLE_BIOMES.get();
ExtendedSurfaceContext.COMPUTED_POSSIBLE_BIOMES.remove();
}
@Override
public @Nullable Set<ResourceKey<Biome>> mfix$getPossibleBiomes() {
return this.mfix$possibleBiomes;
}
}

View File

@ -0,0 +1,98 @@
package org.embeddedt.modernfix.common.mixin.perf.optimize_surface_rules;
import com.llamalad7.mixinextras.sugar.Local;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.Registry;
import net.minecraft.world.level.biome.Biome;
import net.minecraft.world.level.biome.BiomeManager;
import net.minecraft.world.level.chunk.BlockColumn;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.levelgen.NoiseChunk;
import net.minecraft.world.level.levelgen.RandomState;
import net.minecraft.world.level.levelgen.SurfaceRules;
import net.minecraft.world.level.levelgen.SurfaceSystem;
import net.minecraft.world.level.levelgen.WorldGenerationContext;
import org.embeddedt.modernfix.world.gen.ChunkBiomeLookup;
import org.embeddedt.modernfix.world.gen.ExtendedSurfaceContext;
import org.embeddedt.modernfix.world.gen.PrefetchingBlockColumn;
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.ModifyArg;
import org.spongepowered.asm.mixin.injection.Redirect;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Function;
@Mixin(value = SurfaceSystem.class, priority = 2000)
public class SurfaceSystemMixin {
private static final ThreadLocal<ChunkBiomeLookup> MFIX_LOOKUP_CACHE = ThreadLocal.withInitial(ChunkBiomeLookup::new);
private static final ThreadLocal<PrefetchingBlockColumn> MFIX_BLOCK_COLUMN = new ThreadLocal<>();
@ModifyArg(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;<init>(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"), index = 4)
private Function<BlockPos, Holder<Biome>> useFasterLookup(Function<BlockPos, Holder<Biome>> biomeGetter,
@Local(ordinal = 0, argsOnly = true) BiomeManager manager,
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
@Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) {
// If mods use their own BiomeManager subclass, we cannot trust them to use the same blurring as vanilla,
// so we cannot apply our optimized path
if (manager.getClass() == BiomeManager.class) {
var lookup = MFIX_LOOKUP_CACHE.get();
BiomeManagerAccessor accessor = (BiomeManagerAccessor)manager;
lookup.prepare(accessor.mfix$getBiomeSource(), accessor.mfix$getZoomSeed(), chunk, manager);
lookupRef.set(lookup);
return lookup;
} else {
lookupRef.set(null);
return biomeGetter;
}
}
@Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$RuleSource;apply(Ljava/lang/Object;)Ljava/lang/Object;", ordinal = 0))
private void injectBiomesOnContext(CallbackInfo ci, @Local(ordinal = 0) SurfaceRules.Context surfacerules$context) {
((ExtendedSurfaceContext)(Object) surfacerules$context).mfix$applyPossibleBiomes();
}
@Inject(method = "buildSurface", at = @At("TAIL"))
private void finishAndDisposeLookups(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_, WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci) {
MFIX_LOOKUP_CACHE.get().dispose();
var column = MFIX_BLOCK_COLUMN.get();
if (column != null) {
column.dispose();
}
}
@Redirect(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/biome/BiomeManager;getBiome(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/core/Holder;"))
private Holder<Biome> useFasterLookup(BiomeManager instance, BlockPos pos, @Share("chunkBiomeLookup") LocalRef<ChunkBiomeLookup> lookupRef) {
var lookup = lookupRef.get();
if (lookup != null) {
return lookup.apply(pos);
} else {
return instance.getBiome(pos);
}
}
@Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/levelgen/SurfaceRules$Context;<init>(Lnet/minecraft/world/level/levelgen/SurfaceSystem;Lnet/minecraft/world/level/levelgen/RandomState;Lnet/minecraft/world/level/chunk/ChunkAccess;Lnet/minecraft/world/level/levelgen/NoiseChunk;Ljava/util/function/Function;Lnet/minecraft/core/Registry;Lnet/minecraft/world/level/levelgen/WorldGenerationContext;)V"))
private void captureRealBlockColumn(CallbackInfo ci, @Local(ordinal = 0) LocalRef<BlockColumn> column,
@Local(ordinal = 0, argsOnly = true) ChunkAccess chunk,
@Share("prefetchColumn") LocalRef<PrefetchingBlockColumn> prefetchRef) {
var prefetchingBlockColumn = MFIX_BLOCK_COLUMN.get();
if (prefetchingBlockColumn == null || prefetchingBlockColumn.getExpectedHeight() != chunk.getHeight()) {
prefetchingBlockColumn = new PrefetchingBlockColumn(chunk.getHeight());
MFIX_BLOCK_COLUMN.set(prefetchingBlockColumn);
}
column.set(prefetchingBlockColumn);
prefetchRef.set(prefetchingBlockColumn);
}
@Inject(method = "buildSurface", at = @At(value = "INVOKE", target = "Lnet/minecraft/core/BlockPos$MutableBlockPos;setZ(I)Lnet/minecraft/core/BlockPos$MutableBlockPos;", ordinal = 0, shift = At.Shift.AFTER))
private void prefetchBlockArray(RandomState randomState, BiomeManager biomeManager, Registry<Biome> biomes, boolean p_224652_,
WorldGenerationContext context, ChunkAccess chunk, NoiseChunk noiseChunk, SurfaceRules.RuleSource ruleSource, CallbackInfo ci,
@Local(ordinal = 0) BlockColumn column,
@Local(ordinal = 0) BlockPos.MutableBlockPos cursor) {
((PrefetchingBlockColumn)column).prefetch(chunk, cursor.getX() & 15, cursor.getZ() & 15);
}
}

View File

@ -0,0 +1,96 @@
package org.embeddedt.modernfix.common.mixin.perf.release_protochunks;
import com.mojang.datafixers.util.Either;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder;
import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap;
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 java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReferenceArray;
@Mixin(ChunkHolder.class)
public class ChunkHolderMixin implements IClearableChunkHolder {
@Shadow
@Final
private AtomicReferenceArray<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> futures;
@Shadow
private CompletableFuture<ChunkAccess> chunkToSave;
@Shadow
private int ticketLevel;
@Shadow
@Final
private ChunkPos pos;
@Shadow
@Final
private ChunkHolder.PlayerProvider playerProvider;
/**
* Used to track the number of neighboring holders actively using this chunk for generation.
*/
@Unique
private final AtomicInteger mfix$generationRefCount = new AtomicInteger(0);
@Override
public void mfix$resetProtoChunkFutures() {
int len = this.futures.length();
for (int i = 0; i < len; i++) {
this.futures.set(i, null);
}
this.chunkToSave = CompletableFuture.completedFuture(null);
}
@Override
public AtomicInteger mfix$getGenerationRefCount() {
return this.mfix$generationRefCount;
}
/*
* The methods below trigger the ChunkMap to check whether this holder can be "suspended" (have its ProtoChunk-only
* futures cleared) each time a new version of the chunkToSave future has completed. The ChunkMap itself
* also verifies that all conditions are still met for suspension in case the holder has become necessary
* again in the meantime.
*/
@Inject(method = "addSaveDependency", at = @At("RETURN"))
private void recheckSuspensionAfterNeighbor(String source, CompletableFuture<?> future, CallbackInfo ci) {
this.mfix$markAsNeedingProtoChunkDrop();
}
@Inject(method = "updateChunkToSave", at = @At("RETURN"))
private void checkSuspension(CallbackInfo ci) {
this.mfix$markAsNeedingProtoChunkDrop();
}
@Inject(method = "updateFutures", at = @At("RETURN"))
private void markForSuspensionOnDemotion(ChunkMap chunkMap, Executor executor, CallbackInfo ci) {
this.mfix$markAsNeedingProtoChunkDrop();
}
private void mfix$markAsNeedingProtoChunkDrop() {
if (this.ticketLevel >= LOWEST_DROPPABLE_TICKET_LEVEL
&& ChunkLevel.isLoaded(this.ticketLevel)) {
// register for suspension check when chain completes
var map = ((ISuspendedHolderTrackingChunkMap)this.playerProvider);
this.chunkToSave.whenCompleteAsync((r, e) -> {
map.mfix$markForSuspensionCheck(this.pos);
}, map.mfix$getMainThreadExecutor());
}
}
}

View File

@ -0,0 +1,157 @@
package org.embeddedt.modernfix.common.mixin.perf.release_protochunks;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.datafixers.util.Either;
import it.unimi.dsi.fastutil.longs.Long2IntOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectLinkedOpenHashMap;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.util.thread.BlockableEventLoop;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import org.embeddedt.modernfix.duck.release_protochunks.IClearableChunkHolder;
import org.embeddedt.modernfix.duck.release_protochunks.ISuspendedHolderTrackingChunkMap;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.function.BooleanSupplier;
@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin implements ISuspendedHolderTrackingChunkMap {
private static final int MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING = 100;
@Shadow
@Final
public Long2ObjectLinkedOpenHashMap<ChunkHolder> updatingChunkMap;
@Shadow
@Final
private BlockableEventLoop<Runnable> mainThreadExecutor;
@Shadow
protected abstract void lambda$scheduleUnload$14(ChunkHolder holder, CompletableFuture<ChunkAccess> chunkToSaveFuture, long chunkPos, ChunkAccess chunk);
@Shadow
@Final
public Long2ObjectLinkedOpenHashMap<ChunkHolder> pendingUnloads;
private final Long2IntOpenHashMap mfix$protoChunksToDrop = new Long2IntOpenHashMap();
private int mfix$dropTickCounter = 0;
/**
* @author embeddedt
* @reason We keep track of ChunkHolders that only contain protochunks, and are not loaded to a full status.
* This hook unloads their contents once there are no generation tasks actively relying on the chunk.
*/
@Inject(method = "processUnloads(Ljava/util/function/BooleanSupplier;)V", at = @At("RETURN"))
private void dropProtoChunks(BooleanSupplier hasMoreTime, CallbackInfo ci) {
int suspended = 0;
int iterations = 0;
mfix$dropTickCounter++;
var dropIterator = mfix$protoChunksToDrop.long2IntEntrySet().fastIterator();
while (dropIterator.hasNext() && suspended < 50 && iterations < 500 && (hasMoreTime.getAsBoolean() || mfix$protoChunksToDrop.size() > 1000)) {
iterations++;
var entry = dropIterator.next();
long pos = entry.getLongKey();
ChunkHolder holder = this.updatingChunkMap.get(pos);
if (holder == null // already removed
|| holder.getTicketLevel() < IClearableChunkHolder.LOWEST_DROPPABLE_TICKET_LEVEL // promoted to FULL or adjacent to FULL chunk
|| !ChunkLevel.isLoaded(holder.getTicketLevel()) // is going to be dropped through normal code path
) {
dropIterator.remove();
continue;
}
if (!holder.getChunkToSave().isDone() // chunkToSave dependencies have not completed
|| ((IClearableChunkHolder)holder).mfix$getGenerationRefCount().get() != 0 // chunk is still being referenced by another chunk for generation
) {
// Not safe to suspend yet, reset timer
entry.setValue(mfix$dropTickCounter);
continue;
}
if ((mfix$dropTickCounter - entry.getIntValue()) < MFIX$TICKS_TO_WAIT_BEFORE_SUSPENDING) {
// Chunk has not been idle for long enough, wait
continue;
}
// All generation work done, so we can suspend and remove from set
dropIterator.remove();
var chunkToSaveFuture = holder.getChunkToSave();
// Execute the logic inside scheduleUnload() inline, without delegating to a queue
// When this returns it is safe to release any data the ChunkHolder holds
this.pendingUnloads.put(pos, holder);
this.lambda$scheduleUnload$14(holder, chunkToSaveFuture, pos, chunkToSaveFuture.getNow(null));
((IClearableChunkHolder)holder).mfix$resetProtoChunkFutures();
suspended++;
}
}
/**
* @author embeddedt
* @reason increment the generation ref count on all neighboring chunk holders within the range when a generation
* task starts
*/
@Inject(method = "scheduleChunkGeneration", at = @At("HEAD"))
private void incrementGenRefCounts(ChunkHolder chunkHolder, ChunkStatus chunkStatus, CallbackInfoReturnable<CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>>> cir) {
int range = chunkStatus.getRange();
ChunkPos center = chunkHolder.getPos();
for (int dx = -range; dx <= range; dx++) {
for (int dz = -range; dz <= range; dz++) {
ChunkHolder neighbor = this.updatingChunkMap.get(ChunkPos.asLong(center.x + dx, center.z + dz));
if (neighbor != null) {
((IClearableChunkHolder)neighbor).mfix$getGenerationRefCount().incrementAndGet();
}
}
}
}
/**
* @author embeddedt
* @reason decrement the generation ref count on all neighboring chunk holders within the range when the generation
* task is completely finished
*/
@ModifyReturnValue(method = "scheduleChunkGeneration", at = @At("RETURN"))
private CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> decrementGenRefCountsOnComplete(CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> future,
@Local(ordinal = 0, argsOnly = true) ChunkHolder chunkHolder,
@Local(ordinal = 0, argsOnly = true) ChunkStatus chunkStatus) {
int range = chunkStatus.getRange();
ChunkPos center = chunkHolder.getPos();
return future.whenCompleteAsync((result, error) -> {
for (int dx = -range; dx <= range; dx++) {
for (int dz = -range; dz <= range; dz++) {
ChunkHolder neighbor = this.updatingChunkMap.get(ChunkPos.asLong(center.x + dx, center.z + dz));
if (neighbor != null) {
((IClearableChunkHolder)neighbor).mfix$getGenerationRefCount().decrementAndGet();
}
}
}
}, this.mainThreadExecutor);
}
@Override
public void mfix$markForSuspensionCheck(ChunkPos pos) {
this.mfix$protoChunksToDrop.put(pos.toLong(), this.mfix$dropTickCounter);
}
@Override
public Executor mfix$getMainThreadExecutor() {
return this.mainThreadExecutor;
}
}

View File

@ -0,0 +1,46 @@
package org.embeddedt.modernfix.common.mixin.perf.release_protochunks;
import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import com.llamalad7.mixinextras.sugar.Local;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.chunk.ImposterProtoChunk;
import org.embeddedt.modernfix.ModernFix;
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;
@Mixin(ImposterProtoChunk.class)
public class ImposterProtoChunkMixin {
@Shadow
@Final
private boolean allowWrites;
/**
* @author embeddedt
* @reason This is a workaround for a very complicated and subtle vanilla issue. Vanilla uses ImposterProtoChunk as
* a way of exposing fully generated chunks to other chunks that are still working on earlier generation stages.
* The problem is that these fully generated chunks may be in two different states: promoted to FULL and already
* visible to the level (with real BlockEntity objects), or in a loaded but not yet promoted state, where the postload
* hook has not yet run to convert the NBT-serialized block entities from the disk into real BlockEntity objects.
* The former state is the problematic one. If such a chunk is exposed to worldgen, features/structures may try
* to interact with the block entity (e.g. by calling setChanged on it). This has the potential to deadlock.
* <p></p>
* The solution we use here is to simply hide the existence of any "real" BE that has a level attached from worldgen.
* This is consistent with what other code would observe if the fully generated chunk were to be saved to disk
* and then reloaded (ending up in the latter state), so it should not break well-behaved mods.
* <p></p>
* This problem occurs rather often with `mixin.perf.release_protochunks` enabled, because it significantly increases
* the chance of a promoted LevelChunk wrapped in ImposterProtoChunk being used for world generation.
*/
@ModifyExpressionValue(method = "getBlockEntity", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunk;getBlockEntity(Lnet/minecraft/core/BlockPos;)Lnet/minecraft/world/level/block/entity/BlockEntity;"))
private BlockEntity avoidLeakingLiveBE(BlockEntity original, @Local(ordinal = 0, argsOnly = true) BlockPos pos) {
if (!this.allowWrites && original != null && original.getLevel() != null) {
ModernFix.LOGGER.debug("Blocked accessing the main level BlockEntity at {} from the ImposterProtoChunk wrapper, as this is unsafe during worldgen.", pos, new Exception("Stacktrace"));
return null;
} else {
return original;
}
}
}

View File

@ -1,24 +1,99 @@
package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks;
import com.llamalad7.mixinextras.sugar.Local;
import com.mojang.serialization.Dynamic;
import net.minecraft.core.SectionPos;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtOps;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.TicketType;
import net.minecraft.server.level.progress.ChunkProgressListener;
import net.minecraft.util.Mth;
import net.minecraft.util.Unit;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraft.world.level.dimension.DimensionType;
import net.minecraft.world.level.storage.WorldData;
import org.apache.commons.lang3.tuple.Pair;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.duck.ISpawnTrackingMinecraftServer;
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;
import javax.annotation.Nullable;
@Mixin(value = MinecraftServer.class, priority = 1100)
public class MinecraftServerMixin {
public abstract class MinecraftServerMixin implements ISpawnTrackingMinecraftServer {
@Shadow
public abstract boolean isDedicatedServer();
@Shadow
public abstract WorldData getWorldData();
@Shadow
@Nullable
public abstract ServerLevel getLevel(ResourceKey<Level> dimension);
private Pair<ResourceKey<Level>, ChunkPos> mfix$initialSpawnLocation;
private @Nullable Pair<ResourceKey<Level>, ChunkPos> loadPlayerSpawnLocation() {
CompoundTag player = this.getWorldData().getLoadedPlayerTag();
if (player == null) {
return null;
}
ListTag pos = player.getList("Pos", CompoundTag.TAG_DOUBLE);
double x = pos.getDouble(0);
double z = pos.getDouble(2);
// Dimension
ResourceKey<Level> dimension = DimensionType.parseLegacy(
new Dynamic<>(NbtOps.INSTANCE, player.get("Dimension"))
).resultOrPartial(ModernFix.LOGGER::error).orElse(Level.OVERWORLD);
return Pair.of(dimension, new ChunkPos(SectionPos.blockToSectionCoord(Mth.floor(x)), SectionPos.blockToSectionCoord(Mth.floor(z))));
}
@Redirect(method = "prepareLevels", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;addRegionTicket(Lnet/minecraft/server/level/TicketType;Lnet/minecraft/world/level/ChunkPos;ILjava/lang/Object;)V"))
private void addSpawnChunkTicket(ServerChunkCache cache, TicketType<?> type, ChunkPos pos, int distance, Object o) {
// load first chunk
cache.getChunk(pos.x, pos.z, ChunkStatus.FULL, true);
private void addSpawnChunkTicket(ServerChunkCache cache, TicketType<?> type, ChunkPos pos, int distance, Object o, @Local(ordinal = 0, argsOnly = true) ChunkProgressListener listener) {
if (!this.isDedicatedServer()) {
// Temporarily create a START ticket around the player to load the world in parallel with client join
// We remove it once the player has joined the world
var pair = this.mfix$initialSpawnLocation = loadPlayerSpawnLocation();
if (pair != null) {
var level = this.getLevel(pair.getLeft());
if (level != null) {
cache = level.getChunkSource();
pos = pair.getRight();
}
}
listener.updateSpawnPos(pos);
cache.addRegionTicket(TicketType.START, pos, 0, Unit.INSTANCE);
} else {
// just trigger sync load of initial spawn once
// TODO: figure out if this magic is still needed
cache.getChunk(pos.x, pos.z, true);
}
}
@Redirect(method = "prepareLevels", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerChunkCache;getTickingGenerated()I"), require = 0)
private int getGenerated(ServerChunkCache cache) {
return 441;
}
@Override
public Pair<ResourceKey<Level>, ChunkPos> mfix$getInitialStartTicketLocation() {
var pair = this.mfix$initialSpawnLocation;
this.mfix$initialSpawnLocation = null;
return pair;
}
}

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks;
import net.minecraft.network.Connection;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.level.TicketType;
import net.minecraft.server.players.PlayerList;
import net.minecraft.util.Unit;
import org.embeddedt.modernfix.duck.ISpawnTrackingMinecraftServer;
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(PlayerList.class)
public class PlayerListMixin {
@Inject(method = "placeNewPlayer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerLevel;addNewPlayer(Lnet/minecraft/server/level/ServerPlayer;)V", shift = At.Shift.AFTER))
private void removeStartTicket(Connection netManager, ServerPlayer player, CallbackInfo ci) {
var initial = ((ISpawnTrackingMinecraftServer)player.server).mfix$getInitialStartTicketLocation();
if (initial != null) {
var level = player.server.getLevel(initial.getLeft());
if (level != null) {
level.getChunkSource().removeRegionTicket(TicketType.START, initial.getRight(), 0, Unit.INSTANCE);
}
}
}
}

View File

@ -1,12 +0,0 @@
package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks;
import net.minecraft.server.level.DistanceManager;
import net.minecraft.server.level.ServerChunkCache;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
@Mixin(ServerChunkCache.class)
public interface ServerChunkCacheAccessor {
@Accessor("distanceManager")
DistanceManager getDistanceManager();
}

View File

@ -0,0 +1,20 @@
package org.embeddedt.modernfix.common.mixin.perf.remove_spawn_chunks;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
@Mixin(ServerPlayer.class)
public class ServerPlayerMixin {
/**
* @author embeddedt
* @reason do not waste time loading the wrong chunks and placing the player there just to correct it later
*/
@WrapWithCondition(method = "<init>", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/level/ServerPlayer;fudgeSpawnLocation(Lnet/minecraft/server/level/ServerLevel;)V"))
private boolean skipFudgingForSPOwner(ServerPlayer player, ServerLevel targetLevel) {
return targetLevel.getServer().getWorldData().getLoadedPlayerTag() == null
|| !targetLevel.getServer().isSingleplayerOwner(player.getGameProfile());
}
}

View File

@ -0,0 +1,95 @@
package org.embeddedt.modernfix.common.mixin.perf.resourcepacks;
import net.minecraft.server.packs.FilePackResources;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.PackType;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.embeddedt.modernfix.resources.ZipPackIndex;
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.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.io.File;
import java.io.IOException;
import java.util.Set;
import java.util.zip.ZipFile;
@Mixin(FilePackResources.class)
@RequiresFeatureLevel(FeatureLevel.BETA)
public class FilePackResourcesMixin {
@Final
@Shadow private File file;
@Shadow @Nullable private ZipFile getOrCreateZipFile() { return null; }
@Unique
@Nullable
private volatile ZipPackIndex mf$packIndex;
@Unique
@Nullable
private ZipPackIndex mf$getOrCreateIndex() {
var index = mf$packIndex;
if (index == null) {
synchronized (this) {
index = mf$packIndex;
if (index == null) {
// Ensure the ZipFile is open first; if it fails, getOrCreateZipFile returns null.
if (getOrCreateZipFile() == null) {
return null;
}
try {
mf$packIndex = index = new ZipPackIndex(file.toPath());
} catch (IOException e) {
ModernFix.LOGGER.error("Failed to build zip index for {}", file, e);
}
}
}
}
return index;
}
/**
* @author embeddedt
* @reason use the index instead of scanning the whole zip
*/
@Inject(method = "getNamespaces", at = @At("HEAD"), cancellable = true)
private void mf$getNamespaces(PackType type, CallbackInfoReturnable<Set<String>> cir) {
ZipPackIndex index = mf$getOrCreateIndex();
if (index != null) {
cir.setReturnValue(index.getNamespaces(type));
}
}
/**
* @author embeddedt
* @reason use the index instead of scanning the whole zip
*/
@Inject(method = "listResources", at = @At("HEAD"), cancellable = true)
private void mf$listResources(PackType packType, String namespace, String path,
PackResources.ResourceOutput resourceOutput, CallbackInfo ci) {
ZipFile zf = getOrCreateZipFile();
ZipPackIndex index = mf$getOrCreateIndex();
if (index != null && zf != null) {
index.listResources(packType, namespace, path, zf, resourceOutput);
ci.cancel();
}
}
/**
* Drop the index when the pack is closed so it can be rebuilt cleanly if the
* pack is ever re-opened.
*/
@Inject(method = "close", at = @At("HEAD"))
private void mf$invalidateIndex(CallbackInfo ci) {
mf$packIndex = null;
}
}

View File

@ -0,0 +1,30 @@
package org.embeddedt.modernfix.common.mixin.perf.resourcepacks;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.server.packs.repository.RepositorySource;
import net.minecraftforge.forgespi.locating.IModFile;
import net.minecraftforge.resource.PathPackResources;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Function;
@Mixin(MinecraftServer.class)
public class MinecraftServerMixin {
private static final Set<PackRepository> MFIX$INJECTED_REPOSITORIES = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap<>()));
/**
* @author embeddedt
* @reason we do not want to inject the Forge pack finder more than once to any given repository
*/
@WrapWithCondition(method = "configurePackRepository", at = @At(value = "INVOKE", target = "Lnet/minecraftforge/resource/ResourcePackLoader;loadResourcePacks(Lnet/minecraft/server/packs/repository/PackRepository;Ljava/util/function/Function;)V"))
private static boolean skipInjectIfAlreadyInjected(PackRepository resourcePacks, Function<Map<IModFile, ? extends PathPackResources>, ? extends RepositorySource> packFinder) {
return MFIX$INJECTED_REPOSITORIES.add(resourcePacks);
}
}

View File

@ -0,0 +1,36 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import net.minecraft.client.Minecraft;
import net.minecraft.client.multiplayer.ClientPacketListener;
import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
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;
@Mixin(ClientPacketListener.class)
@ClientOnlyMixin
public class ClientPacketListenerMixin {
@Shadow
@Final
private Minecraft minecraft;
@Inject(method = "handleCustomPayload", at = @At("HEAD"), cancellable = true)
private void detectClientLoadSentinel(ClientboundCustomPayloadPacket packet, CallbackInfo ci) {
if (packet.getIdentifier().equals(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL)) {
// Important: flag must be changed on the client thread, as later packets can start decoding
// while earlier ones are still being applied.
this.minecraft.executeIfPossible(() -> {
packet.getData().release();
if (this.minecraft.hasSingleplayerServer()) {
((IDeferrableIntegratedServer)this.minecraft.getSingleplayerServer()).mfix$markClientLoadFinished();
}
});
ci.cancel();
}
}
}

View File

@ -0,0 +1,83 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.mojang.datafixers.DataFixer;
import net.minecraft.client.Minecraft;
import net.minecraft.client.server.IntegratedServer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.Services;
import net.minecraft.server.WorldStem;
import net.minecraft.server.level.progress.ChunkProgressListenerFactory;
import net.minecraft.server.packs.repository.PackRepository;
import net.minecraft.world.level.storage.LevelStorageSource;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
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.net.Proxy;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.BooleanSupplier;
@Mixin(IntegratedServer.class)
@ClientOnlyMixin
public abstract class IntegratedServerMixin extends MinecraftServer implements IDeferrableIntegratedServer {
@Shadow
private boolean paused;
private int mfix$numTickServerCalls = 0;
private final AtomicBoolean mfix$hasPrimaryClientJoined = new AtomicBoolean(false);
public IntegratedServerMixin(Thread serverThread, LevelStorageSource.LevelStorageAccess storageSource, PackRepository packRepository, WorldStem worldStem, Proxy proxy, DataFixer fixerUpper, Services services, ChunkProgressListenerFactory progressListenerFactory) {
super(serverThread, storageSource, packRepository, worldStem, proxy, fixerUpper, services, progressListenerFactory);
}
/**
* @author embeddedt
* @reason Wait to be finished processing all expensive packets (recipes, tags, etc.)
* before continuing to tick the integrated server.
*/
@WrapOperation(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;isPaused()Z", ordinal = 0))
private boolean preventTicks(Minecraft instance, Operation<Boolean> original) {
return !mfix$hasPrimaryClientJoined.get() || original.call(instance);
}
/**
* @author embeddedt
* @reason Keep our own tick count for the integrated server specifically, rather than relying on super
* to increment.
*/
@Inject(method = "tickServer", at = @At("HEAD"))
private void mfix$countTicks(CallbackInfo ci) {
this.mfix$numTickServerCalls++;
}
/**
* @author embeddedt
* @reason If waiting for a client connection to exist, we only need to tick the server connection,
* not the whole server as vanilla does. However, we must tick the whole server once to accommodate mods
* that rely on the first tick to initialize state as a side effect. Not doing this causes issues like
* <a href="https://github.com/embeddedt/ModernFix/issues/639">#639</a>.
*/
@WrapWithCondition(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickServer(Ljava/util/function/BooleanSupplier;)V", ordinal = 0))
private boolean preventRunningFullServerTick(MinecraftServer server, BooleanSupplier hasTimeLeft) {
if (this.mfix$numTickServerCalls >= 2 && this.paused && !mfix$hasPrimaryClientJoined.get()) {
var conn = this.getConnection();
if (conn != null) {
conn.tick();
}
return false;
}
return true;
}
@Override
public void mfix$markClientLoadFinished() {
mfix$hasPrimaryClientJoined.set(true);
}
}

View File

@ -0,0 +1,24 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import net.minecraft.client.Minecraft;
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.Redirect;
@Mixin(Minecraft.class)
@ClientOnlyMixin
public class MinecraftMixin {
/**
* @author embeddedt
* @reason spin-waiting burns CPU time on the main thread, when the server thread is likely to take some time
* to be ready.
*/
@Redirect(method = "doWorldLoad", at = @At(value = "INVOKE", target = "Ljava/lang/Thread;yield()V"))
private void sleepInsteadOfYield() {
try {
Thread.sleep(16L);
} catch (InterruptedException ignored) {
}
}
}

View File

@ -0,0 +1,26 @@
package org.embeddedt.modernfix.common.mixin.perf.suspend_integrated_server_during_load;
import io.netty.buffer.Unpooled;
import net.minecraft.network.Connection;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.game.ClientboundCustomPayloadPacket;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.players.PlayerList;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.duck.suspend_integrated_server_during_load.IDeferrableIntegratedServer;
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(PlayerList.class)
@ClientOnlyMixin
public class PlayerListMixin {
@Inject(method = "placeNewPlayer", at = @At("RETURN"))
private void sendConfigFinishedSentinelPacket(Connection connection, ServerPlayer player, CallbackInfo ci) {
if (connection.isMemoryConnection()) {
FriendlyByteBuf friendlybytebuf = new FriendlyByteBuf(Unpooled.buffer());
player.connection.send(new ClientboundCustomPayloadPacket(IDeferrableIntegratedServer.CLIENT_LOAD_SENTINEL, friendlybytebuf));
}
}
}

View File

@ -1,28 +1,34 @@
package org.embeddedt.modernfix.common.mixin.safety;
import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import net.minecraft.client.Minecraft;
import net.minecraft.client.model.EntityModel;
import net.minecraft.client.renderer.entity.LivingEntityRenderer;
import net.minecraft.client.renderer.entity.layers.RenderLayer;
import net.minecraft.world.entity.Entity;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
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.Collections;
import java.util.List;
@Mixin(LivingEntityRenderer.class)
@ClientOnlyMixin
public class LivingEntityRendererMixin {
@Shadow @Final @Mutable
protected List<RenderLayer<?, ?>> layers;
public abstract class LivingEntityRendererMixin<T extends Entity, M extends EntityModel<T>> {
@Shadow
public abstract boolean addLayer(RenderLayer<T, M> layer);
@Inject(method = "<init>", at = @At("RETURN"))
private void synchronizeLayerList(CallbackInfo ci) {
/* allows buggy mods to call addLayer concurrently, order is not deterministic but can't fix that */
this.layers = Collections.synchronizedList(layers);
/**
* @author embeddedt
* @reason avoid CMEs from buggy mods calling addLayer on wrong thread
*/
@WrapMethod(method = "addLayer")
private boolean handleOffThreadLayerAdd(RenderLayer<T, M> layer, Operation<Boolean> original) {
if (!Minecraft.getInstance().isSameThread()) {
ModernFix.LOGGER.error("LivingEntityRenderer.addLayer called on wrong thread", new Exception());
Minecraft.getInstance().tell(() -> this.addLayer(layer));
return true;
}
return original.call(layer);
}
}

View File

@ -3,8 +3,10 @@ package org.embeddedt.modernfix.core;
import com.google.common.collect.ImmutableSet;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.core.config.ModernFixEarlyConfig;
import org.embeddedt.modernfix.core.config.Option;
import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
import org.embeddedt.modernfix.world.ThreadDumper;
import org.objectweb.asm.Opcodes;
@ -39,6 +41,11 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
this.logger.info("Loaded configuration file for ModernFix {}: {} options available, {} override(s) found",
ModernFixPlatformHooks.INSTANCE.getVersionString(), config.getOptionCount(), config.getOptionOverrideCount());
if(ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL != FeatureLevel.GA) {
this.logger.warn("ModernFix stability level is set to {}. Features at this level may be unstable or cause crashes.",
ModernFixEarlyConfig.ACTIVE_FEATURE_LEVEL);
}
config.getOptionMap().values().forEach(option -> {
if (option.isOverridden()) {
String source = "[unknown]";
@ -78,8 +85,8 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
while(true) {
try {
Thread.sleep(60000);
logger.error("------ DEBUG THREAD DUMP (occurs every 60 seconds) ------");
logger.error(ThreadDumper.obtainThreadDump());
logger.info("------ DEBUG THREAD DUMP (occurs every 60 seconds) ------");
logger.info(ThreadDumper.obtainThreadDump());
} catch(InterruptedException | RuntimeException e) {}
}
}
@ -109,7 +116,7 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
@Override
public void onLoad(String mixinPackage) {
CoreLaunchPluginService.install();
}
@Override
@ -128,10 +135,17 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
}
String mixin = mixinClassName.substring(MIXIN_PACKAGE_ROOT.length());
if(!instance.isOptionEnabled(mixin))
if(!instance.isOptionEnabled(mixin)) {
this.logger.debug("Skipping mixin {}: disabled by configuration", mixin);
return false;
}
String disabledBecauseMod = instance.config.getPermanentlyDisabledMixins().get(mixin);
return disabledBecauseMod == null;
if(disabledBecauseMod != null) {
this.logger.debug("Skipping mixin {}: disabled for mod compat ({})", mixin, disabledBecauseMod);
return false;
}
this.logger.debug("Applying mixin {}", mixin);
return true;
}
public boolean isOptionEnabled(String mixin) {
@ -300,4 +314,4 @@ public class ModernFixMixinPlugin implements IMixinConfigPlugin {
}
});
}
}
}

View File

@ -9,7 +9,9 @@ import org.apache.commons.lang3.SystemUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.embeddedt.modernfix.annotation.ClientOnlyMixin;
import org.embeddedt.modernfix.annotation.FeatureLevel;
import org.embeddedt.modernfix.annotation.IgnoreOutsideDev;
import org.embeddedt.modernfix.annotation.RequiresFeatureLevel;
import org.embeddedt.modernfix.annotation.RequiresMod;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.platform.ModernFixPlatformHooks;
@ -65,6 +67,18 @@ public class ModernFixEarlyConfig {
private static final String MIXIN_CLIENT_ONLY_DESC = Type.getDescriptor(ClientOnlyMixin.class);
private static final String MIXIN_REQUIRES_MOD_DESC = Type.getDescriptor(RequiresMod.class);
private static final String MIXIN_DEV_ONLY_DESC = Type.getDescriptor(IgnoreOutsideDev.class);
private static final String FEATURE_LEVEL_ANNOTATION_DESC = Type.getDescriptor(RequiresFeatureLevel.class);
public static final FeatureLevel ACTIVE_FEATURE_LEVEL = resolveFeatureLevel();
private static FeatureLevel resolveFeatureLevel() {
String prop = System.getProperty("modernfix.stabilityLevel", "ga").toUpperCase(Locale.ROOT);
try {
return FeatureLevel.valueOf(prop);
} catch (IllegalArgumentException e) {
return FeatureLevel.GA;
}
}
private static final Pattern PLATFORM_PREFIX = Pattern.compile("(forge|fabric|common)\\.");
@ -75,12 +89,58 @@ public class ModernFixEarlyConfig {
private final Set<String> mixinOptions = new ObjectOpenHashSet<>();
private final Map<String, String> mixinsMissingMods = new Object2ObjectOpenHashMap<>();
private static class PackageMetadata {
String requiredModId;
FeatureLevel requiredLevel;
}
private final Map<String, PackageMetadata> packageMetadataCache = new HashMap<>();
public static boolean isFabric = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream("modernfix-fabric.mixins.json") != null;
public Map<String, String> getPermanentlyDisabledMixins() {
return mixinsMissingMods;
}
@SuppressWarnings("unchecked")
private static <T> T getAnnotationValue(AnnotationNode ann, String key) {
if (ann.values == null) return null;
for (int i = 0; i < ann.values.size(); i += 2) {
if (ann.values.get(i).equals(key)) return (T) ann.values.get(i + 1);
}
return null;
}
private PackageMetadata loadPackageMetadata(String packageResourcePath) {
String classPath = packageResourcePath + "/package-info.class";
try (InputStream stream = ModernFixEarlyConfig.class.getClassLoader().getResourceAsStream(classPath)) {
if (stream == null) return new PackageMetadata();
ClassReader reader = new ClassReader(stream);
ClassNode node = new ClassNode();
reader.accept(node, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG);
PackageMetadata meta = new PackageMetadata();
List<AnnotationNode> annotations = new ArrayList<>();
if (node.invisibleAnnotations != null) annotations.addAll(node.invisibleAnnotations);
if (node.visibleAnnotations != null) annotations.addAll(node.visibleAnnotations);
for (AnnotationNode annotation : annotations) {
if (Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) {
meta.requiredModId = getAnnotationValue(annotation, "value");
} else if (Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) {
String[] enumVal = getAnnotationValue(annotation, "value");
meta.requiredLevel = FeatureLevel.valueOf(enumVal[1]);
}
}
return meta;
} catch (IOException e) {
LOGGER.error("Error scanning package-info " + classPath, e);
return new PackageMetadata();
}
}
private PackageMetadata getOrLoadPackageMetadata(String packageResourcePath) {
return packageMetadataCache.computeIfAbsent(packageResourcePath, this::loadPackageMetadata);
}
private void scanForAndBuildMixinOptions() {
List<String> configFiles = ImmutableList.of("modernfix-modernfix.mixins.json");
List<String> mixinPaths = new ArrayList<>();
@ -112,24 +172,48 @@ public class ModernFixEarlyConfig {
return;
boolean isMixin = false, isClientOnly = false, requiredModPresent = true, isDevOnly = false;
String requiredModId = "";
FeatureLevel requiredLevel = FeatureLevel.GA;
for(AnnotationNode annotation : node.invisibleAnnotations) {
if(Objects.equals(annotation.desc, MIXIN_DESC)) {
isMixin = true;
} else if(Objects.equals(annotation.desc, MIXIN_CLIENT_ONLY_DESC)) {
isClientOnly = true;
} else if(Objects.equals(annotation.desc, MIXIN_REQUIRES_MOD_DESC)) {
for(int i = 0; i < annotation.values.size(); i += 2) {
if(annotation.values.get(i).equals("value")) {
String modId = (String)annotation.values.get(i + 1);
if(modId != null) {
requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId);
requiredModId = modId;
}
break;
}
String modId = getAnnotationValue(annotation, "value");
if(modId != null) {
requiredModPresent = modId.startsWith("!") ? !modPresent(modId.substring(1)) : modPresent(modId);
requiredModId = modId;
}
} else if(Objects.equals(annotation.desc, MIXIN_DEV_ONLY_DESC)) {
isDevOnly = true;
} else if(Objects.equals(annotation.desc, FEATURE_LEVEL_ANNOTATION_DESC)) {
// ASM stores enum annotation values as String[]{typeDescriptor, constantName}
String[] enumVal = getAnnotationValue(annotation, "value");
requiredLevel = FeatureLevel.valueOf(enumVal[1]);
}
}
// Merge constraints from ancestor package-info files (up to the mixin root)
String classPackagePath = mixinPath.substring(0, mixinPath.lastIndexOf('/'));
int mixinRootEnd = classPackagePath.indexOf("/mixin");
if (mixinRootEnd >= 0) {
String mixinRoot = classPackagePath.substring(0, mixinRootEnd + "/mixin".length());
String walkPkg = mixinRoot;
while (walkPkg.length() < classPackagePath.length()) {
int nextSlash = classPackagePath.indexOf('/', walkPkg.length() + 1);
walkPkg = (nextSlash == -1) ? classPackagePath : classPackagePath.substring(0, nextSlash);
PackageMetadata pkgMeta = getOrLoadPackageMetadata(walkPkg);
if (requiredModPresent && pkgMeta.requiredModId != null) {
boolean present = pkgMeta.requiredModId.startsWith("!")
? !modPresent(pkgMeta.requiredModId.substring(1))
: modPresent(pkgMeta.requiredModId);
if (!present) {
requiredModPresent = false;
requiredModId = pkgMeta.requiredModId;
}
}
if (pkgMeta.requiredLevel != null && pkgMeta.requiredLevel.ordinal() > requiredLevel.ordinal()) {
requiredLevel = pkgMeta.requiredLevel;
}
}
}
if(isMixin && (!isDevOnly || ModernFixPlatformHooks.INSTANCE.isDevEnv())) {
@ -138,6 +222,8 @@ public class ModernFixEarlyConfig {
mixinsMissingMods.put(mixinClassName, requiredModId);
else if(isClientOnly && !ModernFixPlatformHooks.INSTANCE.isClient())
mixinsMissingMods.put(mixinClassName, "[not client]");
else if(!ACTIVE_FEATURE_LEVEL.isAtLeast(requiredLevel))
mixinsMissingMods.put(mixinClassName, "[feature level: requires " + requiredLevel + "]");
String mixinCategoryName = "mixin." + mixinClassName.substring(0, mixinClassName.lastIndexOf('.'));
mixinOptions.add(mixinCategoryName);
}
@ -179,10 +265,9 @@ public class ModernFixEarlyConfig {
.put("mixin.perf.dynamic_entity_renderers", false)
.put("mixin.feature.integrated_server_watchdog", true)
.put("mixin.perf.faster_item_rendering", false)
.put("mixin.perf.ingredient_item_deduplication", false)
.put("mixin.feature.spam_thread_dump", false)
.put("mixin.feature.disable_unihex_font", false)
.put("mixin.feature.remove_chat_signing", false)
.put("mixin.bugfix.skip_redundant_saves", false)
.put("mixin.feature.snapshot_easter_egg", true)
.put("mixin.feature.warn_missing_perf_mods", true)
.put("mixin.feature.spark_profile_launch", false)
@ -193,7 +278,12 @@ public class ModernFixEarlyConfig {
.putConditionally(() -> !isFabric, "mixin.bugfix.fix_config_crashes", true)
.putConditionally(() -> !isFabric, "mixin.bugfix.forge_at_inject_error", true)
.putConditionally(() -> !isFabric, "mixin.feature.registry_event_progress", false)
.putConditionally(() -> isFabric, "mixin.perf.clear_fabric_mapping_tables", false)
// Beta (promote on next release)
.put("mixin.perf.compact_entity_models", false)
.put("mixin.perf.dynamic_languages", false)
.put("mixin.perf.faster_capabilities.bytecode_analysis", false)
.put("mixin.perf.ingredient_item_deduplication", false)
// END
.build();
private ModernFixEarlyConfig(File file) {
@ -237,7 +327,7 @@ public class ModernFixEarlyConfig {
disableIfModPresent("mixin.bugfix.item_cache_flag", "lithium", "canary", "radium");
// DimThread makes changes to the server chunk manager (understandably), C2ME probably does the same
disableIfModPresent("mixin.bugfix.chunk_deadlock", "c2me", "dimthread");
disableIfModPresent("mixin.perf.reuse_datapacks", "tac");
disableIfModPresent("mixin.perf.release_protochunks", "c2me", "moonrise");
disableIfModPresent("mixin.launch.class_search_cache", "optifine");
disableIfModPresent("mixin.perf.faster_texture_stitching", "optifine");
disableIfModPresent("mixin.bugfix.entity_pose_stack", "optifine");
@ -245,7 +335,7 @@ public class ModernFixEarlyConfig {
disableIfModPresent("mixin.bugfix.buffer_builder_leak", "isometric-renders", "witherstormmod");
disableIfModPresent("mixin.feature.remove_chat_signing", "nochatreports");
disableIfModPresent("mixin.perf.faster_texture_loading", "stitch", "optifine", "changed");
disableIfModPresent("mixin.perf.faster_ingredients", "vmp");
disableIfModPresent("mixin.perf.faster_ingredients", "vmp", "prefab");
disableIfModPresent("mixin.perf.smart_ingredient_sync", "crafttweaker");
if(isFabric) {
disableIfModPresent("mixin.bugfix.packet_leak", "memoryleakfix");

View File

@ -0,0 +1,73 @@
package org.embeddedt.modernfix.core.launchplugin;
import cpw.mods.modlauncher.LaunchPluginHandler;
import cpw.mods.modlauncher.Launcher;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.embeddedt.modernfix.core.launchplugin.transformer.CapabilityProviderTransformer;
import org.objectweb.asm.Type;
import org.objectweb.asm.tree.ClassNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.Field;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
public class CoreLaunchPluginService implements ILaunchPluginService {
private static final Logger LOGGER = LoggerFactory.getLogger("ModernFixLaunchPlugin");
public static void install() {
try {
Field launchPluginsField = Launcher.class.getDeclaredField("launchPlugins");
launchPluginsField.setAccessible(true);
LaunchPluginHandler launchPluginHandler = (LaunchPluginHandler) launchPluginsField.get(Launcher.INSTANCE);
Field pluginsField = LaunchPluginHandler.class.getDeclaredField("plugins");
pluginsField.setAccessible(true);
Map<String, ILaunchPluginService> plugins = (Map<String, ILaunchPluginService>)pluginsField.get(launchPluginHandler);
var service = new CoreLaunchPluginService();
try {
plugins.put(service.name(), service);
} catch (Exception e) {
var newMap = new LinkedHashMap<>(plugins);
newMap.put(service.name(), service);
pluginsField.set(launchPluginHandler, newMap);
}
} catch(Exception e) {
LOGGER.error("Error installing launch plugin service", e);
}
}
@Override
public String name() {
return "modernfix";
}
private static final EnumSet<Phase> GO = EnumSet.of(Phase.AFTER);
private static final EnumSet<Phase> NOGO = EnumSet.noneOf(Phase.class);
private static final Map<String, Transformer> TRANSFORMERS = Map.of(
"net.minecraftforge.common.capabilities.CapabilityProvider", new CapabilityProviderTransformer()
);
@Override
public EnumSet<Phase> handlesClass(Type classType, boolean isEmpty) {
return !isEmpty && TRANSFORMERS.containsKey(classType.getClassName()) ? GO : NOGO;
}
@Override
public int processClassWithFlags(Phase phase, ClassNode classNode, Type classType, String reason) {
if (classNode == null) {
return 0;
}
var transformer = TRANSFORMERS.get(classType.getClassName());
if (transformer == null) {
return 0;
}
return transformer.transform(classNode);
}
public interface Transformer {
int transform(ClassNode node);
}
}

View File

@ -0,0 +1,102 @@
package org.embeddedt.modernfix.core.launchplugin.transformer;
import cpw.mods.modlauncher.serviceapi.ILaunchPluginService;
import org.embeddedt.modernfix.core.launchplugin.CoreLaunchPluginService;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
/**
* Injects an early-return into {@code CapabilityProvider#areCapsCompatible} that skips lazy
* initialization when both providers are lazy, uninitialized, of the same class, and carry equal
* {@code lazyData}. In that case the capabilities are trivially compatible and the full init can
* be avoided.
* @author embeddedt
*/
public class CapabilityProviderTransformer implements CoreLaunchPluginService.Transformer {
private static final String OWNER = "net/minecraftforge/common/capabilities/CapabilityProvider";
private static final String COMPOUND_TAG = "net/minecraft/nbt/CompoundTag";
private static final String HELPER_NAME = "mfix$skipLazyInit";
private static final String HELPER_DESC = "(L" + OWNER + ";L" + OWNER + ";)Z";
@Override
public int transform(ClassNode node) {
String targetDesc = "(L" + OWNER + ";)Z";
for (MethodNode method : node.methods) {
if (method.name.equals("areCapsCompatible") && method.desc.equals(targetDesc)) {
injectCall(method);
node.methods.add(buildHelper());
break;
}
}
return ILaunchPluginService.ComputeFlags.COMPUTE_FRAMES;
}
private MethodNode buildHelper() {
MethodNode mn = new MethodNode(
Opcodes.ACC_PRIVATE | Opcodes.ACC_STATIC,
HELPER_NAME, HELPER_DESC, null, null);
InsnList il = mn.instructions;
LabelNode returnFalse = new LabelNode();
// if (!self.isLazy) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z"));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
// if (self.initialized) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z"));
il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse));
// if (!other.isLazy) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "isLazy", "Z"));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
// if (other.initialized) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "initialized", "Z"));
il.add(new JumpInsnNode(Opcodes.IFNE, returnFalse));
// if (other.getClass() != self.getClass()) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false));
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new MethodInsnNode(Opcodes.INVOKEVIRTUAL, "java/lang/Object", "getClass", "()Ljava/lang/Class;", false));
il.add(new JumpInsnNode(Opcodes.IF_ACMPNE, returnFalse));
// if (!Objects.equals(self.lazyData, other.lazyData)) goto returnFalse
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";"));
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new FieldInsnNode(Opcodes.GETFIELD, OWNER, "lazyData", "L" + COMPOUND_TAG + ";"));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, "java/util/Objects", "equals", "(Ljava/lang/Object;Ljava/lang/Object;)Z", false));
il.add(new JumpInsnNode(Opcodes.IFEQ, returnFalse));
il.add(new InsnNode(Opcodes.ICONST_1));
il.add(new InsnNode(Opcodes.IRETURN));
il.add(returnFalse);
il.add(new InsnNode(Opcodes.ICONST_0));
il.add(new InsnNode(Opcodes.IRETURN));
mn.maxLocals = 2;
mn.maxStack = 2;
return mn;
}
private void injectCall(MethodNode method) {
InsnList il = new InsnList();
LabelNode skip = new LabelNode();
il.add(new VarInsnNode(Opcodes.ALOAD, 0));
il.add(new VarInsnNode(Opcodes.ALOAD, 1));
il.add(new MethodInsnNode(Opcodes.INVOKESTATIC, OWNER, HELPER_NAME, HELPER_DESC, false));
il.add(new JumpInsnNode(Opcodes.IFEQ, skip));
il.add(new InsnNode(Opcodes.ICONST_1));
il.add(new InsnNode(Opcodes.IRETURN));
il.add(skip);
method.instructions.insert(il);
}
}

View File

@ -0,0 +1,5 @@
package org.embeddedt.modernfix.duck;
public interface IBatchingCapEvent {
void mfix$sortCaps();
}

View File

@ -1,7 +1,9 @@
package org.embeddedt.modernfix.duck;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.MinecraftServer;
import java.nio.file.Path;
public interface IChunkGenerator {
void mfix$setAssociatedServerLevel(ServerLevel level);
void mfix$setStrongholdCachePath(Path cachePath, MinecraftServer server);
}

View File

@ -1,7 +0,0 @@
package org.embeddedt.modernfix.duck;
import org.embeddedt.modernfix.world.StrongholdLocationCache;
public interface IServerLevel {
StrongholdLocationCache mfix$getStrongholdCache();
}

View File

@ -0,0 +1,10 @@
package org.embeddedt.modernfix.duck;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import org.apache.commons.lang3.tuple.Pair;
public interface ISpawnTrackingMinecraftServer {
Pair<ResourceKey<Level>, ChunkPos> mfix$getInitialStartTicketLocation();
}

View File

@ -0,0 +1,17 @@
package org.embeddedt.modernfix.duck.release_protochunks;
import net.minecraft.server.level.ChunkLevel;
import net.minecraft.server.level.FullChunkStatus;
import java.util.concurrent.atomic.AtomicInteger;
public interface IClearableChunkHolder {
/**
* We don't want to drop FULL chunks, or chunks immediately surrouding FULL. So + 2 is the minimum we can drop.
*/
int LOWEST_DROPPABLE_TICKET_LEVEL = ChunkLevel.byStatus(FullChunkStatus.FULL) + 2;
void mfix$resetProtoChunkFutures();
AtomicInteger mfix$getGenerationRefCount();
}

View File

@ -0,0 +1,11 @@
package org.embeddedt.modernfix.duck.release_protochunks;
import net.minecraft.world.level.ChunkPos;
import java.util.concurrent.Executor;
public interface ISuspendedHolderTrackingChunkMap {
void mfix$markForSuspensionCheck(ChunkPos pos);
Executor mfix$getMainThreadExecutor();
}

View File

@ -0,0 +1,10 @@
package org.embeddedt.modernfix.duck.suspend_integrated_server_during_load;
import net.minecraft.resources.ResourceLocation;
import org.embeddedt.modernfix.ModernFix;
public interface IDeferrableIntegratedServer {
ResourceLocation CLIENT_LOAD_SENTINEL = new ResourceLocation(ModernFix.MODID, "mark_client_load_finished");
void mfix$markClientLoadFinished();
}

View File

@ -0,0 +1,60 @@
package org.embeddedt.modernfix.dynamiclanguages;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Maps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.locale.Language;
import net.minecraft.server.packs.resources.Resource;
import org.embeddedt.modernfix.ModernFix;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class DynamicLanguageMap {
private static Map<String, Object> createStorage(Map<String, String> rawLanguageContents, List<Resource> languageResources) {
Map<String, Object> storage = new HashMap<>(rawLanguageContents);
for (var resource : languageResources) {
try (var stream = resource.open()) {
Language.loadFromJson(stream, (key, value) -> {
if (value != null && value.equals(storage.get(key))) {
storage.put(key, resource);
}
});
} catch (Exception ignored) {
}
}
return Map.copyOf(storage);
}
public static Map<String, String> forVanillaData(Map<String, String> rawLanguageContents, List<Resource> languageResources) {
Map<String, Object> storage = createStorage(rawLanguageContents, languageResources);
LoadingCache<Resource, Map<String, String>> languageFileContents = CacheBuilder.newBuilder()
.softValues()
.build(new CacheLoader<>() {
@Override
public Map<String, String> load(Resource resource) throws Exception {
Map<String, String> data = new Object2ObjectOpenHashMap<>();
try (var stream = resource.open()) {
Language.loadFromJson(stream, data::put);
} catch (IOException e) {
ModernFix.LOGGER.error("Error loading language data from {}", resource.sourcePackId(), e);
}
return data;
}
});
return Maps.asMap(storage.keySet(), k -> {
var value = storage.get(k);
if (value instanceof Resource r) {
return languageFileContents.getUnchecked(r).getOrDefault(k, "");
} else if (value instanceof String s) {
return s;
} else {
return null;
}
});
}
}

View File

@ -2,6 +2,8 @@ package org.embeddedt.modernfix.dynamicresources;
import com.google.common.collect.ImmutableSet;
import com.mojang.math.Transformation;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.client.renderer.block.model.BakedQuad;
import net.minecraft.client.renderer.block.model.ItemOverrides;
@ -248,4 +250,47 @@ public class DynamicBakedModelProvider implements Map<ResourceLocation, BakedMod
return function.apply(loc.id(), oldModel);
});
}
public void dumpStats() {
Object2ObjectOpenHashMap<Class<? extends BakedModel>, Object2IntOpenHashMap<String>> byClassAndNamespace = new Object2ObjectOpenHashMap<>();
Object2IntOpenHashMap<Class<? extends BakedModel>> totalsByClass = new Object2IntOpenHashMap<>();
synchronized (permanentOverrides) {
for (var entry : permanentOverrides.entrySet()) {
var model = entry.getValue();
if (model == null) {
continue;
}
totalsByClass.addTo(model.getClass(), 1);
var byNamespace = byClassAndNamespace.computeIfAbsent(model.getClass(), $ -> new Object2IntOpenHashMap<>());
byNamespace.addTo(entry.getKey().getNamespace(), 1);
}
}
ModernFix.LOGGER.debug("Loaded {} permanent overrides", permanentOverrides.size());
byClassAndNamespace.entrySet().stream().sorted((a, b) ->
Integer.compare(
totalsByClass.getInt(b.getKey()),
totalsByClass.getInt(a.getKey())
))
.forEach(classEntry -> {
var byNamespace = classEntry.getValue();
int totalModels = totalsByClass.getInt(classEntry.getKey());
ModernFix.LOGGER.debug(
"{}: {} models",
classEntry.getKey().getName(),
totalModels
);
// sort namespaces by count (descending)
byNamespace.object2IntEntrySet().stream()
.sorted((a, b) ->
Integer.compare(b.getIntValue(), a.getIntValue()))
.forEach(nsEntry -> {
ModernFix.LOGGER.debug(
" {}: {}",
nsEntry.getKey(),
nsEntry.getIntValue()
);
});
});
}
}

View File

@ -0,0 +1,42 @@
package org.embeddedt.modernfix.entity;
import it.unimi.dsi.fastutil.Hash;
import it.unimi.dsi.fastutil.objects.ObjectOpenCustomHashSet;
import net.minecraft.world.entity.ai.attributes.AttributeInstance;
public class AttributeInstanceTemplates {
private static final ObjectOpenCustomHashSet<AttributeInstance> INTERNER = new ObjectOpenCustomHashSet<>(new Hash.Strategy<>() {
@Override
public int hashCode(AttributeInstance o) {
if (o == null) {
return 0;
}
int h = System.identityHashCode(o.getAttribute());
h = 31 * h + Double.hashCode(o.getBaseValue());
h = 31 * h + o.getModifiers().hashCode();
return h;
}
@Override
public boolean equals(AttributeInstance a, AttributeInstance b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return a.getAttribute() == b.getAttribute()
&& a.getBaseValue() == b.getBaseValue()
&& a.getModifiers().equals(b.getModifiers());
}
});
public static AttributeInstance intern(AttributeInstance a) {
if (a == null || a.getClass() != AttributeInstance.class) {
return a;
}
synchronized (INTERNER) {
return INTERNER.addOrGet(a);
}
}
}

View File

@ -1,16 +1,32 @@
package org.embeddedt.modernfix.forge.capability;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import net.minecraftforge.common.util.LazyOptional;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalysisResult;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityAnalyzer;
import org.embeddedt.modernfix.forge.capability.analysis.CapabilityRef;
import org.jetbrains.annotations.NotNull;
import org.objectweb.asm.*;
import org.objectweb.asm.commons.GeneratorAdapter;
import org.objectweb.asm.commons.Method;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@ -23,6 +39,30 @@ import static org.objectweb.asm.Opcodes.*;
* and performs direct dispatch instead of megamorphic virtual calls.
*/
public class CapabilityProviderDispatcherGenerator {
/**
* Describes the dispatch strategy for a single capability provider in the generated class.
*/
sealed interface ProviderDispatch {
/** Provider handles a known capability - emit an identity guard before dispatch. */
record Guarded(int providerIndex, String fieldDesc, CapabilityRef capability) implements ProviderDispatch {}
/** Provider capabilities are unknown - dispatch unconditionally. */
record Unguarded(int providerIndex, String fieldDesc) implements ProviderDispatch {}
/** Multiple guarded dispatches collapsed into a Map lookup. */
record Hash(int mapIndex, List<Guarded> entries) implements ProviderDispatch {}
}
/**
* Number of consecutive equality checks that must be performed to switch to a hash map.
*/
private static final int HASH_DISPATCH_THRESHOLD = 3;
private static final String GENERATED_CLASSES_FOLDER = System.getProperty("modernfix.generatedCapabilityDispatcherClassDumpFolder", "");
/**
* Sentinel used in generated guards to skip empty results via reference equality,
* avoiding a method call to {@code isPresent()}.
*/
public static final LazyOptional<?> EMPTY = LazyOptional.empty();
private static final ConcurrentHashMap<List<Class<? extends ICapabilityProvider>>, MethodHandle> cache =
new ConcurrentHashMap<>();
@ -35,6 +75,9 @@ public class CapabilityProviderDispatcherGenerator {
private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;";
private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;";
private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;";
private static final String MAP_DESC = "Ljava/util/Map;";
private static final String MAP_SIGNATURE = "Ljava/util/Map<Lnet/minecraftforge/common/capabilities/Capability<*>;Lnet/minecraftforge/common/capabilities/ICapabilityProvider;>;";
private static final String LOOKUP_DESC = "Ljava/lang/invoke/MethodHandles$Lookup;";
/**
* Gets or generates a constructor MethodHandle for the given capability provider types.
@ -51,9 +94,10 @@ public class CapabilityProviderDispatcherGenerator {
* Convenience method that takes an array of providers and returns the constructor.
*/
private static MethodHandle getOrGenerateConstructor(ICapabilityProvider[] providers) {
List<Class<? extends ICapabilityProvider>> types = Arrays.stream(providers)
.<Class<? extends ICapabilityProvider>>map(ICapabilityProvider::getClass)
.toList();
List<Class<? extends ICapabilityProvider>> types = new ArrayList<>(providers.length);
for (ICapabilityProvider provider : providers) {
types.add(provider.getClass());
}
return getOrGenerateConstructor(types);
}
@ -67,18 +111,52 @@ public class CapabilityProviderDispatcherGenerator {
}
private static MethodHandle generateClass(List<Class<? extends ICapabilityProvider>> providerTypes) {
ModernFix.LOGGER.debug("Generating capability dispatcher for types: [{}]", providerTypes.stream().map(Class::getName).collect(Collectors.joining(", ")));
try {
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + classCounter.incrementAndGet();
byte[] classBytes = generateClassBytes(className, providerTypes);
// Analyze each provider type
List<CapabilityAnalysisResult> analysisResults = new ArrayList<>(providerTypes.size());
for (Class<? extends ICapabilityProvider> type : providerTypes) {
CapabilityAnalysisResult result = CapabilityAnalyzer.analyze(type);
analysisResults.add(result);
}
// Define the hidden class
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClass(
int generatedClassId = classCounter.incrementAndGet();
String className = "org.embeddedt.modernfix.forge.capability.CapabilityDispatcher$Generated$" + generatedClassId;
List<ProviderDispatch> dispatches = optimizeDispatches(buildDispatchList(providerTypes, analysisResults));
// Assign a stable index to every unique CapabilityRef across all dispatches.
// We resolve the actual Capability<?> instances here (in Java) so the generated
// <clinit> only needs simple classDataAt calls - no reflection bytecode needed.
LinkedHashMap<CapabilityRef, Integer> capRefIndices = collectCapabilityRefs(dispatches);
List<Capability<?>> capValues = resolveCapabilityValues(capRefIndices);
ModernFix.LOGGER.debug("Generating capability dispatcher #{} for types: [{}]", () -> generatedClassId, () -> {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < providerTypes.size(); i++) {
if (i > 0) sb.append(", ");
sb.append(providerTypes.get(i).getName()).append(" -> ").append(formatAnalysisResult(analysisResults.get(i)));
}
return sb;
});
byte[] classBytes = generateClassBytes(className, providerTypes, dispatches, capRefIndices);
// Define the hidden class, injecting the resolved Capability instances as class data.
// The generated <clinit> retrieves them via MethodHandles.classDataAt so it never
// needs to perform reflection itself - private fields are handled transparently here.
MethodHandles.Lookup hiddenLookup = lookup.defineHiddenClassWithClassData(
classBytes,
capValues,
true,
MethodHandles.Lookup.ClassOption.NESTMATE
);
if (!GENERATED_CLASSES_FOLDER.isBlank()) {
Path path = Paths.get(GENERATED_CLASSES_FOLDER, "generatedDispatcher" + generatedClassId + ".class");
Files.createDirectories(path.getParent());
Files.write(path, classBytes);
}
// Return a MethodHandle to the constructor
// Constructor signature: (ICapabilityProvider[])V
// The constructor is adapted to take an Object and return an ICapabilityProvider to match
@ -92,66 +170,346 @@ public class CapabilityProviderDispatcherGenerator {
}
}
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
/**
* Collects all unique {@link CapabilityRef}s referenced by {@code dispatches} in encounter order,
* assigning each a stable list index for use with {@code classDataAt}.
*/
private static LinkedHashMap<CapabilityRef, Integer> collectCapabilityRefs(List<ProviderDispatch> dispatches) {
LinkedHashMap<CapabilityRef, Integer> result = new LinkedHashMap<>();
for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Guarded g) {
result.putIfAbsent(g.capability(), result.size());
} else if (dispatch instanceof ProviderDispatch.Hash hash) {
for (ProviderDispatch.Guarded g : hash.entries()) {
result.putIfAbsent(g.capability(), result.size());
}
}
}
return result;
}
/**
* Resolves the actual {@link Capability} instances for all refs at class-generation time.
* Uses reflection (with {@code setAccessible}) so private fields are handled without any
* reflection bytecode appearing in the generated class.
* <p>
* Field lookup is delegated to {@link #getRefField(Class, CapabilityRef)}
*/
private static List<Capability<?>> resolveCapabilityValues(LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
@SuppressWarnings("unchecked")
Capability<?>[] caps = new Capability[capRefIndices.size()];
for (Map.Entry<CapabilityRef, Integer> entry : capRefIndices.entrySet()) {
CapabilityRef ref = entry.getKey();
try {
Class<?> clazz = Class.forName(ref.owner().replace('/', '.'), false,
CapabilityProviderDispatcherGenerator.class.getClassLoader());
Field field = getRefField(clazz, ref);
caps[entry.getValue()] = (Capability<?>) field.get(null);
} catch (ReflectiveOperationException e) {
throw new RuntimeException("Failed to resolve capability field " + ref, e);
}
}
return Arrays.asList(caps);
}
/**
* Resolves the {@link Field} for the given {@link CapabilityRef},
* falls back to the implemented interfaces if no match is found.
*/
private static @NotNull Field getRefField(Class<?> clazz, CapabilityRef ref) throws NoSuchFieldException {
Field field = null;
try {
field = clazz.getDeclaredField(ref.fieldName());
} catch (NoSuchFieldException ignored) {
for (Class<?> iface : clazz.getInterfaces()) {
try {
field = iface.getDeclaredField(ref.fieldName());
break;
} catch (NoSuchFieldException ignored1) {}
}
}
if (field == null) throw new NoSuchFieldException(ref.fieldName());
field.setAccessible(true);
return field;
}
/**
* Build the dispatch list describing how each provider should be handled.
*/
static List<ProviderDispatch> buildDispatchList(List<Class<? extends ICapabilityProvider>> providerTypes, List<CapabilityAnalysisResult> analysisResults) {
List<ProviderDispatch> dispatches = new ArrayList<>(providerTypes.size());
for (int i = 0; i < providerTypes.size(); i++) {
Class<? extends ICapabilityProvider> type = providerTypes.get(i);
String fieldDesc = (!type.isHidden() && Modifier.isPublic(type.getModifiers()))
? Type.getDescriptor(type) : ICAP_PROVIDER_DESC;
CapabilityAnalysisResult analysis = analysisResults.get(i);
if (analysis instanceof CapabilityAnalysisResult.AlwaysEmpty) {
// No dispatch needed - provider never returns a capability
} else if (analysis instanceof CapabilityAnalysisResult.KnownCapabilities known
&& known.capabilities().size() <= 5) {
for (CapabilityRef ref : known.capabilities()) {
dispatches.add(new ProviderDispatch.Guarded(i, fieldDesc, ref));
}
} else {
dispatches.add(new ProviderDispatch.Unguarded(i, fieldDesc));
}
}
return dispatches;
}
/**
* Collapse runs of 3+ consecutive Guarded dispatches into Hash dispatches.
* Duplicate CapabilityRefs within a run are kept as trailing Guarded entries
* after the Hash to preserve sequential fallthrough semantics.
*/
static List<ProviderDispatch> optimizeDispatches(List<ProviderDispatch> dispatches) {
List<ProviderDispatch> result = new ArrayList<>(dispatches.size());
int mapIndex = 0;
int i = 0;
while (i < dispatches.size()) {
// Collect a run of consecutive Guarded entries
int runStart = i;
while (i < dispatches.size() && dispatches.get(i) instanceof ProviderDispatch.Guarded) {
i++;
}
List<ProviderDispatch> run = dispatches.subList(runStart, i);
if (run.isEmpty()) {
// Not a Guarded entry, pass through
result.add(dispatches.get(i));
i++;
continue;
}
if (!tryCollapseToHash(run, mapIndex, result)) {
result.addAll(run);
} else {
mapIndex++;
}
}
return result;
}
/**
* Attempt to collapse a run of Guarded dispatches into a Hash.
* Returns true if a Hash was emitted, false if the run should be kept as-is.
*/
private static boolean tryCollapseToHash(List<ProviderDispatch> run, int mapIndex, List<ProviderDispatch> result) {
if (run.size() < HASH_DISPATCH_THRESHOLD) {
return false;
}
// Deduplicate by CapabilityRef - first occurrence goes into the hash,
// duplicates are kept as trailing Guarded entries for fallthrough
Set<CapabilityRef> seen = new HashSet<>();
List<ProviderDispatch.Guarded> hashEntries = new ArrayList<>();
List<ProviderDispatch.Guarded> duplicates = new ArrayList<>();
for (ProviderDispatch dispatch : run) {
ProviderDispatch.Guarded g = (ProviderDispatch.Guarded) dispatch;
if (seen.add(g.capability())) {
hashEntries.add(g);
} else {
duplicates.add(g);
}
}
if (hashEntries.size() < HASH_DISPATCH_THRESHOLD) {
return false;
}
result.add(new ProviderDispatch.Hash(mapIndex, hashEntries));
result.addAll(duplicates);
return true;
}
/**
* Collect all unique provider fields (index fieldDesc) referenced by a dispatch list,
* including those inside Hash entries.
*/
private static LinkedHashMap<Integer, String> collectProviderFields(List<ProviderDispatch> dispatches) {
LinkedHashMap<Integer, String> fields = new LinkedHashMap<>();
for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Guarded g) {
fields.putIfAbsent(g.providerIndex(), g.fieldDesc());
} else if (dispatch instanceof ProviderDispatch.Unguarded u) {
fields.putIfAbsent(u.providerIndex(), u.fieldDesc());
}
// Hash entries don't need provider fields - map reads from constructor array
}
return fields;
}
private static byte[] generateClassBytes(String className, List<Class<? extends ICapabilityProvider>> providerTypes,
List<ProviderDispatch> dispatches, LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS) {
@Override
protected ClassLoader getClassLoader() {
return CapabilityProviderDispatcherGenerator.class.getClassLoader();
}
};
String internalName = className.replace('.', '/');
// Class declaration: implements ICapabilityProvider
cw.visit(
V17,
ACC_PUBLIC | ACC_FINAL | ACC_SUPER,
className.replace('.', '/'),
internalName,
null,
"java/lang/Object",
new String[] { "net/minecraftforge/common/capabilities/ICapabilityProvider" }
);
// Generate final fields for each provider
for (int i = 0; i < providerTypes.size(); i++) {
cw.visitField(
ACC_PRIVATE | ACC_FINAL,
"provider" + i,
ICAP_PROVIDER_DESC,
null,
null
).visitEnd();
// Generate final fields for each distinct provider
LinkedHashMap<Integer, String> providerFields = collectProviderFields(dispatches);
for (var entry : providerFields.entrySet()) {
FieldVisitor fv = cw.visitField(ACC_PRIVATE | ACC_FINAL, "provider" + entry.getKey(), entry.getValue(), null, null);
if (entry.getValue().equals(ICAP_PROVIDER_DESC)) {
String originalName = providerTypes.get(entry.getKey()).getName();
AnnotationVisitor av = fv.visitAnnotation("Lorg/embeddedt/modernfix/forge/capability/OriginalType;", false);
av.visit("value", originalName);
av.visitEnd();
}
fv.visitEnd();
}
// Generate map fields for Hash dispatches
for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Hash hash) {
cw.visitField(ACC_PRIVATE | ACC_FINAL, "capMap" + hash.mapIndex(), MAP_DESC, MAP_SIGNATURE, null).visitEnd();
}
}
// Generate one static final field per unique CapabilityRef.
// These are populated in <clinit> via MethodHandles.classDataAt, which reads the
// Capability<?> instances injected by defineHiddenClassWithClassData. This avoids
// any reflection bytecode in the generated class and handles private fields transparently.
for (Map.Entry<CapabilityRef, Integer> entry : capRefIndices.entrySet()) {
cw.visitField(ACC_PRIVATE | ACC_STATIC | ACC_FINAL,
capRefFieldName(entry.getValue()), CAPABILITY_DESC, null, null).visitEnd();
}
// Generate <clinit> to load capability instances from class data
if (!capRefIndices.isEmpty()) {
generateClinit(cw, internalName, capRefIndices);
}
// Generate constructor
generateConstructor(cw, className, providerTypes.size());
generateConstructor(cw, className, providerFields, dispatches, capRefIndices);
// Generate getCapability method with sided parameter
generateGetCapabilityMethod(cw, className, providerTypes.size());
generateGetCapabilityMethod(cw, className, dispatches, capRefIndices);
cw.visitEnd();
return cw.toByteArray();
}
private static void generateConstructor(ClassWriter cw, String className, int providerCount) {
private static String capRefFieldName(int index) {
return "capRef" + index;
}
/**
* Generates {@code <clinit>} that loads each capability from class data injected at define time.
* The bytecode is simply: {@code capRefN = MethodHandles.classDataAt(lookup(), "", Capability.class, N)}.
*/
private static void generateClinit(ClassWriter cw, String internalName, LinkedHashMap<CapabilityRef, Integer> capRefIndices) {
MethodVisitor mv = cw.visitMethod(ACC_STATIC, "<clinit>", "()V", null, null);
mv.visitCode();
for (int i = 0; i < capRefIndices.size(); i++) {
// MethodHandles.lookup()
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "lookup",
"()" + LOOKUP_DESC, false);
// "_" (classDataAt requires this exact name)
mv.visitLdcInsn("_");
// Capability.class
mv.visitLdcInsn(Type.getType(CAPABILITY_DESC));
// index
mv.visitLdcInsn(i);
// MethodHandles.classDataAt(lookup, name, type, index) Object
mv.visitMethodInsn(INVOKESTATIC, "java/lang/invoke/MethodHandles", "classDataAt",
"(" + LOOKUP_DESC + "Ljava/lang/String;Ljava/lang/Class;I)Ljava/lang/Object;", false);
mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/Capability");
mv.visitFieldInsn(PUTSTATIC, internalName, capRefFieldName(i), CAPABILITY_DESC);
}
mv.visitInsn(RETURN);
mv.visitMaxs(0, 0);
mv.visitEnd();
}
/**
* Emits a load of the capability constant for {@code ref} from the generated class's own static field.
*/
private static void emitCapabilityLoad(MethodVisitor mv, String internalName, CapabilityRef ref,
Map<CapabilityRef, Integer> capRefIndices) {
mv.visitFieldInsn(GETSTATIC, internalName, capRefFieldName(capRefIndices.get(ref)), CAPABILITY_DESC);
}
private static void generateConstructor(ClassWriter cw, String className, Map<Integer, String> providerFields,
List<ProviderDispatch> dispatches, Map<CapabilityRef, Integer> capRefIndices) {
Method constructor = Method.getMethod("void <init>(net.minecraftforge.common.capabilities.ICapabilityProvider[])");
GeneratorAdapter mg = new GeneratorAdapter(ACC_PUBLIC, constructor, null, null, cw);
Type classType = Type.getObjectType(className.replace('.', '/'));
// Call super constructor
mg.loadThis();
mg.invokeConstructor(Type.getType(Object.class), Method.getMethod("void <init>()"));
// Unpack array into final fields
for (int i = 0; i < providerCount; i++) {
mg.loadThis(); // this
// Unpack array into provider fields
for (var entry : providerFields.entrySet()) {
int idx = entry.getKey();
String desc = entry.getValue();
Type fieldType = Type.getType(desc);
mg.loadThis();
mg.loadArg(0); // array
mg.push(i); // index
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC)); // array[i]
mg.putField(
Type.getObjectType(className.replace('.', '/')),
"provider" + i,
Type.getType(ICAP_PROVIDER_DESC)
);
mg.push(idx); // index
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC));
if (!desc.equals(ICAP_PROVIDER_DESC)) {
mg.checkCast(fieldType);
}
mg.putField(classType, "provider" + idx, fieldType);
}
// Build hash maps
for (ProviderDispatch dispatch : dispatches) {
if (dispatch instanceof ProviderDispatch.Hash hash) {
generateMapConstruction(mg, classType, hash, capRefIndices);
}
}
mg.returnValue();
mg.endMethod();
}
private static void generateGetCapabilityMethod(ClassWriter cw, String className, int providerCount) {
private static void generateMapConstruction(GeneratorAdapter mg, Type classType, ProviderDispatch.Hash hash,
Map<CapabilityRef, Integer> capRefIndices) {
List<ProviderDispatch.Guarded> entries = hash.entries();
mg.loadThis(); // for PUTFIELD at the end
mg.push(entries.size());
mg.visitTypeInsn(ANEWARRAY, "java/util/Map$Entry");
for (int i = 0; i < entries.size(); i++) {
ProviderDispatch.Guarded g = entries.get(i);
mg.dup();
mg.push(i);
emitCapabilityLoad(mg, classType.getInternalName(), g.capability(), capRefIndices);
mg.loadArg(0);
mg.push(g.providerIndex());
mg.arrayLoad(Type.getType(ICAP_PROVIDER_DESC));
mg.visitMethodInsn(INVOKESTATIC, "java/util/Map", "entry",
"(Ljava/lang/Object;Ljava/lang/Object;)Ljava/util/Map$Entry;", true);
mg.visitInsn(AASTORE);
}
mg.visitMethodInsn(INVOKESTATIC, "java/util/Map", "ofEntries",
"([Ljava/util/Map$Entry;)Ljava/util/Map;", true);
mg.putField(classType, "capMap" + hash.mapIndex(), Type.getType(MAP_DESC));
}
private static void generateGetCapabilityMethod(ClassWriter cw, String className, List<ProviderDispatch> dispatches,
Map<CapabilityRef, Integer> capRefIndices) {
// Method: <T> LazyOptional<T> getCapability(Capability<T>, Direction)
MethodVisitor mv = cw.visitMethod(
ACC_PUBLIC,
@ -163,61 +521,57 @@ public class CapabilityProviderDispatcherGenerator {
mv.visitCode();
// Generate unrolled dispatch loop
// For each provider, call getCapability and check if present
Label endLabel = new Label();
Label methodStart = new Label();
Label methodEnd = new Label();
mv.visitLabel(methodStart);
for (int i = 0; i < providerCount; i++) {
String internalName = className.replace('.', '/');
String getCapDesc = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC;
// slot 3 = LazyOptional<T> result (all paths)
// slot 4 = ICapabilityProvider provider (Hash paths only)
boolean usesProviderLocal = dispatches.stream().anyMatch(d -> d instanceof ProviderDispatch.Hash);
for (int di = 0; di < dispatches.size(); ) {
ProviderDispatch dispatch = dispatches.get(di);
Label nextLabel = new Label();
// LazyOptional<T> result = this.providerN.getCapability(cap, side);
mv.visitVarInsn(ALOAD, 0); // this
mv.visitFieldInsn(
GETFIELD,
className.replace('.', '/'),
"provider" + i,
ICAP_PROVIDER_DESC
);
mv.visitVarInsn(ALOAD, 1); // cap parameter
mv.visitVarInsn(ALOAD, 2); // side parameter
mv.visitMethodInsn(
INVOKEINTERFACE,
"net/minecraftforge/common/capabilities/ICapabilityProvider",
"getCapability",
"(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC,
true
);
if (dispatch instanceof ProviderDispatch.Hash hash) {
emitHashDispatch(mv, internalName, getCapDesc, hash, nextLabel);
di++;
} else if (dispatch instanceof ProviderDispatch.Guarded) {
di = emitGuardedDispatch(mv, internalName, getCapDesc, dispatches, di, nextLabel, capRefIndices);
} else {
var u = (ProviderDispatch.Unguarded) dispatch;
emitProviderGetCapability(mv, internalName, getCapDesc, u.providerIndex(), u.fieldDesc());
di++;
}
// Store result in local variable
mv.visitVarInsn(ASTORE, 3);
// If not a hash lookup, then optimistically check for the exact LazyOptional.empty() value to avoid
// an isPresent call
if (!(dispatch instanceof ProviderDispatch.Hash)) {
// if (result == EMPTY) goto next
mv.visitVarInsn(ALOAD, 3);
mv.visitFieldInsn(GETSTATIC,
"org/embeddedt/modernfix/forge/capability/CapabilityProviderDispatcherGenerator",
"EMPTY", LAZY_OPTIONAL_DESC);
mv.visitJumpInsn(IF_ACMPEQ, nextLabel);
}
// if (result == null) continue to next;
// if (result.isPresent()) return result
mv.visitVarInsn(ALOAD, 3);
mv.visitJumpInsn(IFNULL, nextLabel);
// if (result.isPresent()) return result;
mv.visitVarInsn(ALOAD, 3);
mv.visitMethodInsn(
INVOKEVIRTUAL,
mv.visitMethodInsn(INVOKEVIRTUAL,
"net/minecraftforge/common/util/LazyOptional",
"isPresent",
"()Z",
false
);
"isPresent", "()Z", false);
mv.visitJumpInsn(IFEQ, nextLabel);
// return result
mv.visitVarInsn(ALOAD, 3);
mv.visitInsn(ARETURN);
mv.visitLabel(nextLabel);
if (i < providerCount - 1) {
mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null);
}
}
// If no provider returned a capability, return empty
mv.visitLabel(endLabel);
mv.visitMethodInsn(
INVOKESTATIC,
"net/minecraftforge/common/util/LazyOptional",
@ -227,33 +581,107 @@ public class CapabilityProviderDispatcherGenerator {
);
mv.visitInsn(ARETURN);
mv.visitLabel(methodEnd);
// Local variable table for clean decompilation
String capSig = CAPABILITY_DESC.replace(";", "<TT;>;");
String resultSig = LAZY_OPTIONAL_DESC.replace(";", "<TT;>;");
mv.visitLocalVariable("cap", CAPABILITY_DESC, capSig, methodStart, methodEnd, 1);
mv.visitLocalVariable("side", DIRECTION_DESC, null, methodStart, methodEnd, 2);
mv.visitLocalVariable("result", LAZY_OPTIONAL_DESC, resultSig, methodStart, methodEnd, 3);
if (usesProviderLocal) {
mv.visitLocalVariable("provider", ICAP_PROVIDER_DESC, null, methodStart, methodEnd, 4);
}
mv.visitMaxs(0, 0); // Computed by COMPUTE_MAXS
mv.visitEnd();
}
private static void emitHashDispatch(MethodVisitor mv, String internalName, String getCapDesc,
ProviderDispatch.Hash hash, Label nextLabel) {
// ICapabilityProvider provider = (ICapabilityProvider) this.capMapN.get(cap);
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, internalName, "capMap" + hash.mapIndex(), MAP_DESC);
mv.visitVarInsn(ALOAD, 1);
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "get",
"(Ljava/lang/Object;)Ljava/lang/Object;", true);
mv.visitTypeInsn(CHECKCAST, "net/minecraftforge/common/capabilities/ICapabilityProvider");
mv.visitVarInsn(ASTORE, 4);
// if (provider == null) goto next
mv.visitVarInsn(ALOAD, 4);
mv.visitJumpInsn(IFNULL, nextLabel);
// LazyOptional<T> result = provider.getCapability(cap, side)
mv.visitVarInsn(ALOAD, 4);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEINTERFACE,
"net/minecraftforge/common/capabilities/ICapabilityProvider",
"getCapability", getCapDesc, true);
mv.visitVarInsn(ASTORE, 3);
}
/**
* Creates an instance of the optimized dispatcher for the given providers.
* Emit guarded dispatch for one or more consecutive Guarded entries sharing the same providerIndex.
* When multiple caps map to the same provider, an OR-chain guard is emitted instead of separate dispatches.
*
* @return the updated dispatch index (past the consumed group)
*/
public static ICapabilityProvider createDispatcher(ICapabilityProvider[] providers) {
try {
MethodHandle constructor = getOrGenerateConstructor(providers);
return (ICapabilityProvider) constructor.invoke(providers);
} catch (Throwable e) {
throw new RuntimeException("Failed to create capability dispatcher instance", e);
private static int emitGuardedDispatch(MethodVisitor mv, String internalName, String getCapDesc,
List<ProviderDispatch> dispatches, int di, Label nextLabel,
Map<CapabilityRef, Integer> capRefIndices) {
var guarded = (ProviderDispatch.Guarded) dispatches.get(di);
// Peek ahead to collect consecutive Guarded entries with same providerIndex
int groupEnd = di + 1;
while (groupEnd < dispatches.size()
&& dispatches.get(groupEnd) instanceof ProviderDispatch.Guarded next
&& next.providerIndex() == guarded.providerIndex()) {
groupEnd++;
}
// OR-chain: IF_ACMPEQ matchLabel for each cap except the last, IF_ACMPNE nextLabel for the last
Label matchLabel = new Label();
for (int gi = di; gi < groupEnd; gi++) {
var g = (ProviderDispatch.Guarded) dispatches.get(gi);
CapabilityRef ref = g.capability();
mv.visitVarInsn(ALOAD, 1);
emitCapabilityLoad(mv, internalName, ref, capRefIndices);
if (gi < groupEnd - 1) {
mv.visitJumpInsn(IF_ACMPEQ, matchLabel);
} else {
mv.visitJumpInsn(IF_ACMPNE, nextLabel);
}
}
mv.visitLabel(matchLabel);
emitProviderGetCapability(mv, internalName, getCapDesc, guarded.providerIndex(), guarded.fieldDesc());
return groupEnd;
}
/**
* Clears the cache of generated classes. Use with caution.
*/
public static void clearCache() {
cache.clear();
private static void emitProviderGetCapability(MethodVisitor mv, String internalName, String getCapDesc,
int providerIndex, String fieldDesc) {
mv.visitVarInsn(ALOAD, 0);
mv.visitFieldInsn(GETFIELD, internalName, "provider" + providerIndex, fieldDesc);
mv.visitVarInsn(ALOAD, 1);
mv.visitVarInsn(ALOAD, 2);
mv.visitMethodInsn(INVOKEINTERFACE,
"net/minecraftforge/common/capabilities/ICapabilityProvider",
"getCapability", getCapDesc, true);
mv.visitVarInsn(ASTORE, 3);
}
/**
* Returns the number of cached dispatcher classes.
*/
public static int getCacheSize() {
return cache.size();
private static String formatAnalysisResult(CapabilityAnalysisResult result) {
if (result instanceof CapabilityAnalysisResult.AlwaysEmpty) {
return "always empty (skipped)";
} else if (result instanceof CapabilityAnalysisResult.KnownCapabilities known) {
return "known caps: " + known.capabilities().stream()
.map(ref -> ref.owner() + "#" + ref.fieldName())
.collect(Collectors.joining(", "));
} else if (result instanceof CapabilityAnalysisResult.Indeterminate ind) {
return "indeterminate (" + ind.reason() + ")";
}
return result.toString();
}
}
}

View File

@ -0,0 +1,17 @@
package org.embeddedt.modernfix.forge.capability;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Applied to generated provider fields whose declared type has been widened to
* {@link net.minecraftforge.common.capabilities.ICapabilityProvider} because the
* concrete class is non-public or hidden. The value records the original type name.
*/
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.FIELD)
public @interface OriginalType {
String value();
}

View File

@ -0,0 +1,23 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import java.util.Set;
/**
* Result of analyzing a capability provider's {@code getCapability} bytecode.
*/
public sealed interface CapabilityAnalysisResult {
/**
* The provider can only return non-empty for these specific capabilities.
*/
record KnownCapabilities(Set<CapabilityRef> capabilities) implements CapabilityAnalysisResult {}
/**
* The provider always returns {@code LazyOptional.empty()}.
*/
record AlwaysEmpty() implements CapabilityAnalysisResult {}
/**
* Analysis could not determine the set of capabilities; fall back to unguarded dispatch.
*/
record Indeterminate(String reason) implements CapabilityAnalysisResult {}
}

View File

@ -0,0 +1,426 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import net.minecraft.core.Direction;
import net.minecraftforge.common.capabilities.Capability;
import net.minecraftforge.common.capabilities.ICapabilityProvider;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.*;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.Frame;
import org.objectweb.asm.tree.analysis.SourceValue;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/**
* Analyzes {@code getCapability} bytecode to determine which capabilities a provider handles.
*/
public class CapabilityAnalyzer {
private static final ConcurrentHashMap<Class<?>, CapabilityAnalysisResult> cache = new ConcurrentHashMap<>();
private static final String CAPABILITY_INTERNAL = "net/minecraftforge/common/capabilities/Capability";
private static final String CAPABILITY_DESC = "Lnet/minecraftforge/common/capabilities/Capability;";
private static final String LAZY_OPTIONAL_INTERNAL = "net/minecraftforge/common/util/LazyOptional";
private static final String DIRECTION_DESC = "Lnet/minecraft/core/Direction;";
private static final String LAZY_OPTIONAL_DESC = "Lnet/minecraftforge/common/util/LazyOptional;";
private static final String GET_CAPABILITY_DESC = "(" + CAPABILITY_DESC + DIRECTION_DESC + ")" + LAZY_OPTIONAL_DESC;
private static final String ICAP_PROVIDER_INTERNAL = "net/minecraftforge/common/capabilities/ICapabilityProvider";
public static CapabilityAnalysisResult analyze(Class<? extends ICapabilityProvider> clazz) {
CapabilityAnalysisResult result = cache.get(clazz);
if (result != null) return result;
if (!ModernFixMixinPlugin.instance.isOptionEnabled("perf.faster_capabilities.bytecode_analysis.CapabilityAnalyzer")) {
result = new CapabilityAnalysisResult.Indeterminate("bytecode analysis disabled");
} else {
result = doAnalyzeSafe(clazz);
}
CapabilityAnalysisResult existing = cache.putIfAbsent(clazz, result);
return existing != null ? existing : result;
}
private static CapabilityAnalysisResult doAnalyzeSafe(Class<?> clazz) {
try {
return doAnalyze(clazz);
} catch (Exception e) {
ModernFix.LOGGER.debug("Capability analysis failed for {}: {}", clazz.getName(), e.getMessage());
return new CapabilityAnalysisResult.Indeterminate("analysis exception: " + e.getMessage());
}
}
private static CapabilityAnalysisResult doAnalyze(Class<?> clazz) throws IOException, AnalyzerException {
// Find the class that actually declares getCapability via reflection
Class<?> declaringClass;
try {
declaringClass = clazz.getMethod("getCapability", Capability.class, Direction.class).getDeclaringClass();
} catch (NoSuchMethodException e) {
return new CapabilityAnalysisResult.AlwaysEmpty();
}
if (declaringClass.getName().replace('.', '/').equals(ICAP_PROVIDER_INTERNAL)) {
return new CapabilityAnalysisResult.AlwaysEmpty();
}
String declaringClassName = declaringClass.getName().replace('.', '/');
ClassNode declaringClassNode = readClass(declaringClass);
if (declaringClassNode == null) {
return new CapabilityAnalysisResult.Indeterminate("cannot read bytecode for " + declaringClass.getName());
}
MethodNode getCapMethod = findGetCapabilityMethod(declaringClassNode);
if (getCapMethod == null) {
return new CapabilityAnalysisResult.Indeterminate("method not found in bytecode for " + declaringClass.getName());
}
// Run the source analysis
CapabilitySourceInterpreter interpreter = new CapabilitySourceInterpreter();
Analyzer<SourceValue> analyzer = new Analyzer<>(interpreter);
Frame<SourceValue>[] frames = analyzer.analyze(declaringClassName, getCapMethod);
// Build if-guard map: maps instruction indices to CapabilityRef for guarded regions
List<GuardRegion> guardRegions = findGuardRegions(getCapMethod, frames);
// Classify each ARETURN
InsnList instructions = getCapMethod.instructions;
Set<CapabilityRef> knownCaps = new HashSet<>();
boolean hasIndeterminate = false;
String indeterminateReason = null;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
if (insn.getOpcode() != Opcodes.ARETURN) continue;
Frame<SourceValue> frame = frames[i];
if (frame == null) continue; // dead code
SourceValue topOfStack = frame.getStack(frame.getStackSize() - 1);
ReturnClassification classification = classifyReturnSources(
topOfStack, interpreter, i, guardRegions, clazz, instructions);
if (classification instanceof ReturnClassification.Known known) {
knownCaps.addAll(known.caps);
} else if (classification instanceof ReturnClassification.Unknown unknown) {
hasIndeterminate = true;
indeterminateReason = unknown.reason;
}
// Empty: skip
}
if (hasIndeterminate) {
CapabilityAnalysisResult result = new CapabilityAnalysisResult.Indeterminate(indeterminateReason);
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
return result;
}
if (knownCaps.isEmpty()) {
ModernFix.LOGGER.debug("Capability analysis for {}: AlwaysEmpty", clazz.getName());
return new CapabilityAnalysisResult.AlwaysEmpty();
}
CapabilityAnalysisResult result = new CapabilityAnalysisResult.KnownCapabilities(Set.copyOf(knownCaps));
ModernFix.LOGGER.debug("Capability analysis for {}: {}", clazz.getName(), result);
return result;
}
private static ReturnClassification classifyReturnSources(
SourceValue topOfStack,
CapabilitySourceInterpreter interpreter,
int returnIndex,
List<GuardRegion> guardRegions,
Class<?> originalClass,
InsnList instructions) {
Set<CapabilityRef> caps = new HashSet<>();
List<AbstractInsnNode> unknownSources = new ArrayList<>();
String unknownReason = null;
for (AbstractInsnNode source : topOfStack.insns) {
ReturnClassification sourceClassification = classifySingleSource(
source, interpreter, originalClass);
if (sourceClassification instanceof ReturnClassification.Unknown unknown) {
unknownSources.add(source);
unknownReason = unknown.reason;
} else if (sourceClassification instanceof ReturnClassification.Known known) {
caps.addAll(known.caps);
}
}
// If any source is unknown, try the guard region fallback before giving up
if (!unknownSources.isEmpty()) {
boolean allResolved = false;
// Check if the return itself is in a guard region
for (GuardRegion guard : guardRegions) {
if (returnIndex > guard.guardIndex && returnIndex < guard.targetIndex) {
caps.add(guard.capabilityRef);
allResolved = true;
break;
}
}
// Also check if each unknown source instruction is inside a guard region.
// This handles ternary patterns where both branches merge into a single
// ARETURN after the guard, but the unknown value was produced inside it.
if (!allResolved) {
allResolved = true;
for (AbstractInsnNode unknownSource : unknownSources) {
int sourceIndex = instructions.indexOf(unknownSource);
boolean resolved = false;
for (GuardRegion guard : guardRegions) {
if (sourceIndex > guard.guardIndex && sourceIndex < guard.targetIndex) {
caps.add(guard.capabilityRef);
resolved = true;
break;
}
}
if (!resolved) {
allResolved = false;
break;
}
}
}
if (!allResolved) {
return new ReturnClassification.Unknown(unknownReason);
}
}
if (caps.isEmpty()) {
return ReturnClassification.EMPTY;
}
return new ReturnClassification.Known(caps);
}
private static ReturnClassification classifySingleSource(
AbstractInsnNode source,
CapabilitySourceInterpreter interpreter,
Class<?> originalClass) {
if (source instanceof MethodInsnNode methodInsn) {
// Case: Capability.orEmpty(...)
if (methodInsn.getOpcode() == Opcodes.INVOKEVIRTUAL
&& methodInsn.owner.equals(CAPABILITY_INTERNAL)
&& methodInsn.name.equals("orEmpty")) {
return classifyOrEmptyCall(methodInsn, interpreter);
}
// Case: LazyOptional.empty()
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
&& methodInsn.name.equals("empty")) {
return ReturnClassification.EMPTY;
}
// Case: LazyOptional.of(null)
if (methodInsn.getOpcode() == Opcodes.INVOKESTATIC
&& methodInsn.owner.equals(LAZY_OPTIONAL_INTERNAL)
&& methodInsn.name.equals("of")) {
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
if (!args.isEmpty()) {
SourceValue arg = args.get(0);
if (arg.insns.size() == 1
&& arg.insns.iterator().next().getOpcode() == Opcodes.ACONST_NULL) {
return ReturnClassification.EMPTY;
}
}
}
// Case: super.getCapability(...)
if (methodInsn.getOpcode() == Opcodes.INVOKESPECIAL
&& methodInsn.name.equals("getCapability")
&& methodInsn.desc.equals(GET_CAPABILITY_DESC)) {
return classifySuperDelegation(originalClass);
}
}
if (source instanceof MethodInsnNode m) {
return new ReturnClassification.Unknown("unclassified method: " + m.owner + "." + m.name + m.desc);
}
return new ReturnClassification.Unknown("unclassified source: " + source.getClass().getSimpleName()
+ " opcode=" + source.getOpcode());
}
private static ReturnClassification classifyOrEmptyCall(
MethodInsnNode methodInsn, CapabilitySourceInterpreter interpreter) {
List<SourceValue> args = interpreter.getCallArguments(methodInsn);
if (args.isEmpty()) {
return new ReturnClassification.Unknown("orEmpty call with no recorded arguments");
}
// arg 0 is the receiver (the Capability instance)
SourceValue receiver = args.get(0);
for (AbstractInsnNode recvSource : receiver.insns) {
if (recvSource instanceof FieldInsnNode fieldInsn
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
return new ReturnClassification.Known(
Set.of(new CapabilityRef(fieldInsn.owner, fieldInsn.name)));
}
}
return new ReturnClassification.Unknown("orEmpty receiver is not a static Capability field");
}
private static ReturnClassification classifySuperDelegation(Class<?> originalClass) {
Class<?> superClass = originalClass.getSuperclass();
if (superClass == null || superClass == Object.class) {
return ReturnClassification.EMPTY;
}
@SuppressWarnings("unchecked")
Class<? extends ICapabilityProvider> superProvider =
(Class<? extends ICapabilityProvider>) superClass;
CapabilityAnalysisResult superResult = analyze(superProvider);
if (superResult instanceof CapabilityAnalysisResult.KnownCapabilities known) {
return new ReturnClassification.Known(known.capabilities());
} else if (superResult instanceof CapabilityAnalysisResult.AlwaysEmpty) {
return ReturnClassification.EMPTY;
} else if (superResult instanceof CapabilityAnalysisResult.Indeterminate ind) {
return new ReturnClassification.Unknown("super delegation: " + ind.reason());
}
return ReturnClassification.EMPTY;
}
private static List<GuardRegion> findGuardRegions(MethodNode method, Frame<SourceValue>[] frames) {
List<GuardRegion> regions = new ArrayList<>();
InsnList instructions = method.instructions;
for (int i = 0; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
int opcode = insn.getOpcode();
if (opcode != Opcodes.IF_ACMPEQ && opcode != Opcodes.IF_ACMPNE) continue;
Frame<SourceValue> frame = frames[i];
if (frame == null || frame.getStackSize() < 2) continue;
SourceValue val1 = frame.getStack(frame.getStackSize() - 2);
SourceValue val2 = frame.getStack(frame.getStackSize() - 1);
// Check if one traces to ALOAD 1 (cap parameter) and other to GETSTATIC Capability
CapabilityRef capRef = findCapRef(val1);
if (capRef == null) capRef = findCapRef(val2);
boolean hasParamLoad = isCapParamLoad(val1) || isCapParamLoad(val2);
if (capRef != null && hasParamLoad) {
JumpInsnNode jumpInsn = (JumpInsnNode) insn;
int targetIndex = instructions.indexOf(jumpInsn.label);
if (opcode == Opcodes.IF_ACMPNE) {
// if (cap != KNOWN_CAP) goto target -> the code between insn and target is for KNOWN_CAP
regions.add(new GuardRegion(capRef, i, targetIndex));
} else {
// IF_ACMPEQ: if (cap == KNOWN_CAP) goto target -> the target is the guarded code
// Find the end of the guarded region (next label or return)
// For simplicity, mark from target to the next unconditional branch or return
// TODO: that simplification may have edge cases
int endIndex = findGuardedRegionEnd(instructions, targetIndex);
regions.add(new GuardRegion(capRef, targetIndex, endIndex));
}
}
}
// Extend guard regions for forward jumps that land beyond the guard target.
// This handles compound conditions like (cap == X && cond) compiled as:
// if_acmpne L_false // guard: [here, L_false)
// evaluate cond
// ifeq L_true // forward jump beyond L_false
// L_false: empty(); areturn
// L_true: cast(); areturn // <-- also guarded by cap == X
int baseSize = regions.size();
for (int r = 0; r < baseSize; r++) {
GuardRegion guard = regions.get(r);
for (int j = guard.guardIndex + 1; j < guard.targetIndex; j++) {
AbstractInsnNode inner = instructions.get(j);
if (inner instanceof JumpInsnNode jump) {
int jumpTarget = instructions.indexOf(jump.label);
if (jumpTarget >= guard.targetIndex) {
int endIndex = findGuardedRegionEnd(instructions, jumpTarget);
regions.add(new GuardRegion(guard.capabilityRef, jumpTarget, endIndex));
}
}
}
}
return regions;
}
private static int findGuardedRegionEnd(InsnList instructions, int startIndex) {
for (int i = startIndex; i < instructions.size(); i++) {
AbstractInsnNode insn = instructions.get(i);
int opcode = insn.getOpcode();
if (opcode == Opcodes.GOTO || opcode == Opcodes.ARETURN
|| opcode == Opcodes.RETURN || opcode == Opcodes.ATHROW) {
return i + 1;
}
}
return instructions.size();
}
private static CapabilityRef findCapRef(SourceValue sv) {
CapabilityRef ref = null;
for (AbstractInsnNode src : sv.insns) {
if (src instanceof FieldInsnNode fieldInsn
&& fieldInsn.getOpcode() == Opcodes.GETSTATIC
&& fieldInsn.desc.equals(CAPABILITY_DESC)) {
if (ref == null) {
ref = new CapabilityRef(fieldInsn.owner, fieldInsn.name);
} else if (!ref.owner().equals(fieldInsn.owner) || !ref.fieldName().equals(fieldInsn.name)) {
return null; // ambiguous: multiple different capability fields
}
} else {
return null; // non-capability source
}
}
return ref;
}
private static boolean isCapParamLoad(SourceValue sv) {
if (sv.insns.isEmpty()) return false;
for (AbstractInsnNode src : sv.insns) {
if (!(src instanceof VarInsnNode varInsn)
|| varInsn.getOpcode() != Opcodes.ALOAD
|| varInsn.var != 1) {
return false;
}
}
return true;
}
private static MethodNode findGetCapabilityMethod(ClassNode classNode) {
for (MethodNode method : classNode.methods) {
if (method.name.equals("getCapability") && method.desc.equals(GET_CAPABILITY_DESC)) {
return method;
}
}
return null;
}
private static ClassNode readClass(Class<?> clazz) throws IOException {
String resourcePath = "/" + clazz.getName().replace('.', '/') + ".class";
try (InputStream is = clazz.getResourceAsStream(resourcePath)) {
if (is == null) return null;
ClassReader reader = new ClassReader(is);
ClassNode node = new ClassNode();
reader.accept(node, 0);
return node;
}
}
private sealed interface ReturnClassification {
ReturnClassification EMPTY = new Empty();
record Known(Set<CapabilityRef> caps) implements ReturnClassification {}
record Empty() implements ReturnClassification {}
record Unknown(String reason) implements ReturnClassification {}
}
private record GuardRegion(CapabilityRef capabilityRef, int guardIndex, int targetIndex) {}
}

View File

@ -0,0 +1,14 @@
package org.embeddedt.modernfix.forge.capability.analysis;
/**
* Identifies a capability by the static field that holds it.
*
* @param owner internal class name (e.g. {@code net/minecraftforge/common/capabilities/ForgeCapabilities})
* @param fieldName field name (e.g. {@code ITEM_HANDLER})
*/
public record CapabilityRef(String owner, String fieldName) {
@Override
public String toString() {
return owner + "." + fieldName;
}
}

View File

@ -0,0 +1,49 @@
package org.embeddedt.modernfix.forge.capability.analysis;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.analysis.SourceInterpreter;
import org.objectweb.asm.tree.analysis.SourceValue;
import java.util.*;
/**
* Extends {@link SourceInterpreter} with two enhancements:
* <ul>
* <li>Propagates source values through copy operations (ALOAD/ASTORE), so that
* {@code result = cap.orEmpty(...); return result;} traces back to the INVOKEVIRTUAL.</li>
* <li>Records argument {@link SourceValue}s for each call instruction, so we can later
* inspect the receiver/arguments of {@code orEmpty()} calls.</li>
* </ul>
*/
public class CapabilitySourceInterpreter extends SourceInterpreter {
private final Map<AbstractInsnNode, List<SourceValue>> callArguments = new HashMap<>();
public CapabilitySourceInterpreter() {
super(ASM9);
}
/**
* Returns the recorded argument SourceValues for a given call instruction.
*/
public List<SourceValue> getCallArguments(AbstractInsnNode insn) {
return callArguments.getOrDefault(insn, Collections.emptyList());
}
@Override
public SourceValue copyOperation(AbstractInsnNode insn, SourceValue value) {
// Propagate sources through loads/stores instead of creating a new source.
// However, if the value has no sources (e.g. a method parameter), fall back to
// the default behavior so the ALOAD instruction itself is recorded as the source.
if (value.insns.isEmpty()) {
return super.copyOperation(insn, value);
}
return value;
}
@Override
public SourceValue naryOperation(AbstractInsnNode insn, List<? extends SourceValue> values) {
callArguments.put(insn, new ArrayList<>(values));
return super.naryOperation(insn, values);
}
}

View File

@ -0,0 +1,32 @@
package org.embeddedt.modernfix.forge.classloading;
import cpw.mods.jarhandling.impl.Jar;
import net.minecraftforge.fml.loading.LoadingModList;
import java.util.HashSet;
import java.util.Set;
import java.util.jar.Attributes;
public class ManifestCompactor {
public static void compactManifests() {
for (var mfi : LoadingModList.get().getModFiles()) {
if (!(mfi.getFile().getSecureJar() instanceof Jar jar)) {
continue;
}
var manifest = jar.getManifest();
if (manifest == null) {
continue;
}
var entries = jar.getManifest().getEntries();
var entryKeys = new HashSet<>(entries.keySet());
var digests = Set.of(new Attributes.Name("SHA-256-Digest"), new Attributes.Name("SHA-384-Digest"));
entryKeys.forEach(key -> entries.compute(key, (k, attrs) -> {
if (attrs != null && attrs.keySet().stream().anyMatch(n -> n != null && !digests.contains(n))) {
return attrs;
} else {
return null;
}
}));
}
}
}

View File

@ -18,11 +18,21 @@ import java.util.concurrent.locks.LockSupport;
*/
public class NightConfigWatchThrottler {
private static final long DELAY = TimeUnit.MILLISECONDS.toNanos(1000);
// FIXED: Add shutdown hook to clean up watcher threads
private static void addShutdownHook() {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
isShuttingDown.set(true);
}, "ModernFix-ShutdownHook"));
}
@SuppressWarnings("rawtypes")
public static void throttle() {
// FIXED: Register shutdown hook for clean cleanup
addShutdownHook();
Map watchedDirs = ObfuscationReflectionHelper.getPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), "watchedDirs");
ObfuscationReflectionHelper.setPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), new ForwardingMap() {
Thread launchThread = Thread.currentThread();
Map watchedDirsWrapper = new ForwardingMap() {
@Override
protected Map delegate() {
return watchedDirs;
@ -44,13 +54,32 @@ public class NightConfigWatchThrottler {
public Iterator iterator() {
// iterator() is called at the beginning of each iteration of the watch loop,
// so it is a good spot to inject the delay.
LockSupport.parkNanos(DELAY);
if (Thread.currentThread() != launchThread) {
// FIXED: Check for shutdown state to prevent new watches from being created
if (isShuttingDown.get()) {
return java.util.Collections.emptyIterator();
}
LockSupport.parkNanos(DELAY);
// FIXED: Properly handle thread interruption to allow graceful container shutdown
if (Thread.currentThread().isInterrupted()) {
return java.util.Collections.emptyIterator();
}
}
return super.iterator();
}
};
}
return cachedValues;
}
}, "watchedDirs");
};
// Force all classes related to the iterator to be loaded ahead of time. This is necessary to prevent
// a ConcurrentModificationException from being thrown inside ModLauncher when the NightConfig file
// watcher thread loads forwarding collection classes while the main thread is still mutating the
// launch plugin map.
//noinspection StatementWithEmptyBody
for (var ignored : watchedDirsWrapper.values()) {
}
ObfuscationReflectionHelper.setPrivateValue(FileWatcher.class, FileWatcher.defaultInstance(), watchedDirsWrapper, "watchedDirs");
}
}

View File

@ -21,7 +21,17 @@ public class ModelLocationBuilder {
private record PropertyData(ImmutableList<String> nameValuePairs, int maxPairLength) {}
public void generateForBlock(Set<ResourceLocation> destinationSet, Block block, ResourceLocation baseLocation) {
var props = block.getStateDefinition().getProperties();
// Make sure to iterate over the properties in the order of the getValues() map rather than using
// StateDefinition.getProperties(), to match the logic in BlockModelShaper.statePropertiesToString.
// In vanilla, these have the same order, because the backing implementation of getValues() is a map
// that preserves insertion order. However, in some versions of FerriteCore, getValues() may not
// preserve insertion order, but instead rely on hash order of the keys. This results in BlockModelShape
// and ModelLocationBuilder producing different MRLs. Using the keySet produces the same ordering as
// BlockModelShaper, provided that all states were built with the keys inserted in the same order into the same
// map implementation (which should always be true in practice).
// The above issue only seems to affect versions of FerriteCore after the switch to fastutil maps, but it
// is harmless to be consistent on older versions too, especially if another mod backports the fastutil change.
var props = block.defaultBlockState().getValues().keySet();
List<ImmutableList<String>> optionsList = new ArrayList<>(props.size());
int requiredBuilderSize = Math.max(0, props.size() - 1); // commas
for (var prop : props) {

View File

@ -5,6 +5,8 @@ import com.mojang.brigadier.builder.LiteralArgumentBuilder;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.chunk.ChunkStatus;
import net.minecraftforge.api.distmarker.Dist;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.event.RegisterCommandsEvent;
@ -24,6 +26,7 @@ import net.minecraftforge.registries.RegisterEvent;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.embeddedt.modernfix.ModernFix;
import org.embeddedt.modernfix.benchmark.WorldgenBenchmark;
import org.embeddedt.modernfix.core.ModernFixMixinPlugin;
import org.embeddedt.modernfix.forge.ModernFixConfig;
import org.embeddedt.modernfix.forge.config.ConfigFixer;
@ -38,6 +41,7 @@ import java.util.List;
public class ModernFixForge {
private static ModernFix commonMod;
public static boolean launchDone = false;
public static boolean registryEventsFired = false;
public ModernFixForge() {
commonMod = new ModernFix();
@ -124,6 +128,8 @@ public class ModernFixForge {
});
}
ObjectHolderClearer.clearThrowables();
event.enqueueWork(ObjectHolderClearer::removeRedundantHolders);
event.enqueueWork(ModernFix::runAuditIfRequested);
}
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onServerDead(ServerStoppedEvent event) {
@ -132,5 +138,12 @@ public class ModernFixForge {
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onServerStarted(ServerStartedEvent event) {
commonMod.onServerStarted();
if (Boolean.getBoolean("modernfix.runWorldgenBenchmark")) {
int iterations = Integer.getInteger("modernfix.worldgenIterations", 15);
int testRadius = Integer.getInteger("modernfix.worldgenTestRadius", 10);
var level = event.getServer().overworld();
ModernFix.LOGGER.info("Worldgen results: {}", WorldgenBenchmark.run(level, new ChunkPos(0, 0), testRadius, iterations,
ChunkStatus.SURFACE, ChunkStatus.SURFACE));
}
}
}

View File

@ -11,28 +11,15 @@ public class IngredientItemStacksSoftReference extends SoftReference<ItemStack[]
private final Ingredient ingredient;
private static final ReferenceQueue<ItemStack[]> QUEUE = new ReferenceQueue<>();
private static final Thread DISCARD_THREAD = new Thread(IngredientItemStacksSoftReference::clearReferences, "Ingredient reference clearing thread");
static {
DISCARD_THREAD.setPriority(Thread.NORM_PRIORITY + 2);
DISCARD_THREAD.setDaemon(true);
DISCARD_THREAD.start();
}
public IngredientItemStacksSoftReference(Ingredient ingredient, ItemStack[] stacks) {
super(stacks, QUEUE);
this.ingredient = ingredient;
}
private static void clearReferences() {
while (true) {
Reference<? extends ItemStack[]> ref;
try {
ref = QUEUE.remove();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
public static void clearReferences() {
Reference<? extends ItemStack[]> ref;
while ((ref = QUEUE.poll()) != null) {
if (ref instanceof IngredientItemStacksSoftReference ingRef && ingRef.ingredient instanceof ExtendedIngredient extIng) {
// Null out the reference to the SoftReference object, to allow the SoftReference itself to be garbage collected.
extIng.mfix$clearReference();

View File

@ -2,11 +2,14 @@ package org.embeddedt.modernfix.forge.registry;
import net.minecraft.resources.ResourceLocation;
import net.minecraftforge.fml.util.ObfuscationReflectionHelper;
import net.minecraftforge.registries.ForgeRegistry;
import net.minecraftforge.registries.ObjectHolderRegistry;
import org.embeddedt.modernfix.ModernFix;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Predicate;
@ -43,4 +46,47 @@ public class ObjectHolderClearer {
ModernFix.LOGGER.debug("Cleared " + numCleared + " object holder stacktrace references");
}
}
public static void removeRedundantHolders() {
try {
Field holdersField = ObjectHolderRegistry.class.getDeclaredField("objectHolders");
holdersField.setAccessible(true);
Set<Consumer<Predicate<ResourceLocation>>> holders = (Set<Consumer<Predicate<ResourceLocation>>>)holdersField.get(null);
Class<?> refClass = Class.forName("net.minecraftforge.registries.ObjectHolderRef");
Field registryField = refClass.getDeclaredField("registry");
registryField.setAccessible(true);
Field injectedObjectField = refClass.getDeclaredField("injectedObject");
injectedObjectField.setAccessible(true);
Method getOverrideOwnersMethod = ForgeRegistry.class.getDeclaredMethod("getOverrideOwners");
getOverrideOwnersMethod.setAccessible(true);
HashMap<ForgeRegistry<?>, Map<ResourceLocation, String>> overrideCache = new HashMap<>();
int removed = 0;
var it = holders.iterator();
while (it.hasNext()) {
var holder = it.next();
if (!refClass.isInstance(holder))
continue;
ForgeRegistry<?> registry = (ForgeRegistry<?>)registryField.get(holder);
ResourceLocation injectedObject = (ResourceLocation)injectedObjectField.get(holder);
Map<ResourceLocation, String> overrides = overrideCache.computeIfAbsent(registry, r -> {
try {
return (Map<ResourceLocation, String>)getOverrideOwnersMethod.invoke(r);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
});
if (!overrides.containsKey(injectedObject)) {
it.remove();
removed++;
}
}
ModernFix.LOGGER.debug("Removed {} redundant object holders", removed);
} catch (Exception e) {
ModernFix.LOGGER.error("Failed to remove object holders", e);
}
}
}

View File

@ -0,0 +1,99 @@
package org.embeddedt.modernfix.render.font;
import com.mojang.blaze3d.font.GlyphInfo;
import com.mojang.blaze3d.font.GlyphProvider;
import com.mojang.datafixers.util.Either;
import com.mojang.serialization.MapCodec;
import it.unimi.dsi.fastutil.ints.IntSet;
import net.minecraft.client.gui.font.providers.GlyphProviderDefinition;
import net.minecraft.client.gui.font.providers.GlyphProviderType;
import net.minecraft.server.packs.resources.ResourceManager;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.function.Function;
public class LazyGlyphProvider implements GlyphProvider {
private final GlyphProviderDefinition.Loader loader;
private final ResourceManager manager;
private SoftReference<GlyphProvider> innerProvider = new SoftReference<>(null);
LazyGlyphProvider(GlyphProviderDefinition.Loader loader, ResourceManager manager) {
this.loader = loader;
this.manager = manager;
}
@Override
public void close() {
// best effort
var prov = innerProvider.get();
if (prov != null) {
prov.close();
}
}
private synchronized @Nullable GlyphProvider getGlyphProvider() {
GlyphProvider prov = innerProvider.get();
if (prov == null) {
try {
prov = this.loader.load(this.manager);
} catch (IOException e) {
return null;
}
innerProvider = new SoftReference<>(prov);
}
return prov;
}
@Override
public @Nullable GlyphInfo getGlyph(int character) {
var prov = getGlyphProvider();
if (prov != null) {
return prov.getGlyph(character);
} else {
return null;
}
}
@Override
public IntSet getSupportedGlyphs() {
var prov = getGlyphProvider();
if (prov != null) {
return prov.getSupportedGlyphs();
} else {
return IntSet.of();
}
}
private static class Definition implements GlyphProviderDefinition {
private final GlyphProviderDefinition delegate;
public Definition(GlyphProviderDefinition delegate) {
this.delegate = delegate;
}
@Override
public GlyphProviderType type() {
return this.delegate.type();
}
@Override
public Either<Loader, Reference> unpack() {
return this.delegate.unpack().mapBoth(
loader -> resourceManager -> new LazyGlyphProvider(loader, resourceManager),
Function.identity()
);
}
@SuppressWarnings("unchecked")
public <T extends GlyphProviderDefinition> T delegate() {
return (T)this.delegate;
}
}
public static MapCodec<? extends GlyphProviderDefinition> wrap(MapCodec<? extends GlyphProviderDefinition> codec) {
return codec.xmap(Definition::new, Definition::delegate);
}
}

View File

@ -0,0 +1,393 @@
package org.embeddedt.modernfix.resources;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.packs.PackResources;
import net.minecraft.server.packs.PackType;
import net.minecraft.server.packs.resources.IoSupplier;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.*;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
/**
* An index over a zip file's central directory that allows efficient namespace listing
* and resource enumeration without iterating all entries on every call.
*
* <p>The index is built once at construction time by memory-mapping the zip's central
* directory and parsing it into a {@link DirNode} tree. All subsequent queries run in
* O(depth + k) time where k is the number of matching results.
*
* <p>The caller is responsible for opening and closing the {@link ZipFile}; this class
* only holds a read-only view of the zip's metadata via a mmap'd buffer.
*/
public class ZipPackIndex {
// -------------------------------------------------------------------------
// Zip structural constants (identical to EfficientZipFileSystem in blacksmith)
// -------------------------------------------------------------------------
private static final int EOCD_SIGNATURE = 0x06054b50;
private static final int EOCD_SIZE = 22;
private static final int EOCD_OFF_CD_SIZE = 12;
private static final int EOCD_OFF_CD_OFFSET = 16;
private static final int EOCD_MAX_COMMENT_LENGTH = 65535;
private static final int CD_ENTRY_SIGNATURE = 0x02014b50;
private static final int CD_ENTRY_HEADER_SIZE = 46;
private static final int CD_OFF_FILENAME_LENGTH = 28;
private static final int CD_OFF_EXTRA_LENGTH = 30;
private static final int CD_OFF_COMMENT_LENGTH = 32;
private static final IntList EMPTY_OFFSETS = IntList.of();
// -------------------------------------------------------------------------
// DirNode
// -------------------------------------------------------------------------
static final class DirNode {
Map<String, DirNode> childDirs;
IntList fileChildOffsets; // offsets into cdBuffer for each direct file child
DirNode() {
childDirs = new Object2ObjectOpenHashMap<>();
fileChildOffsets = EMPTY_OFFSETS;
}
void freeze() {
if (fileChildOffsets instanceof IntArrayList arrayList) {
arrayList.trim();
}
childDirs = childDirs.isEmpty() ? Map.of() : Map.copyOf(childDirs);
for (DirNode child : childDirs.values()) {
child.freeze();
}
}
}
// -------------------------------------------------------------------------
// Fields
// -------------------------------------------------------------------------
/** Central directory buffer (memory-mapped or heap-allocated fallback). May be null for empty/invalid zips. */
private final ByteBuffer cdBuffer;
/** Top-level directories tracked by the index. */
private final Set<String> trackedTopLevelDirs;
/** Root of the directory tree, always non-null (may be empty but frozen). */
private final DirNode root;
// -------------------------------------------------------------------------
// Construction
// -------------------------------------------------------------------------
/**
* Build an index from the zip at the given path. Does not open a {@link ZipFile}
* and does not keep a reference to one; the caller owns all {@link ZipFile} lifecycle.
*
* @throws IOException if the file cannot be read or its central directory cannot be parsed
*/
public ZipPackIndex(Path zipPath) throws IOException {
this.cdBuffer = readCentralDirectory(zipPath);
// Computed here (not statically) so that any loader-injected PackType values
// registered after class-load are included.
Set<String> packTypeDirs = new HashSet<>();
for (PackType type : PackType.values()) packTypeDirs.add(type.getDirectory());
this.trackedTopLevelDirs = Set.copyOf(packTypeDirs);
this.root = buildTree();
}
private static SeekableByteChannel obtainChannel(Path filePath) throws IOException {
try {
return FileChannel.open(filePath, StandardOpenOption.READ);
} catch (Exception e) {
return Files.newByteChannel(filePath);
}
}
private static ByteBuffer readCentralDirectory(Path filePath) throws IOException {
try (SeekableByteChannel channel = obtainChannel(filePath)) {
long fileSize = channel.size();
if (fileSize < EOCD_SIZE) return null;
int tailSize = (int) Math.min(fileSize, (long) EOCD_SIZE + EOCD_MAX_COMMENT_LENGTH);
ByteBuffer tail = ByteBuffer.allocate(tailSize);
tail.order(ByteOrder.LITTLE_ENDIAN);
long tailStart = fileSize - tailSize;
while (tail.hasRemaining()) {
channel.position(tailStart + tail.position());
int n = channel.read(tail);
if (n < 0) {
break;
}
}
if (tail.hasRemaining()) {
throw new IOException("Failed to read ZIP tail");
}
tail.flip();
// Scan backwards for the EOCD signature and validate comment length.
int eocdPos = -1;
for (int i = tailSize - EOCD_SIZE; i >= 0; i--) {
if (tail.getInt(i) == EOCD_SIGNATURE) {
int commentLen = Short.toUnsignedInt(tail.getShort(i + 20));
if (i + EOCD_SIZE + commentLen == tailSize) {
eocdPos = i;
break;
}
}
}
if (eocdPos < 0) return null;
long cdSize = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_SIZE));
long cdOffset = Integer.toUnsignedLong(tail.getInt(eocdPos + EOCD_OFF_CD_OFFSET));
if (cdSize == 0) return null;
if (cdSize == 0xFFFFFFFFL || cdOffset == 0xFFFFFFFFL) {
throw new IOException("ZIP64 not supported by ZipPackIndex");
}
if (cdOffset > fileSize - cdSize) {
throw new IOException("Invalid central directory range");
}
// Try memory-mapping first; fall back to a heap copy if the OS refuses.
if (channel instanceof FileChannel fc) {
try {
ByteBuffer buf = fc.map(FileChannel.MapMode.READ_ONLY, cdOffset, cdSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
return buf;
} catch (Exception ignored) {
// mmap unavailable (e.g. some Linux mount flags, container restrictions);
// read the central directory into a heap buffer instead.
}
}
ByteBuffer buf = ByteBuffer.allocate((int) cdSize);
buf.order(ByteOrder.LITTLE_ENDIAN);
while (buf.hasRemaining()) {
channel.position(cdOffset + buf.position());
int n = channel.read(buf);
if (n < 0) throw new IOException("Truncated central directory during heap read");
}
buf.flip();
return buf;
}
}
private DirNode buildTree() throws IOException {
var cdBuffer = this.cdBuffer;
DirNode treeRoot = new DirNode();
if (cdBuffer == null) {
treeRoot.freeze();
return treeRoot;
}
int pos = 0;
int limit = cdBuffer.limit();
while (pos + CD_ENTRY_HEADER_SIZE <= limit) {
if (cdBuffer.getInt(pos) != CD_ENTRY_SIGNATURE) break;
pos += indexCdEntry(pos, limit, treeRoot, cdBuffer);
}
treeRoot.freeze();
return treeRoot;
}
/**
* Parses the CD entry at {@code pos}, inserts it into the tree, and returns the
* number of bytes to advance {@code pos} (i.e. the full record length).
*/
private int indexCdEntry(int pos, int limit,
DirNode treeRoot,
ByteBuffer cdBuffer) throws IOException {
int fileNameLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_FILENAME_LENGTH));
int extraLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_EXTRA_LENGTH));
int commentLen = Short.toUnsignedInt(cdBuffer.getShort(pos + CD_OFF_COMMENT_LENGTH));
int recordLen = CD_ENTRY_HEADER_SIZE + fileNameLen + extraLen + commentLen;
if (pos + recordLen > limit) {
throw new IOException("Truncated central directory");
}
byte[] nameBytes = new byte[fileNameLen];
cdBuffer.get(pos + CD_ENTRY_HEADER_SIZE, nameBytes);
DirNode current = treeRoot;
boolean tracked = false;
boolean skipped = false;
int segStart = 0;
for (int i = 0; i < fileNameLen; i++) {
if (nameBytes[i] == '/') {
int segLen = i - segStart;
if (segLen > 0) {
String segment = new String(nameBytes, segStart, segLen, StandardCharsets.UTF_8);
if (!tracked) {
if (!trackedTopLevelDirs.contains(segment)) { skipped = true; break; }
tracked = true;
}
DirNode next = current.childDirs.get(segment);
//noinspection Java8MapApi
if (next == null) {
current.childDirs.put(segment, next = new DirNode());
}
current = next;
}
segStart = i + 1;
}
}
// A remaining non-empty segment after the last '/' is a file basename.
if (!skipped && tracked && segStart < fileNameLen) {
if (current.fileChildOffsets == EMPTY_OFFSETS) {
current.fileChildOffsets = new IntArrayList();
}
current.fileChildOffsets.add(pos);
}
return recordLen;
}
// -------------------------------------------------------------------------
// CD buffer reads absolute-position gets are thread-safe on Java 13+
// -------------------------------------------------------------------------
/**
* Extract the basename (the portion after the last '/') of the entry whose
* central-directory record starts at {@code cdOffset}.
*/
String readBasename(int cdOffset) {
int nameLen = Short.toUnsignedInt(cdBuffer.getShort(cdOffset + CD_OFF_FILENAME_LENGTH));
byte[] nameBytes = new byte[nameLen];
cdBuffer.get(cdOffset + CD_ENTRY_HEADER_SIZE, nameBytes);
int lastSlash = -1;
for (int i = nameBytes.length - 1; i >= 0; i--) {
if (nameBytes[i] == '/') { lastSlash = i; break; }
}
return new String(nameBytes, lastSlash + 1, nameLen - lastSlash - 1, StandardCharsets.UTF_8);
}
// -------------------------------------------------------------------------
// Public API
// -------------------------------------------------------------------------
public Set<String> getTrackedTopLevelDirs() {
return this.trackedTopLevelDirs;
}
/**
* Returns all namespaces present under the given pack type directory.
*
* <p>Equivalent to {@code FilePackResources.getNamespaces(type)} but reads from
* the pre-built tree rather than scanning all zip entries.
*/
public Set<String> getNamespaces(PackType type) {
DirNode typeNode = root.childDirs.get(type.getDirectory());
if (typeNode == null) return Set.of();
Set<String> result = new HashSet<>();
for (String ns : typeNode.childDirs.keySet()) {
if (ns.equals(ns.toLowerCase(Locale.ROOT))) {
result.add(ns);
}
}
return result;
}
public boolean hasResource(String... paths) {
var node = this.root;
for (int i = 0; i < paths.length - 1; i++) {
var path = paths[i];
if (path.isEmpty()) {
continue;
}
node = node.childDirs.get(path);
if (node == null) {
return false;
}
}
String basename = paths[paths.length - 1];
var offsets = node.fileChildOffsets;
for (int i = 0; i < offsets.size(); i++) {
if (basename.equals(readBasename(offsets.getInt(i)))) {
return true;
}
}
return false;
}
/**
* Enumerate all resources under {@code type/namespace/path/} and deliver them
* to {@code output}.
*
* <p>Equivalent to {@code FilePackResources.listResources(type, namespace, path, output)}
* but uses the pre-built tree for O(k) traversal instead of a full zip scan.
*
* @param zipFile the open zip file, used only to supply {@link InputStream}s on demand;
* the caller retains ownership of its lifecycle
*/
public void listResources(PackType type, String namespace, String path,
ZipFile zipFile, PackResources.ResourceOutput output) {
DirNode node = root.childDirs.get(type.getDirectory());
if (node == null) return;
node = node.childDirs.get(namespace);
if (node == null) return;
// Walk to the requested sub-path
String rlSubPath;
if (!path.isEmpty()) {
for (String segment : path.split("/")) {
if (segment.isEmpty()) continue;
node = node.childDirs.get(segment);
if (node == null) return;
}
rlSubPath = path + "/";
} else {
rlSubPath = "";
}
// entryPrefix = the part of the zip entry name before the ResourceLocation path
String entryPrefix = type.getDirectory() + "/" + namespace + "/";
collectResources(node, entryPrefix, rlSubPath, zipFile, namespace, output);
}
/**
* Recursively walk {@code node}, reconstructing zip entry names as we go and
* emitting each file to {@code output}.
*
* @param entryPrefix the constant prefix before the RL path, e.g. {@code "assets/minecraft/"}
* @param rlSubPath the RL-relative path accumulated so far, e.g. {@code "textures/block/"}
*/
private void collectResources(DirNode node, String entryPrefix, String rlSubPath,
ZipFile zipFile, String namespace,
PackResources.ResourceOutput output) {
// Emit direct file children of this node
var offsets = node.fileChildOffsets;
for (int i = 0; i < offsets.size(); i++) {
String basename = readBasename(offsets.getInt(i));
String rlPathFull = rlSubPath + basename;
ResourceLocation rl = ResourceLocation.tryBuild(namespace, rlPathFull);
if (rl != null) {
ZipEntry entry = zipFile.getEntry(entryPrefix + rlPathFull);
if (entry != null) {
output.accept(rl, IoSupplier.create(zipFile, entry));
}
}
}
// Recurse into subdirectories
for (Map.Entry<String, DirNode> child : node.childDirs.entrySet()) {
collectResources(child.getValue(), entryPrefix,
rlSubPath + child.getKey() + "/", zipFile, namespace, output);
}
}
}

View File

@ -1,43 +0,0 @@
package org.embeddedt.modernfix.util;
import org.jetbrains.annotations.NotNull;
import java.util.List;
import java.util.concurrent.AbstractExecutorService;
import java.util.concurrent.TimeUnit;
public class DirectExecutorService extends AbstractExecutorService {
private boolean isShutdown;
@Override
public void shutdown() {
isShutdown = true;
}
@NotNull
@Override
public List<Runnable> shutdownNow() {
isShutdown = true;
return List.of();
}
@Override
public boolean isShutdown() {
return isShutdown;
}
@Override
public boolean isTerminated() {
return isShutdown;
}
@Override
public boolean awaitTermination(long timeout, @NotNull TimeUnit unit) throws InterruptedException {
return true;
}
@Override
public void execute(@NotNull Runnable command) {
command.run();
}
}

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