From 8bd73e19bb5b0e68b671c1359941bba633149327 Mon Sep 17 00:00:00 2001 From: JohnBraham Date: Wed, 14 Oct 2026 11:46:14 +0100 Subject: [PATCH] Fix OreSpawn settings crash on Minecraft 1.14.4 --- .github/workflows/ci.yml | 6 +- CHANGELOG.txt | 5 + README.md | 5 +- docs/VERSIONS.md | 5 +- gradle.properties | 2 +- gradle/release/artifacts.gradle | 10 +- .../mc/orespawn/client/TextFieldWidget.java | 8 +- .../worldgen/LegacyConfigMigrator.java | 2 +- .../LegacyMineralogyProfileMigration.java | 2 +- .../client/TextFieldRemappingTest.java | 129 ++++++++++++++++++ 10 files changed, 158 insertions(+), 16 deletions(-) create mode 100644 src/test/java/zone/moddev/mc/orespawn/client/TextFieldRemappingTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 72419d81..91b7e509 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -126,9 +126,9 @@ jobs: if-no-files-found: error retention-days: 30 path: | - build/libs/OreSpawn-4.1.0.114041.jar - build/libs/OreSpawn-4.1.0.114041-sources.jar - build/libs/OreSpawn-4.1.0.114041-javadoc.jar + build/libs/OreSpawn-4.1.1.114041.jar + build/libs/OreSpawn-4.1.1.114041-sources.jar + build/libs/OreSpawn-4.1.1.114041-javadoc.jar build/release/SHA256SUMS CHANGELOG.txt diff --git a/CHANGELOG.txt b/CHANGELOG.txt index 0221e5f1..adedd8fa 100644 --- a/CHANGELOG.txt +++ b/CHANGELOG.txt @@ -1,3 +1,8 @@ +Version 4.1.1.114041 + +* Fixed a crash when opening or editing OreSpawn's settings in development runs. + The fix has also been checked in a normal Forge installation. + Version 4.1.0.114041 * Reject invalid Geome IDs instead of silently renaming them. diff --git a/README.md b/README.md index afc582c1..84dbcb95 100644 --- a/README.md +++ b/README.md @@ -17,11 +17,14 @@ End" policy used by mods such as Base Metals. This is not the unrelated mod that adds mobs and dimensions under the same name. -This branch builds target-qualified version `4.1.0.114041` for Minecraft 1.14.4 +This branch builds target-qualified version `4.1.1.114041` for Minecraft 1.14.4 and Forge. It adds material-group ore arbitration, exact biome replacements, and a directory for installed provider mods to the OreSpawn 4.0.16 base. See the [versioning policy](docs/VERSIONS.md) for the encoding and release convention. +I've fixed a crash when opening or editing OreSpawn's settings in development +runs. The fix has also been checked in a normal Forge installation. + ## What Happens When It Is Installed? OreSpawn is deliberately passive on its own. It does not replace stone, remove diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 72bcaaac..6052c92e 100644 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -52,7 +52,7 @@ Examples: | Minecraft | Loader | Target | Example full OreSpawn version | | --- | --- | ---: | --- | | 1.13.2 | Forge | `113021` | `4.0.16.113021` | -| 1.14.4 | Forge | `114041` | `4.1.0.114041` | +| 1.14.4 | Forge | `114041` | `4.1.1.114041` | | 1.20.6 | Forge | `120061` | `4.0.6.120061` | | 1.21.11 | Forge | `121111` | `4.0.6.121111` | | 26.1.2 | Forge | `2601021` | `4.0.6.2601021` | @@ -162,6 +162,9 @@ the loaded-mod directory, and exact new-terrain biome replacements. It keeps the `114041` target suffix, API major 1, and the target's existing registry identities while provider/global/world schemas advance to 5/8/7. +Version 4.1.1 fixes a crash in the settings screens during development runs. +The same fix is included in the Forge 1.14.4 and 1.15.2 releases. + This provides three useful guarantees: 1. A functional version is not used to describe two unrelated change sets. diff --git a/gradle.properties b/gradle.properties index e2e92b43..c83bf774 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ mcp_version=20190829.143755 mod_id=orespawn mod_name=MMD OreSpawn mod_license=LGPL-2.1 -mod_version=4.1.0.114041 +mod_version=4.1.1.114041 mod_group=zone.moddev.mc.orespawn mod_authors=SkyBlade1978, dshadowwolf, the MMD Team mod_description=Configurable, provider-driven terrain, ore, and deposit generation. diff --git a/gradle/release/artifacts.gradle b/gradle/release/artifacts.gradle index 4bcb4a80..4a1175da 100644 --- a/gradle/release/artifacts.gradle +++ b/gradle/release/artifacts.gradle @@ -91,7 +91,7 @@ def preparedReleaseDir = providers.gradleProperty('preparedReleaseDir') tasks.register('verifyReleaseConfiguration') { group = 'verification' doLast { - if (project.mod_version != '4.1.0.114041' + if (project.mod_version != '4.1.1.114041' || project.mod_group != expectedMavenGroup || project.minecraft_version != '1.14.4' || project.forge_version != '28.2.26' @@ -105,9 +105,9 @@ tasks.register('verifyReleaseConfiguration') { throw new GradleException('Unexpected dispatcher or Java target metadata') } List expectedPublicArtifacts = [ - 'OreSpawn-4.1.0.114041.jar', - 'OreSpawn-4.1.0.114041-sources.jar', - 'OreSpawn-4.1.0.114041-javadoc.jar' + 'OreSpawn-4.1.1.114041.jar', + 'OreSpawn-4.1.1.114041-sources.jar', + 'OreSpawn-4.1.1.114041-javadoc.jar' ] if (base.archivesName.get() != expectedMavenArtifact || expectedReleaseFiles.get().collect { it.toString() } != expectedPublicArtifacts) { @@ -124,7 +124,7 @@ tasks.register('verifyReleaseConfiguration') { 'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java', 'README.md', 'CHANGELOG.txt' ].each { path -> - if (!file(path).getText('UTF-8').contains('4.1.0.114041')) { + if (!file(path).getText('UTF-8').contains('4.1.1.114041')) { throw new GradleException("Release identity missing from ${path}") } } diff --git a/src/main/java/zone/moddev/mc/orespawn/client/TextFieldWidget.java b/src/main/java/zone/moddev/mc/orespawn/client/TextFieldWidget.java index d18b3833..d835b2bf 100644 --- a/src/main/java/zone/moddev/mc/orespawn/client/TextFieldWidget.java +++ b/src/main/java/zone/moddev/mc/orespawn/client/TextFieldWidget.java @@ -14,15 +14,17 @@ class TextFieldWidget extends net.minecraft.client.gui.widget.TextFieldWidget { } void setValue(String value) { - setText(value); + // Official remapping gives the native method our alias name. Keep this + // a superclass call so imported jars cannot dispatch back into this bridge. + super.setText(value); } String getValue() { - return getText(); + return super.getText(); } void setMaxLength(int length) { - setMaxStringLength(length); + super.setMaxStringLength(length); } void setBounds(int x, int y, int width, int height) { diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java index 8ffdda64..324346bc 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyConfigMigrator.java @@ -359,7 +359,7 @@ private static void writeReport(Path config, List lines) { private static void writeUpgradeReport(Path config, int imported, List detail) { List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.0.114041 Upgrade Report"); + lines.add("OreSpawn 4.1.1.114041 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Legacy OreSpawn settings were imported into the OS4 profile."); diff --git a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java index b2170071..cf918720 100644 --- a/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java +++ b/src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyMineralogyProfileMigration.java @@ -197,7 +197,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configPath, Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt"); List missing = missingBlocks(igneous, metamorphic, sedimentary); List lines = new ArrayList<>(); - lines.add("OreSpawn 4.1.0.114041 Upgrade Report"); + lines.add("OreSpawn 4.1.1.114041 Upgrade Report"); lines.add("================================"); lines.add(""); lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected."); diff --git a/src/test/java/zone/moddev/mc/orespawn/client/TextFieldRemappingTest.java b/src/test/java/zone/moddev/mc/orespawn/client/TextFieldRemappingTest.java new file mode 100644 index 00000000..cd060da2 --- /dev/null +++ b/src/test/java/zone/moddev/mc/orespawn/client/TextFieldRemappingTest.java @@ -0,0 +1,129 @@ +package zone.moddev.mc.orespawn.client; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import net.minecraft.client.gui.FontRenderer; + +/** Exercises the name collision introduced when another mod uses official mappings. */ +public class TextFieldRemappingTest { + private static final String WRAPPER = TextFieldWidget.class.getName().replace('.', '/'); + private static final String NATIVE = "net/minecraft/client/gui/widget/TextFieldWidget"; + private static final String REMAPPED_NATIVE = OfficialTextField.class.getName().replace('.', '/'); + + @ParameterizedTest + @ValueSource(strings = { "setValue", "getValue", "setMaxLength" }) + void eachOfficialNameStillDelegatesToTheNativeWidget(String method) throws Exception { + Object field = remappedField(); + if (method.equals("setValue")) { + invoke(field.getClass(), field, method, String.class, "minecraft:stone"); + assertEquals("minecraft:stone", ((OfficialTextField) field).value); + } else if (method.equals("setMaxLength")) { + invoke(field.getClass(), field, method, int.class, 1024); + assertEquals(1024, ((OfficialTextField) field).maximum); + } else { + assertEquals("", readValue(field.getClass(), field)); + } + } + + @Test + void remappedWidgetPreservesLongValuesAndNativeLimits() throws Exception { + Object field = remappedField(); + Class remapped = field.getClass(); + String value = "minecraft:stone,minecraft:granite,minecraft:diorite,minecraft:andesite"; + + invoke(remapped, field, "setMaxLength", int.class, 1024); + invoke(remapped, field, "setValue", String.class, value); + assertEquals(value, readValue(remapped, field)); + invoke(remapped, field, "setMaxLength", int.class, 12); + assertEquals(value.substring(0, 12), readValue(remapped, field)); + invoke(remapped, field, "setValue", String.class, "minecraft:granite"); + assertEquals("minecraft:gr", readValue(remapped, field)); + } + + private Object remappedField() throws Exception { + // Use the real compiled wrapper, remapping the three native MCP names as + // ForgeGradle does when importing an SRG jar into an official-mapped project. + ClassWriter writer = new ClassWriter(0); + try (InputStream input = TextFieldWidget.class.getResourceAsStream("TextFieldWidget.class")) { + new ClassReader(input).accept(new ClassVisitor(Opcodes.ASM6, writer) { + @Override + public void visit(int version, int access, String name, String signature, + String superName, String[] interfaces) { + super.visit(version, access, name, signature, REMAPPED_NATIVE, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + return new MethodVisitor(Opcodes.ASM6, + super.visitMethod(access, name, descriptor, signature, exceptions)) { + @Override + public void visitMethodInsn(int opcode, String owner, String name, + String descriptor, boolean isInterface) { + if (owner.equals(WRAPPER) || owner.equals(NATIVE)) { + if (name.equals("setText")) name = "setValue"; + if (name.equals("getText")) name = "getValue"; + if (name.equals("setMaxStringLength")) name = "setMaxLength"; + } + super.visitMethodInsn(opcode, owner.equals(NATIVE) ? REMAPPED_NATIVE : owner, + name, descriptor, isInterface); + } + }; + } + }, 0); + } + Class remapped = new ClassLoader(getClass().getClassLoader()) { + Class defineWrapper() { + byte[] bytes = writer.toByteArray(); + return defineClass(TextFieldWidget.class.getName(), bytes, 0, bytes.length); + } + }.defineWrapper(); + Constructor constructor = remapped.getDeclaredConstructor(FontRenderer.class, + int.class, int.class, int.class, int.class, String.class); + constructor.setAccessible(true); + return constructor.newInstance(null, 0, 0, 200, 20, "host_blocks"); + } + + private static void invoke(Class type, Object field, String name, Class parameter, + Object value) throws Exception { + Method method = type.getDeclaredMethod(name, parameter); + method.setAccessible(true); + method.invoke(field, value); + } + + private static Object readValue(Class type, Object field) throws Exception { + Method method = type.getDeclaredMethod("getValue"); + method.setAccessible(true); + return method.invoke(field); + } + + /** Small native-widget stand-in: only naming and virtual dispatch matter here. */ + public static class OfficialTextField { + public int x, y, width, height; + private int maximum = 32; + private String value = ""; + + public OfficialTextField(FontRenderer font, int x, int y, int width, int height, String label) { } + public void setValue(String value) { + this.value = value.substring(0, Math.min(maximum, value.length())); + } + public String getValue() { return value; } + public void setMaxLength(int maximum) { + this.maximum = maximum; + value = value.substring(0, Math.min(maximum, value.length())); + } + } +}