Compare commits
107 Commits
5.26.1+1.2
...
1.20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
292a6aeab3 | ||
|
|
7fbfcf1a92 | ||
|
|
1bcb28a1ad | ||
|
|
d51b0f60a2 | ||
|
|
ab9880159e | ||
|
|
0f94634361 | ||
|
|
f1492cc829 | ||
|
|
0ecee529d7 | ||
|
|
e9bfd96dd9 | ||
|
|
fb9dcf77c6 | ||
|
|
33851c1cb6 | ||
|
|
494203ef5a | ||
|
|
74f76f7305 | ||
|
|
62dbbea083 | ||
|
|
538c52bc2a | ||
|
|
b62eb1845b | ||
|
|
7c45564979 | ||
|
|
f8d2425242 | ||
|
|
50cedfc699 | ||
|
|
f4f596ca0c | ||
|
|
85aab426c5 | ||
|
|
29ff5f152e | ||
|
|
8213a720a3 | ||
|
|
afe3e09a27 | ||
|
|
ae20fa17c9 | ||
|
|
a6c03e9928 | ||
|
|
864c751aea | ||
|
|
f931d5c442 | ||
|
|
55cec86e5f | ||
|
|
4ec8ef753a | ||
|
|
3f22e23565 | ||
|
|
a73dd5ef6a | ||
|
|
653a477060 | ||
|
|
44113d2536 | ||
|
|
1165d3bdd1 | ||
|
|
c73cdc49a4 | ||
|
|
4e3ecf9b6d | ||
|
|
a40363c1fb | ||
|
|
46dd5ecddd | ||
|
|
b765bcb51f | ||
|
|
26bd7116a1 | ||
|
|
4d2f0da1fc | ||
|
|
c2f585da95 | ||
|
|
327c3cd9ff | ||
|
|
c64ca2e54b | ||
|
|
85955ebf75 | ||
|
|
d749205427 | ||
|
|
438ceb1984 | ||
|
|
5acb5115b9 | ||
|
|
37dc9e60eb | ||
|
|
c2191df359 | ||
|
|
d08da1b3c8 | ||
|
|
36f425b8cd | ||
|
|
dc3c379049 | ||
|
|
4ff7d4c554 | ||
|
|
db13f39b30 | ||
|
|
5a9c49f8d4 | ||
|
|
8ee85f2c16 | ||
|
|
2081b63b56 | ||
|
|
94f1fbf4db | ||
|
|
ab8a8068e0 | ||
|
|
79d2b28d5b | ||
|
|
18dc488ab9 | ||
|
|
a9340b2642 | ||
|
|
670e06816b | ||
|
|
53349cbd1a | ||
|
|
1794c81b61 | ||
|
|
dbe9acb3d8 | ||
|
|
22915a91a1 | ||
|
|
1289897004 | ||
|
|
9692da12b4 | ||
|
|
e34a99b38c | ||
|
|
f79eae8b83 | ||
|
|
38288d5e6a | ||
|
|
2050516bf1 | ||
|
|
02f486ebf4 | ||
|
|
9edce9ad91 | ||
|
|
ac8d93d5b9 | ||
|
|
bee4536c1a | ||
|
|
da2206168b | ||
|
|
17f930ea6f | ||
|
|
f23348c6cb | ||
|
|
21cbcb0e04 | ||
|
|
925c7526ee | ||
|
|
30e3deb8e2 | ||
|
|
ee34dcf96e | ||
|
|
49d800ff27 | ||
|
|
15f30b532c | ||
|
|
df06010846 | ||
|
|
696b344ef5 | ||
|
|
e63d99763e | ||
|
|
60850610f9 | ||
|
|
e16179b797 | ||
|
|
784b914a43 | ||
|
|
b9933b1158 | ||
|
|
878b3798f3 | ||
|
|
bc0e9a09fc | ||
|
|
8c34c0de50 | ||
|
|
5a93bc6109 | ||
|
|
8125da7882 | ||
|
|
d699187006 | ||
|
|
cff29149db | ||
|
|
3926f27d33 | ||
|
|
9bc5f06a19 | ||
|
|
a70f76a34d | ||
|
|
4dcdf09a01 | ||
|
|
f26d35070e |
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
82
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
|
|
@ -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
|
||||
|
|
|
|||
102
.github/workflows/gradle.yml
vendored
102
.github/workflows/gradle.yml
vendored
|
|
@ -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:
|
||||
|
|
|
|||
41
.github/workflows/release.yml
vendored
41
.github/workflows/release.yml
vendored
|
|
@ -1,41 +0,0 @@
|
|||
name: Release ModernFix Artifacts
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- published
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: github.repository_owner == 'embeddedt'
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: 21
|
||||
check-latest: true
|
||||
- name: Remove tags for release on other versions
|
||||
run: ./scripts/tagcleaner.sh
|
||||
- name: Build and publish mod to CurseForge & Modrinth
|
||||
run: ./gradlew publishMods copyJarToBin
|
||||
env:
|
||||
CURSEFORGE_TOKEN: ${{ secrets.CURSEFORGE_TOKEN }}
|
||||
MODRINTH_TOKEN: ${{ secrets.MODRINTH_TOKEN }}
|
||||
- name: Upload assets to GitHub
|
||||
uses: AButler/upload-release-assets@v3.0
|
||||
with:
|
||||
files: 'bin/*'
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Add changelog to release
|
||||
uses: irongut/EditRelease@v1.2.0
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
id: ${{ github.event.release.id }}
|
||||
replacebody: true
|
||||
files: "CHANGELOG.md"
|
||||
|
|
@ -30,7 +30,7 @@ dependencies {
|
|||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.release = 21
|
||||
options.release = 17
|
||||
}
|
||||
|
||||
shadowJar {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package org.embeddedt.modernfix.annotation;
|
||||
|
||||
public enum FeatureLevel {
|
||||
GA, BETA;
|
||||
|
||||
public boolean isAtLeast(FeatureLevel required) {
|
||||
return this.ordinal() >= required.ordinal();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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 "";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
7
buildSrc/build.gradle.kts
Normal file
7
buildSrc/build.gradle.kts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
61
buildSrc/src/main/kotlin/GitVersionSource.kt
Normal file
61
buildSrc/src/main/kotlin/GitVersionSource.kt
Normal 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
1
release_line.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
5.27
|
||||
|
|
@ -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]*)?$'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.embeddedt.modernfix.chunk;
|
||||
|
||||
import net.minecraft.world.level.chunk.Palette;
|
||||
|
||||
public interface ExtendedPalettedContainer<T> {
|
||||
Palette<T> mfix$getPalette();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 += 2π / 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package org.embeddedt.modernfix.duck;
|
||||
|
||||
public interface IBatchingCapEvent {
|
||||
void mfix$sortCaps();
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
package org.embeddedt.modernfix.duck;
|
||||
|
||||
import org.embeddedt.modernfix.world.StrongholdLocationCache;
|
||||
|
||||
public interface IServerLevel {
|
||||
StrongholdLocationCache mfix$getStrongholdCache();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
}
|
||||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,7 +22,8 @@ public class NightConfigWatchThrottler {
|
|||
@SuppressWarnings("rawtypes")
|
||||
public static void throttle() {
|
||||
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 +45,24 @@ 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) {
|
||||
LockSupport.parkNanos(DELAY);
|
||||
}
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import org.embeddedt.modernfix.ModernFix;
|
|||
import org.objectweb.asm.Type;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -65,8 +65,10 @@ public class ModFileScanDataCompactor {
|
|||
memberNames.addOrGet(a.memberName()),
|
||||
a.annotationData().entrySet().stream().collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, e -> {
|
||||
Object annValue = e.getValue();
|
||||
if (annValue instanceof List<?> list) {
|
||||
annValue = List.copyOf(list);
|
||||
if (annValue instanceof ArrayList<?> list) {
|
||||
// We cannot properly compact annValue using List.of() because there are mods that
|
||||
// (unnecessarily) rely on the list implementation being ArrayList.
|
||||
list.trimToSize();
|
||||
}
|
||||
return annValue;
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user