Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,9 +126,9 @@ jobs:
if-no-files-found: error
retention-days: 30
path: |
build/libs/OreSpawn-4.0.16.112021.jar
build/libs/OreSpawn-4.0.16.112021-sources.jar
build/libs/OreSpawn-4.0.16.112021-javadoc.jar
build/libs/OreSpawn-4.0.17.112021.jar
build/libs/OreSpawn-4.0.17.112021-sources.jar
build/libs/OreSpawn-4.0.17.112021-javadoc.jar
build/release/SHA256SUMS
CHANGELOG.txt

Expand Down
9 changes: 9 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
Version 4.0.17.112021

* Avoid a client-startup IncompatibleClassChangeError when another legacy
modpack component supplies an interface-shaped Mojang serialization Codec
ahead of OreSpawn's 1.12 compatibility adapter.
* Preserve OreSpawn's existing public Codec descriptors and ordinary Forge 14
behaviour while decoding built-in pattern settings through an internal
class/interface-neutral bridge.

Version 4.0.16.112021

* Adopt the shared 4.0.16 release identity. Forge 1.12 has neither
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ It gives mods and modpacks one place to configure ores, deposit shapes, optional
rock strata and geomes, provider-owned underground fluid deposits, biome
palettes and world materials, flat bedrock, and bounded ore retrogen.

This branch builds target-qualified version `4.0.16.112021`: the OreSpawn 4.0.16
feature set for Minecraft 1.12.2 and Forge. See the
This branch builds target-qualified version `4.0.17.112021`: the OreSpawn 4.0.17
feature set for Minecraft 1.12.2 and Forge. This release avoids a startup
linkage failure when a legacy modpack supplies an interface-shaped Mojang
serialization `Codec` ahead of OreSpawn's compatibility adapter. See the
[versioning policy](docs/VERSIONS.md) for the encoding and release convention.

Its deprecated OS3 compatibility layer imports OreSpawn 3 configuration and
Expand Down
19 changes: 15 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ minecraft {
}
register('client')
register('server') {
args '--nogui'
args 'nogui'
}
}
}
Expand Down Expand Up @@ -280,6 +280,9 @@ tasks.named('check') {
}
def configureFromForgeRun = { JavaExec process, String runTaskName ->
process.actions.clear()
if (runTaskName == 'runServer') {
process.setArgs(['nogui'])
}
process.dependsOn {
JavaExec run = tasks.getByName(runTaskName) as JavaExec
run.taskDependencies.getDependencies(run)
Expand Down Expand Up @@ -1330,7 +1333,7 @@ tasks.register('verifyReleaseConfiguration') {
description = 'Validates the target-qualified release, API, schemas, reports, and publishing identity.'

doLast {
if (project.mod_version != '4.0.16.112021'
if (project.mod_version != '4.0.17.112021'
|| project.mod_group != expectedMavenGroup) {
throw new GradleException("Unexpected OreSpawn release version: ${project.mod_version}")
}
Expand All @@ -1357,8 +1360,8 @@ tasks.register('verifyReleaseConfiguration') {
'src/main/java/zone/moddev/mc/orespawn/worldgen/LegacyOs3ProfileMigration.java',
'README.md', 'CHANGELOG.txt'
].each { path ->
if (!file(path).getText('UTF-8').contains('4.0.16.112021')) {
throw new GradleException("Authoritative release location does not contain 4.0.16.112021: ${path}")
if (!file(path).getText('UTF-8').contains('4.0.17.112021')) {
throw new GradleException("Authoritative release location does not contain 4.0.17.112021: ${path}")
}
}
if (!file('docs/API.md').getText('UTF-8').contains('orespawn@[4.0.6,5.0.0)')) {
Expand Down Expand Up @@ -1421,6 +1424,7 @@ tasks.register('verifyReleaseArtifacts') {
['src/test/', 'src/biomeIntegrationTest/', 'src/migrationIntegrationTest/',
'src/clientIntegrationTest/', 'src/os1AbiFixture/', 'src/os3AbiFixture/',
'agent-notes/', 'surfaceprobe', 'migrationprobe', 'clientprobe',
'codec-conflict/', 'LegacyApiConsumer',
'org/junit/', 'org/mockito/', 'net/bytebuddy/'].each { forbidden ->
if (candidateZip.entries().any { it.name.contains(forbidden) }) {
throw new GradleException(
Expand Down Expand Up @@ -1787,6 +1791,9 @@ tasks.register('isolateEclipseProductionRuns') {
contents = contents.replace(
'key="MC_VERSION" value="${MC_VERSION}"',
"key=\"MC_VERSION\" value=\"${minecraft_version}\"")
contents = contents.replace(
'key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="--nogui"',
'key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="nogui"')
launch.setText(contents.replace('\r\n', '\n'), 'UTF-8')
}

Expand Down Expand Up @@ -1862,6 +1869,10 @@ tasks.register('verifyEclipseProductionClasspath') {
}.files as List<File>
allGeneratedLaunches.each { launch ->
String contents = launch.getText('UTF-8')
if (contents.contains('--nogui')) {
throw new GradleException(
"${launch.name} uses invalid legacy server argument --nogui; expected bare nogui")
}
if (contents.contains('${MC_VERSION}')) {
throw new GradleException(
"${launch.name} retains ForgeGradle's unresolved MC_VERSION token")
Expand Down
8 changes: 5 additions & 3 deletions docs/VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Examples:
| Minecraft | Loader | Target | Example full OreSpawn version |
| --- | --- | ---: | --- |
| 1.10.2 | Forge | `110021` | `4.0.6.110021` |
| 1.12.2 | Forge | `112021` | `4.0.16.112021` |
| 1.12.2 | Forge | `112021` | `4.0.17.112021` |
| 1.13.2 | Forge | `113021` | `4.0.6.113021` |
| 1.20.6 | Forge | `120061` | `4.0.6.120061` |
| 1.21.11 | Forge | `121111` | `4.0.6.121111` |
Expand Down Expand Up @@ -154,8 +154,10 @@ to 4.0.13 for provider biome-filter parity and namespaced geome support, then to
conversion. Forge 1.12 has no Y-sensitive biome-cell attribution or server-side
GameTest harness, so 4.0.15 and the GameTest lifecycle portion of 4.0.16 are not
applicable; it adopts the shared 4.0.16 identity while retaining ordinary
benchmark auto-stop. A branch may therefore legitimately skip functional version
numbers.
benchmark auto-stop. Forge 1.12 then advances to 4.0.17 to tolerate legacy
modpacks that supply an interface-shaped Mojang serialization `Codec` before
OreSpawn's class-shaped compatibility adapter. A branch may therefore
legitimately skip functional version numbers.

This provides three useful guarantees:

Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ curseforge_project_id=245586
mod_id=orespawn
mod_name=MMD OreSpawn
mod_license=LGPL-2.1
mod_version=4.0.16.112021
mod_version=4.0.17.112021
mod_group=zone.moddev.mc.orespawn
mod_authors=SkyBlade1978, dshadowwolf, the MMD Team
mod_description=Configurable, provider-driven terrain, ore, and deposit generation.
Original file line number Diff line number Diff line change
Expand Up @@ -818,7 +818,7 @@ private static void writeHumanUpgradeReport(Path destination) throws IOException
}
}
List<String> lines = new ArrayList<>();
lines.add("OreSpawn 4.0.16.112021 Upgrade Report");
lines.add("OreSpawn 4.0.17.112021 Upgrade Report");
lines.add("================================");
lines.add("");
lines.add("RESULT: Legacy OreSpawn configuration was consumed and translated for OS4.");
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/zone/moddev/mc/orespawn/OreSpawn.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public class OreSpawn {

public static final String MODID = "orespawn";
public static final String NAME = "OreSpawn";
public static final String VERSION = "4.0.16.112021";
public static final String VERSION = "4.0.17.112021";

private static final Logger LOGGER = LogManager.getLogger();

Expand Down
171 changes: 171 additions & 0 deletions src/main/java/zone/moddev/mc/orespawn/api/LegacyCodecBridge.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
package zone.moddev.mc.orespawn.api;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Proxy;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;

import com.google.gson.JsonElement;
import com.mojang.serialization.Codec;

/**
* Keeps the legacy public Codec descriptor while avoiding class-versus-interface
* invocation bytecode in OreSpawn's own startup and profile decoding paths.
*/
final class LegacyCodecBridge {
private static final Map<Object, Function<JsonElement, ?>> DECODERS = new IdentityHashMap<>();

private LegacyCodecBridge() {
}

@SuppressWarnings("unchecked")
static <A> Codec<A> create(Function<JsonElement, A> decoder) {
if (decoder == null) throw new NullPointerException("decoder");
Class<?> codecType = Codec.class;
Object codec;
try {
if (codecType.isInterface()) {
codec = Proxy.newProxyInstance(codecType.getClassLoader(), new Class<?>[] { codecType },
new CodecInvocationHandler<>(decoder));
} else {
Method factory = codecType.getMethod("of", Function.class);
codec = factory.invoke(null, decoder);
}
} catch (ReflectiveOperationException exception) {
throw linkageFailure("create the legacy ore-pattern codec", codecType, exception);
}
synchronized (DECODERS) {
DECODERS.put(codec, decoder);
}
return (Codec<A>) codec;
}

static Object decode(Object codec, JsonElement input) {
Function<JsonElement, ?> decoder;
synchronized (DECODERS) {
decoder = DECODERS.get(codec);
}
if (decoder != null) {
return decoder.apply(input);
}

try {
ClassLoader loader = codec.getClass().getClassLoader();
Class<?> jsonOps = Class.forName("com.mojang.serialization.JsonOps", true, loader);
Object operations = jsonOps.getField("INSTANCE").get(null);
Method parse = findParse(codec.getClass(), operations, input);
Object result = parse.invoke(codec, operations, input);
return unwrapResult(result);
} catch (InvocationTargetException exception) {
Throwable cause = exception.getCause() == null ? exception : exception.getCause();
throw new IllegalArgumentException(message(cause), cause);
} catch (ReflectiveOperationException exception) {
throw linkageFailure("decode legacy ore-pattern settings", codec.getClass(), exception);
}
}

private static Method findParse(Class<?> codecType, Object operations, JsonElement input)
throws NoSuchMethodException {
for (Method method : codecType.getMethods()) {
if (!"parse".equals(method.getName()) || method.getParameterCount() != 2) continue;
Class<?>[] parameters = method.getParameterTypes();
if (parameters[0].isInstance(operations)
&& (input == null || parameters[1].isInstance(input)
|| parameters[1] == Object.class)) {
return method;
}
}
throw new NoSuchMethodException(codecType.getName()
+ " has no JSON-compatible parse(operations, input) method");
}

private static Object unwrapResult(Object dataResult) throws ReflectiveOperationException {
if (dataResult == null) {
throw new IllegalArgumentException("Codec returned no result");
}
Object value = dataResult.getClass().getMethod("result").invoke(dataResult);
if (value instanceof Optional && ((Optional<?>) value).isPresent()) {
return ((Optional<?>) value).get();
}
Object error = dataResult.getClass().getMethod("error").invoke(dataResult);
String detail = error instanceof Optional && ((Optional<?>) error).isPresent()
? String.valueOf(((Optional<?>) error).get()) : "unknown codec error";
throw new IllegalArgumentException(detail);
}

private static IllegalStateException linkageFailure(String action, Class<?> codecType,
ReflectiveOperationException exception) {
String origin = "unknown origin";
if (codecType.getProtectionDomain() != null
&& codecType.getProtectionDomain().getCodeSource() != null) {
origin = String.valueOf(codecType.getProtectionDomain().getCodeSource().getLocation());
}
return new IllegalStateException("Cannot " + action + " using " + codecType.getName()
+ " from " + origin, exception);
}

private static String message(Throwable failure) {
String text = failure.getMessage();
return text == null || text.trim().isEmpty() ? failure.getClass().getSimpleName() : text;
}

private static final class CodecInvocationHandler<A> implements InvocationHandler {
private final Function<JsonElement, A> decoder;

private CodecInvocationHandler(Function<JsonElement, A> decoder) {
this.decoder = decoder;
}

@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
if (method.getDeclaringClass() == Object.class) {
switch (method.getName()) {
case "toString": return "OreSpawn legacy Codec bridge";
case "hashCode": return System.identityHashCode(proxy);
case "equals": return proxy == arguments[0];
default: throw new UnsupportedOperationException(method.toString());
}
}
if ("parse".equals(method.getName()) && arguments != null && arguments.length == 2
&& arguments[1] instanceof JsonElement) {
try {
return dataResult(method.getReturnType(), decoder.apply((JsonElement) arguments[1]), null);
} catch (RuntimeException exception) {
return dataResult(method.getReturnType(), null, message(exception));
}
}
throw new UnsupportedOperationException("OreSpawn's legacy Codec bridge supports JSON parse only: "
+ method);
}
}

private static Object dataResult(Class<?> resultType, Object value, String error)
throws ReflectiveOperationException {
if (error == null) {
return invokeStaticFactory(resultType, "success", value, null);
}
return invokeStaticFactory(resultType, "error", error, () -> error);
}

private static Object invokeStaticFactory(Class<?> type, String name, Object direct,
Supplier<String> supplied) throws ReflectiveOperationException {
for (Method method : type.getMethods()) {
if (!name.equals(method.getName()) || !Modifier.isStatic(method.getModifiers())
|| method.getParameterCount() != 1) continue;
Class<?> parameter = method.getParameterTypes()[0];
if (supplied != null && Supplier.class.isAssignableFrom(parameter)) {
return method.invoke(null, supplied);
}
if (direct == null || parameter.isInstance(direct) || parameter == Object.class) {
return method.invoke(null, direct);
}
}
throw new NoSuchMethodException(type.getName() + "." + name + "(value)");
}
}
15 changes: 8 additions & 7 deletions src/main/java/zone/moddev/mc/orespawn/api/OrePatternType.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@

import com.google.gson.JsonElement;
import com.mojang.serialization.Codec;
import com.mojang.serialization.DataResult;
import com.mojang.serialization.JsonOps;

import net.minecraftforge.registries.IForgeRegistryEntry;

Expand Down Expand Up @@ -35,11 +33,14 @@ public Codec<?> codec() {
}

public CompiledOrePattern decode(JsonElement configuration) {
DataResult<?> result = codec.parse(JsonOps.INSTANCE, configuration);
Object value = result.result().orElseThrow(() -> new IllegalArgumentException(
"Invalid settings for ore pattern " + getRegistryName() + ": "
+ result.error().map(Object::toString).orElse("unknown codec error")));
return compile(value);
try {
return compile(LegacyCodecBridge.decode(codec, configuration));
} catch (RuntimeException exception) {
String detail = exception.getMessage() == null
? exception.getClass().getSimpleName() : exception.getMessage();
throw new IllegalArgumentException("Invalid settings for ore pattern "
+ getRegistryName() + ": " + detail, exception);
}
}

private CompiledOrePattern compile(Object configuration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

/** Shared bounded settings understood by OreSpawn's six built-in patterns. */
public final class StandardPatternSettings {
public static final Codec<StandardPatternSettings> CODEC = Codec.of(element -> {
public static final Codec<StandardPatternSettings> CODEC = LegacyCodecBridge.create(element -> {
JsonObject json = element == null || !element.isJsonObject()
? new JsonObject() : element.getAsJsonObject();
return new StandardPatternSettings(integer(json, "spread", 8),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ private static void writeUpgradeReport(Path worldRoot, Path configDirectory,
Path report = worldRoot.resolve("serverconfig/orespawn-upgrade-report.txt");
List<String> missing = missingBlocks(igneous, metamorphic, sedimentary);
List<String> lines = new ArrayList<>();
lines.add("OreSpawn 4.0.16.112021 Upgrade Report");
lines.add("OreSpawn 4.0.17.112021 Upgrade Report");
lines.add("================================");
lines.add("");
lines.add("RESULT: Existing Mineralogy " + identity.version + " world detected.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ private static void writeInitialUpgradeReport(Path configDirectory,
boolean forceRetrogen, boolean flatBedrock, boolean retrogenBedrock,
int bedrockLayers) throws IOException {
String newline = System.lineSeparator();
String text = "OreSpawn 4.0.16.112021 Upgrade Report" + newline
String text = "OreSpawn 4.0.17.112021 Upgrade Report" + newline
+ "================================" + newline + newline
+ "RESULT: Legacy OreSpawn settings were imported into the OS4 profile." + newline
+ "- Manage vanilla ores: " + manageVanilla + newline
Expand Down
Loading
Loading