diff --git a/patches/minecraft/net/minecraft/util/datafix/DataFixers.java.patch b/patches/minecraft/net/minecraft/util/datafix/DataFixers.java.patch new file mode 100644 index 0000000000..b073a46f05 --- /dev/null +++ b/patches/minecraft/net/minecraft/util/datafix/DataFixers.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/util/datafix/DataFixers.java ++++ b/net/minecraft/util/datafix/DataFixers.java +@@ -222,7 +_,7 @@ + private static final DataFixer f_14510_ = m_14529_(); + + private static DataFixer m_14529_() { +- DataFixerBuilder datafixerbuilder = new DataFixerBuilder(SharedConstants.m_183709_().getWorldVersion()); ++ DataFixerBuilder datafixerbuilder = new net.minecraftforge.datafix.ForgeDataFixerBuilder(SharedConstants.m_183709_().getWorldVersion()); + m_14513_(datafixerbuilder); + return datafixerbuilder.build(Util.m_183988_()); + } diff --git a/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java b/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java index 5c27bd60fb..abef6930c9 100644 --- a/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java +++ b/src/main/java/net/minecraftforge/common/ForgeStatesProvider.java @@ -20,7 +20,9 @@ package net.minecraftforge.common; import net.minecraftforge.common.capabilities.CapabilityManager; +import net.minecraftforge.datafix.ForgeDataFixerEventHandler; import net.minecraftforge.event.RegistryEvent; +import net.minecraftforge.event.datafix.ConfigureDataFixSchemaEvent; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.*; import net.minecraftforge.fml.event.IModBusEvent; @@ -38,7 +40,8 @@ import java.util.function.Supplier; import java.util.stream.Stream; public class ForgeStatesProvider implements IModStateProvider { - final ModLoadingState CREATE_REGISTRIES = ModLoadingState.withTransition("CREATE_REGISTRIES", "CONSTRUCT", ModLoadingPhase.GATHER, SerialTransition.of(()-> Stream.of(IModStateTransition.EventGenerator.fromFunction(RegistryEvent.NewRegistry::new)))); + final ModLoadingState SETUP_DFU = ModLoadingState.withTransition("SETUP_DFU", "CONSTRUCT", ModLoadingPhase.GATHER, SerialTransition.forDFU()); + final ModLoadingState CREATE_REGISTRIES = ModLoadingState.withTransition("CREATE_REGISTRIES", "SETUP_DFU", ModLoadingPhase.GATHER, SerialTransition.of(()-> Stream.of(IModStateTransition.EventGenerator.fromFunction(RegistryEvent.NewRegistry::new)))); final ModLoadingState OBJECT_HOLDERS = ModLoadingState.withInline("OBJECT_HOLDERS", "CREATE_REGISTRIES", ModLoadingPhase.GATHER, ml-> ObjectHolderRegistry.findObjectHolders()); final ModLoadingState INJECT_CAPABILITIES = ModLoadingState.withInline("INJECT_CAPABILITIES", "OBJECT_HOLDERS", ModLoadingPhase.GATHER, ml-> CapabilityManager.INSTANCE.injectCapabilities(ml.getAllScanData())); final ModLoadingState CUSTOM_TAGS = ModLoadingState.withInline("CUSTOM_TAGS", "INJECT_CAPABILITIES", ModLoadingPhase.GATHER, ml-> GameData.setCustomTagTypesFromRegistries()); @@ -47,13 +50,23 @@ public class ForgeStatesProvider implements IModStateProvider { final ModLoadingState NETLOCK = ModLoadingState.withInline("NETWORK_LOCK", "FREEZE_DATA", ModLoadingPhase.COMPLETE, ml-> NetworkRegistry.lock()); @Override public List getAllStates() { - return List.of(CREATE_REGISTRIES, OBJECT_HOLDERS, INJECT_CAPABILITIES, CUSTOM_TAGS, LOAD_REGISTRIES, FREEZE, NETLOCK); + return List.of(SETUP_DFU, CREATE_REGISTRIES, OBJECT_HOLDERS, INJECT_CAPABILITIES, CUSTOM_TAGS, LOAD_REGISTRIES, FREEZE, NETLOCK); } static record SerialTransition(Supplier>> eventStream, BiFunction, CompletableFuture>> preDispatchHook, BiFunction, CompletableFuture>> postDispatchHook, BiFunction>, CompletableFuture>> finalActivityGenerator) implements IModStateTransition { public static SerialTransition of(Supplier>> eventStream) { return new SerialTransition(eventStream, (t, f)->CompletableFuture.completedFuture(Collections.emptyList()), (t, f)->CompletableFuture.completedFuture(Collections.emptyList()), (e, prev) ->prev.thenApplyAsync(Function.identity(), e)); } + + private static SerialTransition forDFU() { + return new SerialTransition<>( + ForgeDataFixerEventHandler::generateConfigureEvents, + ForgeDataFixerEventHandler::preDispatch, + ForgeDataFixerEventHandler::postDispatch, + ForgeDataFixerEventHandler::rebuildFixer + ); + } + @Override public Supplier>> eventFunctionStream() { return eventStream; diff --git a/src/main/java/net/minecraftforge/datafix/ForgeDataFixDelegate.java b/src/main/java/net/minecraftforge/datafix/ForgeDataFixDelegate.java new file mode 100644 index 0000000000..d0fd89a261 --- /dev/null +++ b/src/main/java/net/minecraftforge/datafix/ForgeDataFixDelegate.java @@ -0,0 +1,123 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.datafix; + +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.TypeRewriteRule; +import net.minecraftforge.fml.util.ObfuscationReflectionHelper; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * This delegate allows for rebuilding of the cached rule, which is needed since it is + * cached in {@link DataFix} and the recompute of schema will need to invalidate this cached + * type rewrite rule. + */ +class ForgeDataFixDelegate extends DataFix +{ + + private static final Method makeRuleMethod = ObfuscationReflectionHelper.findMethod( + DataFix.class, + "makeRule" + ); + + private final DataFix wrapped; + + @Nullable + private TypeRewriteRule wrappedRule; + + ForgeDataFixDelegate(final DataFix wrapped) + { + //We pass in dummy data to the super type. + //However we can not pass in null to the super type for the output schema, since this would cause an NPE. + //Luckily ForgeSchema has this option while preserving the version key. + //So we pass it that, for good measure. + super(new ForgeSchema(wrapped.getVersionKey(), null, null), false); + this.wrapped = wrapped; + } + + @Override + public TypeRewriteRule getRule() + { + //Check if OUR cached rule is available. + if (this.wrappedRule == null) + { + //Not available get a new one. + this.wrappedRule = rebuildRuleReflectively(); + } + + //Cached version available, return it. + return wrappedRule; + } + + /** + * Rebuilds the rule using reflection. + * This is needed since "makeRule" is protected in DataFix. + * And "getRule" returns a potentially cached version, which is explicitly not what we want, + * since that cached version might contain an older invalid schema from before a rebake. + * + * @return The type rewrite rule as defined by "makeRule" in the wrapped DataFix. + */ + private TypeRewriteRule rebuildRuleReflectively() + { + try + { + //Just invoke it via reflection. + final Object candidate = makeRuleMethod.invoke(this.wrapped); + //Check if we have a rule, this is just a security barrier to just crash hard if in the future the schema changes, + //so that this is noted during the initial porting phase. + if (candidate instanceof TypeRewriteRule typeRewriteRule) + { + return typeRewriteRule; + } + + //WTF Who put this here? Who is returning a none TypeRewriteRule from a method that should return it.! + throw new IllegalStateException("Failed to get a rewrite rule. The returned object was not a TypeRewriteRule!"); //Should never be reached. + } + catch (IllegalAccessException | InvocationTargetException e) + { + throw new IllegalStateException("Could not get or invoke makeRule() Method on data fixer!", e); + } + } + + /** + * Allows for resetting of the cached rule. + */ + public void resetRule() + { + this.wrappedRule = null; + } + + /** + * Returns the wrapped datafix originally cached rule. + * Does not rebuild it!. + * + * Is also irrelevant since we are not using this value anyway and is only needed to prevent NPEs in the super class. + * + * @return The value. + */ + @Override + protected TypeRewriteRule makeRule() + { + return wrapped.getRule(); + } +} diff --git a/src/main/java/net/minecraftforge/datafix/ForgeDataFixerBuilder.java b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerBuilder.java new file mode 100644 index 0000000000..fc04569e45 --- /dev/null +++ b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerBuilder.java @@ -0,0 +1,123 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.datafix; + +import com.google.common.collect.Lists; +import com.mojang.datafixers.DataFixUtils; +import com.mojang.datafixers.DataFixer; +import com.mojang.datafixers.DataFixerBuilder; +import com.mojang.datafixers.DataFixerUpper; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.schemas.Schema; +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.function.BiFunction; + +/** + * The purpose of this custom data fixer builder is to gain full control over the datafixer that is in play at runtime. + *

+ * This means that schemas become adaptable and that the ruleset can be dynamically rebaked to adapt to the changes mod authors make to the schemas. + *

+ * Some methods are carbon copies of the DFU counterpart, but are copied so that we can control the data. + */ +public class ForgeDataFixerBuilder extends DataFixerBuilder +{ + private static final Logger LOGGER = LogManager.getLogger(); + + private final int dataVersion; + private final List allSchemas = new ArrayList<>(); + private final Int2ObjectSortedMap schemas = new Int2ObjectAVLTreeMap<>(); + private final List globalList = Lists.newArrayList(); + private final IntSortedSet fixerVersions = new IntAVLTreeSet(); + + public ForgeDataFixerBuilder(final int dataVersion) + { + super(dataVersion); + this.dataVersion = dataVersion; + } + + @Override + public Schema addSchema(final int version, final int subVersion, final BiFunction factory) + { + final int key = DataFixUtils.makeKey(version, subVersion); + final Schema parent = schemas.isEmpty() ? null : schemas.get(getLowestSchemaSameVersion(schemas, key - 1)); + return addWrappedSchema(factory.apply(key, parent), parent); + } + + @Override + public void addSchema(final Schema schema) + { + final Schema parent = schemas.isEmpty() ? null : schemas.get(getLowestSchemaSameVersion(schemas, schema.getVersionKey() - 1)); + addWrappedSchema(schema, parent); + } + + private ForgeSchema addWrappedSchema(final Schema schema, final Schema parent) + { + final ForgeSchema wrapped = new ForgeSchema(schema.getVersionKey(), parent, schema); + schemas.put(wrapped.getVersionKey(), wrapped); + allSchemas.add(wrapped); + return wrapped; + } + + @Override + public DataFixer build(final Executor executor) + { + return new ForgeDataFixerDelegateHandler(dataVersion, schemas, allSchemas, globalList, executor, fixerVersions); + } + + /** + * CC of the original method in {@link DataFixerUpper}. Is protected so not accessible for us. + * + * @param schemas The schemas to search in. + * @param versionKey The version to find the lowest matching schema for. + * @return The version for the lowest matching schema. + */ + protected static int getLowestSchemaSameVersion(final Int2ObjectSortedMap schemas, final int versionKey) + { + if (versionKey < schemas.firstIntKey()) + { + return schemas.firstIntKey(); + } + return schemas.subMap(0, versionKey + 1).lastIntKey(); + } + + @Override + public void addFixer(final DataFix fix) + { + final int version = DataFixUtils.getVersion(fix.getVersionKey()); + + if (version > dataVersion) + { + LOGGER.warn("Ignored fix registered for version: {} as the DataVersion of the game is: {}", version, dataVersion); + return; + } + + globalList.add(new ForgeDataFixDelegate(fix)); + fixerVersions.add(fix.getVersionKey()); + } +} diff --git a/src/main/java/net/minecraftforge/datafix/ForgeDataFixerDelegateHandler.java b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerDelegateHandler.java new file mode 100644 index 0000000000..4380ef4f9d --- /dev/null +++ b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerDelegateHandler.java @@ -0,0 +1,214 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.datafix; + +import com.mojang.datafixers.DataFixUtils; +import com.mojang.datafixers.DataFixerUpper; +import com.mojang.datafixers.DataFix; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.TypeRewriteRule; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.Type; +import com.mojang.serialization.Dynamic; +import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectSortedMaps; +import it.unimi.dsi.fastutil.ints.IntAVLTreeSet; +import it.unimi.dsi.fastutil.ints.IntSortedSet; +import it.unimi.dsi.fastutil.ints.IntSortedSets; +import it.unimi.dsi.fastutil.ints.IntBidirectionalIterator; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * A delegate handler which can rebuild a data fixer on the fly. + */ +class ForgeDataFixerDelegateHandler extends DataFixerUpper +{ + private static final Logger LOGGER = LogManager.getLogger(); + + private final int dataVersion; + private final List allSchemas; + private final Int2ObjectSortedMap schemas; + private final List globalList; + private final IntSortedSet fixerVersions; + + private InnerFixer activeFixer; + + ForgeDataFixerDelegateHandler( + final int dataVersion, + final Int2ObjectSortedMap schemas, + final List allSchemas, + final List globalList, + final Executor executor, + final IntSortedSet fixerVersions + ) + { + //Supply dummy data to the super constructor + //We never invoke and method on the super and so this data does not matter. + super(Int2ObjectSortedMaps.emptyMap(), Collections.emptyList(), IntSortedSets.EMPTY_SET); + + //Store the builders input so we can rebuild it later if needed. + this.dataVersion = dataVersion; + this.allSchemas = Collections.unmodifiableList(allSchemas); + this.schemas = schemas; + this.globalList = globalList; + this.fixerVersions = fixerVersions; + + //Rebuild the setup immediately. + rebuildFixer(executor); + } + + public void rebuildFixer(final Executor executor) + { + //Reset our rules, normal vanilla rules (ours too) keep a cache of their schemas + //We can not use that and need to forcefully recompute them. + this.globalList.forEach(ForgeDataFixDelegate::resetRule); + + //All schemas are already reset via the mod event bus as this time. + //Rebuild of those is not needed. + this.activeFixer = new InnerFixer(new Int2ObjectAVLTreeMap(schemas), new ArrayList<>(globalList), new IntAVLTreeSet(fixerVersions)); + + //This section was added to make it easier to debug the rule set at runtime, since it is no longer pre-computed off thread, + //But only on the main thread, at the moment it is needed. + //This basically disables the REWRITE_CACHE prefill and fills it on-demand. + //Although it is mainly used for debugging, it also provides a good way of reducing DFU's memory footprint, when it is not needed. + //Since the following code will compute an update rule from every version to the current for every known type in the schema, + //relating to this version. By default, however this is disabled to preserve vanilla compatibility, since it does do the pre-compute + //to fill up the cache. + //To enable it start the JVM with the system property set to true, -Dforge.datafixer.disablePreCompute=true needs to be added as + //a JVM Launch argument. + if (Boolean.getBoolean("forge.datafixer.disablePreCompute")) + { + LOGGER.warn("The pre-compute of the data fixer rules is disabled, this is not recommended for normal use, but can be useful for debugging or in low-memory situations."); + return; + } + + //Get an iterator for all known versions. + final IntBidirectionalIterator iterator = activeFixer.fixerVersions().iterator(); + while (iterator.hasNext()) + { + //Grab the key. + final int versionKey = iterator.nextInt(); + + //Grab its schema. + final Schema schema = schemas.get(versionKey); + + //For each type in the schema, compute its rewrite rule for the current version. + for (final String typeName : schema.types()) + { + //Schedule the recompute off-thread + CompletableFuture.runAsync(() -> { + //Grab the type. + final Type dataType = schema.getType(() -> typeName); + + //Grab its normal rewrite rule for the given version and our current target version. + final TypeRewriteRule rule = activeFixer.getRule(DataFixUtils.getVersion(versionKey), dataVersion); + + //Optimize the rule to only apply to the given type. + dataType.rewrite(rule, DataFixerUpper.OPTIMIZATION_RULE); + }, executor).exceptionally(e -> { + //Fails. + LOGGER.error("Unable to build datafixers", e); + + //Just kill it, if this fails we have a problem anyway! + Runtime.getRuntime().exit(1); + return null; + }); + } + } + } + + @Override + public Dynamic update( + final DSL.TypeReference type, final Dynamic input, final int version, final int newVersion + ) + { + //Invoke the internal DFU engine. + return activeFixer.update( + type, input, version, newVersion + ); + } + + @Override + public Schema getSchema(final int key) + { + //Grab it from the internal DFU engine. + return activeFixer.getSchema(key); + } + + /** + * Gives access to all known schemas at the point of building of this wrapper. + * + * @return All schemas known to DFU. + */ + public List getAllSchemas() + { + return allSchemas; + } + + /** + * Represents a DFU that gives access to the protected sub methods so data can be properly extracted. + * Delegates all of its method invocations to the super DFU instance. + */ + private static final class InnerFixer extends DataFixerUpper + { + private InnerFixer(final Int2ObjectSortedMap schemas, final List globalList, final IntSortedSet fixerVersions) + { + super(schemas, globalList, fixerVersions); + } + + @Override + public Dynamic update(final DSL.TypeReference type, final Dynamic input, final int version, final int newVersion) + { + return super.update(type, input, version, newVersion); + } + + @Override + public Schema getSchema(final int key) + { + return super.getSchema(key); + } + + @Override + public Type getType(final DSL.TypeReference type, final int version) + { + return super.getType(type, version); + } + + @Override + public TypeRewriteRule getRule(final int version, final int dataVersion) + { + return super.getRule(version, dataVersion); + } + + @Override + public IntSortedSet fixerVersions() + { + return super.fixerVersions(); + } + } +} diff --git a/src/main/java/net/minecraftforge/datafix/ForgeDataFixerEventHandler.java b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerEventHandler.java new file mode 100644 index 0000000000..a330fa1118 --- /dev/null +++ b/src/main/java/net/minecraftforge/datafix/ForgeDataFixerEventHandler.java @@ -0,0 +1,190 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.datafix; + +import com.google.common.collect.Lists; +import com.mojang.datafixers.schemas.Schema; +import net.minecraft.Util; +import net.minecraft.util.datafix.DataFixers; +import net.minecraftforge.event.datafix.ConfigureDataFixSchemaEvent; +import net.minecraftforge.fml.IModStateTransition; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.stream.Stream; + +/** + * Event handler utility class for managing the events related to data fixers. + * This particular class is used to handle firing of the parallel mod loading events + * for the DFU setup. + * + * These events are fired immediately after mod construction and before the rest of the game + * loads, even before registries are fired. + * + * THIS CLASS IS FOR INTERNAL USE ONLY! + */ +public class ForgeDataFixerEventHandler +{ + private static final Logger LOGGER = LogManager.getLogger(); + + /** + * Gives access to the list of schemas registered with the data fixers. + * If we are currently running an incompatible version of DFU or somebody messed with it, + * then an empty list is returned. + * + * @return The list of all known schemas to handle. + */ + private static List getAllSchemas() + { + //Check if we are running in a compatible environment. + if (DataFixers.getDataFixer() instanceof ForgeDataFixerDelegateHandler forgeDataFixerDelegateHandler) + { + //Forge DFU wrapper is active, grab its configured schemas. + return forgeDataFixerDelegateHandler.getAllSchemas(); + } + + //Incompatible environment, return an empty list. + return Collections.emptyList(); + } + + /** + * Builder function for creating the events that are fired on start up. + * This returns one generator per schema, and keeps the schema in the order defined. + * + * The generator will always return the same multi-thread capable event reference regardless + * of the passed in mod name. + * + * @return The stream of event generators used to fire one event per mod per schema. + */ + public static Stream> generateConfigureEvents() + { + //Grab the schemas + List schemas = Lists.newArrayList(getAllSchemas()); + //Sort them based on their version key. + schemas.sort(Comparator.comparingInt(Schema::getVersionKey)); + + //Make one event per schema, and return it reference based from the event generator, + //regardless of which mod the generator is invoked for. + return schemas.stream() + .map(ConfigureDataFixSchemaEvent::new) + .map(event -> IModStateTransition.EventGenerator.fromFunction(modContainer -> event)); + } + + /** + * Invoked by the event management system to indicate that we are about the fire an event for the given generator to all mods. + * This is invoked exactly once, and only once, per generator (and as such per event, and schema). + * + * Clears out the current status of the schema by resetting its type definition maps. + * + * @param executor The executor to execute this task on. + * @param eventGenerator The event generator that is about to be executed. + * @return A future that indicates success or failure. + */ + public static CompletableFuture> preDispatch( + final Executor executor, final IModStateTransition.EventGenerator eventGenerator + ) + { + return CompletableFuture.runAsync(() -> { + //Grab the event, with a null container (is reference equals anyways for all containers). + final ConfigureDataFixSchemaEvent event = eventGenerator.apply(null); + + //Log that we are processing that particular event. + LOGGER.debug("Registering DFU Schema updates: {}", event.getSchema().getVersionKey()); + + //Check if we are a compatible schema. + if (event.getSchema() instanceof ForgeSchema forgeSchema) + { + //And reset if needed. + forgeSchema.resetSchema(); + } + }, executor).thenApply(v -> Collections.emptyList()); + } + + /** + * Invoked by the event management system to indicate that it has fired all the events for the given schema. + * This allows us to collect the data from the events and trigger the given schema to rebuild its type map. + * + * @param executor The executor to execute the task on. + * @param eventGenerator The event generator for the event (and thus the schema in question). + * @return The future that indicates success or failure. + */ + public static CompletableFuture> postDispatch( + final Executor executor, final IModStateTransition.EventGenerator eventGenerator + ) + { + return CompletableFuture.runAsync(() -> { + //Grab the event again, with a null container (is reference equals anyways for all containers). + final ConfigureDataFixSchemaEvent event = eventGenerator.apply(null); + + //Notify about the completion of the event firing. + LOGGER.debug("Registered DFU Schema updates: {}", event.getSchema().getVersionKey()); + + //Check for a compatible schema. + if (event.getSchema() instanceof ForgeSchema forgeSchema) + { + //Rebuild the schema with the mod data. + forgeSchema.rebuildSchema( + event.getEntityTypes(), + event.getBlockEntityTypes() + ); + } + }, executor).handle((v, t) -> t != null ? Collections.singletonList(t) : Collections.emptyList()); + } + + /** + * Invoked by the event management system to indicate that it completely fired all events from the event stream. + * This indicates that all the events for all the schemas have been fired and post processed. + * As such we can now safely reconfigure the DFU wrapper so that the new schema configurations are picked + * up by the system. + * + * @param executor The executor to execute the task on. + * @param listCompletableFuture The list of previous failures. + * @return The future which indicates success or failure. + */ + public static CompletableFuture> rebuildFixer( + final Executor executor, final CompletableFuture> listCompletableFuture + ) + { + return listCompletableFuture.whenCompleteAsync((errors, except) -> { + //Check for errors. + if (except != null) + { + //Ouch that failed, log it and escape. + LOGGER.fatal("Detected errors during dfu setup. DFU might not be available.", except); + } + else + { + //Success, check for a compatible DFU instance and then rebuild it. + if (DataFixers.getDataFixer() instanceof ForgeDataFixerDelegateHandler forgeDataFixerDelegateHandler) + { + //Do not do this on the mod executor! + //This will crash the game. + //Rebuild the DFU wrapper on the bootstrap executor + forgeDataFixerDelegateHandler.rebuildFixer(Util.bootstrapExecutor()); + } + } + }, executor); + } +} diff --git a/src/main/java/net/minecraftforge/datafix/ForgeSchema.java b/src/main/java/net/minecraftforge/datafix/ForgeSchema.java new file mode 100644 index 0000000000..bcf3c3b64d --- /dev/null +++ b/src/main/java/net/minecraftforge/datafix/ForgeSchema.java @@ -0,0 +1,316 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.datafix; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.DataFixUtils; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.Type; +import com.mojang.datafixers.types.families.RecursiveTypeFamily; +import com.mojang.datafixers.types.families.TypeFamily; +import com.mojang.datafixers.types.templates.RecursivePoint; +import com.mojang.datafixers.types.templates.TypeTemplate; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.Objects; +import java.util.function.Supplier; + +/** + * A partial carbon copy of the {@link Schema} class. + * But this allows us to delay the setup of the type hierarchy until we need it. + */ +class ForgeSchema extends Schema +{ + //Can not be final, needs to be initialized on the fly! + //Constructor invocation order and the what not! + private Map> TYPE_TEMPLATES = Maps.newConcurrentMap(); + private Map> TYPES = Maps.newConcurrentMap(); + + private final Map RECURSIVE_TYPES = Maps.newConcurrentMap(); + private final Map> MODDED_ENTITY_TYPES = Maps.newConcurrentMap(); + private final Map> MODDED_BLOCK_ENTITY_TYPES = Maps.newConcurrentMap(); + + //Store some data we can not access later anymore. + private final String name; + private final Schema wrapped; + + ForgeSchema(final int versionKey, final Schema parent, final Schema wrapped) + { + super(versionKey, parent); + this.wrapped = wrapped; + + final int subVersion = DataFixUtils.getSubVersion(versionKey); + name = "V" + DataFixUtils.getVersion(versionKey) + (subVersion == 0 ? "" : "." + subVersion); + + //Now with the vanilla data in the schema, lets build our first version again. + //Yes this executes the type resolve multiple times per schema (but it's not a big deal) + resetSchema(); + rebuildSchema( + Collections.emptyMap(), + Collections.emptyMap() + ); + } + + /** + * Rebuilds the types, with support for the case that no types exists. + * (Which should never happen, but you never know!) + * + * @return The type map. + */ + protected Map> buildTypes() + { + //Result map. + final Map> types = Maps.newConcurrentMap(); + + //All the core templates into which recursion of data is possible. + final List templates = Lists.newArrayList(); + + //Check all recursion types + for (final Map.Entry entry : RECURSIVE_TYPES.entrySet()) + { + //Generate a template for the type. + templates.add(DSL.check(entry.getKey(), entry.getValue(), getTemplate(entry.getKey()))); + } + + //Merge them all together into one root recursive type. + final Optional availableTypeTemplate = templates.stream().reduce(DSL::or); + if (availableTypeTemplate.isEmpty()) + { + //Check for its existence or nuke. + return Maps.newConcurrentMap(); + } + //Build a type family for our recursive types. + final TypeTemplate choice = availableTypeTemplate.get(); + final TypeFamily family = new RecursiveTypeFamily(name, choice); + + //Now process all other known types, taking care of recursion. + for (final String name : TYPE_TEMPLATES.keySet()) + { + final Type type; + final int recurseId = RECURSIVE_TYPES.getOrDefault(name, -1); + if (recurseId != -1) + { + type = family.apply(recurseId); + } + else + { + type = getTemplate(name).apply(family).apply(-1); + } + types.put(name, type); + } + return types; + } + + @Override + public Set types() + { + return TYPES.keySet(); + } + + @Override + public Type getTypeRaw(final DSL.TypeReference type) + { + final String name = type.typeName(); + return TYPES.computeIfAbsent(name, key -> { + throw new IllegalArgumentException("Unknown type: " + name); + }); + } + + @Override + public Type getType(final DSL.TypeReference type) + { + final String name = type.typeName(); + final Type type1 = TYPES.computeIfAbsent(name, key -> { + throw new IllegalArgumentException("Unknown type: " + name); + }); + if (type1 instanceof RecursivePoint.RecursivePointType) + { + return type1.findCheckedType(-1).orElseThrow(() -> new IllegalStateException("Could not find choice type in the recursive type")); + } + return type1; + } + + @Override + public TypeTemplate resolveTemplate(final String name) + { + return TYPE_TEMPLATES.getOrDefault(name, () -> { + throw new IllegalArgumentException("Unknown type: " + name); + }).get(); + } + + @Override + public TypeTemplate id(final String name) + { + final int id = RECURSIVE_TYPES.getOrDefault(name, -1); + if (id != -1) + { + return DSL.id(id); + } + return getTemplate(name); + } + + @Override + public void registerTypes( + final Schema schema, final Map> entityTypes, final Map> blockEntityTypes + ) + { + //This is a bit wonky, in vanilla it suffices to register the types to one single version + //And all newer versions will have this type. + //Due to the way we set up the schemas with events later, that is not possible anymore. + //Modders will need to register their types on all versions that they should exist in, + //not just in the oldest version. + + if (wrapped != null) + { + wrapped.registerTypes(schema, entityTypes, blockEntityTypes); + } + else if (parent != null) + { + parent.registerTypes(schema, entityTypes, blockEntityTypes); + } + } + + @Override + public Map> registerEntities(final Schema schema) + { + //This is a bit wonky, in vanilla it suffices to register the types to one single version + //And all newer versions will have this type. + //Due to the way we set up the schemas with events later, that is not possible anymore. + //Modders will need to register their types on all versions that they should exist in, + //not just in the oldest version. + + if (wrapped != null) + { + return wrapped.registerEntities(schema); + } + if (parent != null) + { + return parent.registerEntities(schema); + } + + return Maps.newHashMap(); + } + + @Override + public Map> registerBlockEntities(final Schema schema) + { + //This is a bit wonky, in vanilla it suffices to register the types to one single version + //And all newer versions will have this type. + //Due to the way we set up the schemas with events later, that is not possible anymore. + //Modders will need to register their types on all versions that they should exist in, + //not just in the oldest version. + + if (wrapped != null) + { + return wrapped.registerBlockEntities(schema); + } + if (parent != null) + { + return parent.registerBlockEntities(schema); + } + + return Maps.newHashMap(); + } + + /** + * Registers a new type to the Schema with its type template. + * + * @param recursive Indicates if the type is potentially recursive. + * @param type The type in question. + * @param template Its template. + */ + @Override + public void registerType(final boolean recursive, final DSL.TypeReference type, final Supplier template) + { + //This is invoked from the super constructor, so our field init code has not run yet. + //Set it to a none null value, so the super type can do its initial construction. + if (TYPE_TEMPLATES == null) + { + TYPE_TEMPLATES = Maps.newHashMap(); + } + + TYPE_TEMPLATES.put(type.typeName(), template); + if (recursive && !RECURSIVE_TYPES.containsKey(type.typeName())) + { + RECURSIVE_TYPES.put(type.typeName(), RECURSIVE_TYPES.size()); + } + } + + /** + * Resets the schema so that the type map can be properly rebuild. + */ + void resetSchema() + { + this.TYPE_TEMPLATES.clear(); + this.TYPES.clear(); + this.RECURSIVE_TYPES.clear(); + this.MODDED_ENTITY_TYPES.clear(); + this.MODDED_BLOCK_ENTITY_TYPES.clear(); + } + + /** + * Rebuilds the schema's type map. + * This also injects custom types for entities and block entities. + * + * @param modEntityTypes The mod entity types to inject. + * @param modBlockEntityTypes The block entity types to inject. + */ + void rebuildSchema( + final Map> modEntityTypes, + final Map> modBlockEntityTypes + ) + { + //First grab the parent's additional modded types, if the parent is compatible. + if (this.parent instanceof ForgeSchema parentForgeSchema) + { + //Compatible parent found, add its modded types. + this.MODDED_ENTITY_TYPES.putAll(parentForgeSchema.MODDED_ENTITY_TYPES); + this.MODDED_BLOCK_ENTITY_TYPES.putAll(parentForgeSchema.MODDED_BLOCK_ENTITY_TYPES); + } + + //Now add our own modded types, and potentially override the data. + this.MODDED_ENTITY_TYPES.putAll(modEntityTypes); + this.MODDED_BLOCK_ENTITY_TYPES.putAll(modBlockEntityTypes); + + //Remove those types from the map, which the event marked for removal. + this.MODDED_ENTITY_TYPES.values().removeIf(Objects::isNull); + this.MODDED_BLOCK_ENTITY_TYPES.values().removeIf(Objects::isNull); + + //Grab the vanilla entities. + final Map> entityTypes = registerEntities(this); + final Map> blockEntityTypes = registerBlockEntities(this); + + //Add all modded entities and block entities. + entityTypes.putAll(this.MODDED_ENTITY_TYPES); + blockEntityTypes.putAll(this.MODDED_BLOCK_ENTITY_TYPES); + + //Re-register them. + registerTypes(this, entityTypes, blockEntityTypes); + + //Build the type map again. + TYPES = buildTypes(); + } +} diff --git a/src/main/java/net/minecraftforge/event/datafix/ConfigureDataFixSchemaEvent.java b/src/main/java/net/minecraftforge/event/datafix/ConfigureDataFixSchemaEvent.java new file mode 100644 index 0000000000..9a9c56810a --- /dev/null +++ b/src/main/java/net/minecraftforge/event/datafix/ConfigureDataFixSchemaEvent.java @@ -0,0 +1,214 @@ +/* + * Minecraft Forge + * Copyright (c) 2016-2021. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation version 2.1 + * of the License. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +package net.minecraftforge.event.datafix; + +import com.google.common.collect.Maps; +import com.mojang.datafixers.DSL; +import com.mojang.datafixers.schemas.Schema; +import com.mojang.datafixers.types.templates.TypeTemplate; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.util.datafix.fixes.References; +import net.minecraftforge.eventbus.api.Event; +import net.minecraftforge.fml.event.IModBusEvent; + +import java.util.Collections; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * Event fired in parallel on the mod event bus to allow for the modification + * of a DFU Schemas type signature. + * + * If used to register a new entity or block entity to a particular version, + * then this also will need to be registered to all newer versions to persist properly! + * As opposed to vanilla which automatically transfers the entities and block entities from older + * schemas to newer ones, this event does not do that and the modder is on their own to implement this properly. + * + * We suggest instead of checking for version equality to check for a greater or equal version, and to unregister (or override with a newer version) + * when the type changes again in a newer version. + * + * If a DFU touches a vanilla type (like a list of block entities in a chunk), then all the possible variations of that type need to be known, + * in other words, when mojang changes the type signature of a vanilla type, all the mods that touch that type need also to register their types in that schema to persist. + * An example of this is when Mojang moved the block entity list in the chunk during the upgrade from 1.17 to 1.18, any block entity type which is + * not registered to the 1.18 schema will be lost, since the rule considers a not registered block entity type, as a type that got removed from the game. + */ +public class ConfigureDataFixSchemaEvent extends Event implements IModBusEvent +{ + + private final Schema schema; + + private final Map> entityTypes = Maps.newConcurrentMap(); + private final Map> blockEntityTypes = Maps.newConcurrentMap(); + + //Some unmodifiable views of the above maps + private final Map> entityTypesView = Collections.unmodifiableMap(entityTypes); + private final Map> blockEntityTypesView = Collections.unmodifiableMap(blockEntityTypes); + + public ConfigureDataFixSchemaEvent(final Schema schema) + { + this.schema = schema; + } + + /** + * The schema which you can configure. + * + * @return The schema + */ + public Schema getSchema() + { + return schema; + } + + /** + * The version of the schema that is about to be configured. + * + * @return The version. + */ + public int getVersion() + { + return getSchema().getVersionKey(); + } + + /** + * Registers a new simple (catch-all-nbt) block entity to the current schema and all its descendent versions. + * + * @param registryName The registry name of the block entity. + */ + public void registerSimpleBlockEntity(final ResourceLocation registryName) + { + registerBlockEntity(registryName, DSL::remainder); + } + + /** + * Registers a new simple (catch-all-nbt) block entity with an item inventory array on the nbt field with the given name to the current schema and all its descendent versions. + * + * @param registryName The registry name of the block entity. + * @param nbtKey The name of the nbt field to where the item array is located. + */ + public void registerSimpleItemInventory(final ResourceLocation registryName, final String nbtKey) + { + registerBlockEntity(registryName, () -> DSL.optionalFields(nbtKey, DSL.list(References.ITEM_STACK.in(getSchema())))); + } + + /** + * Allows for the custom definition of a new block entity type and registration of it to the current schema and all its descendent versions. + * The function is given the registry name of the block entity type template to produce. + * + * @param registryName The registry name of the block entity type. + * @param template The function to produce the block entity type template. + */ + public void registerBlockEntity(final ResourceLocation registryName, final Function template) + { + registerBlockEntity(registryName, () -> template.apply(registryName.toString())); + } + + /** + * Allows for the custom definition of a new block entity type and registration of it to the current schema and all its descendent versions. + * The supplier is invoked to produce the block entity type template. + * + * @param registryName The registry name of the block entity type. + * @param template The supplier of the type template. + */ + public void registerBlockEntity(final ResourceLocation registryName, final Supplier template) + { + blockEntityTypes.put(registryName.toString(), template); + } + + /** + * Removes an already registered block entity from the current schema and all its descendent versions. + * This allows for the removal of a custom block entity type when it is registered in a parent version and is no longer needed. + * + * @param registryName The registry name of the block entity to remove. + */ + public void removeBlockEntity(final ResourceLocation registryName) + { + blockEntityTypes.put(registryName.toString(), null); + } + + /** + * Registers a new simple (catch-all-nbt) entity to the current schema and all its descendent versions. + * + * @param registryName The registry name of the entity. + */ + public void registerSimpleEntity(final ResourceLocation registryName) + { + registerEntity(registryName, DSL::remainder); + } + + /** + * Allows for the custom definition of a new entity type and registration of it to the current schema and all its descendent versions. + * The function is given the registry name of the entity type template to produce. + * + * @param registryName The registry name of the entity type. + * @param template The function to produce the entity type template. + */ + public void registerEntity(final ResourceLocation registryName, final Function template) + { + registerEntity(registryName, () -> template.apply(registryName.toString())); + } + + /** + * Allows for the custom definition of a new entity type and registration of it to the current schema and all its descendent versions. + * The supplier is invoked to produce the entity type template. + * + * @param registryName The registry name of the entity type. + * @param template The supplier of the type template. + */ + public void registerEntity(final ResourceLocation registryName, final Supplier template) + { + entityTypes.put(registryName.toString(), template); + } + + /** + * Removes an already registered entity from the current schema and all its descendent versions. + * This allows for the removal of a custom entity type when it is registered in a parent version and is no longer needed. + * + * @param registryName The registry name of the entity to remove. + */ + public void removeEntity(final ResourceLocation registryName) + { + entityTypes.put(registryName.toString(), null); + } + + /** + * An unmodifiable map of all registered additional modded block entity types. + * Keys may be mapped to a null value if they are supposed to be removed from the schema and its descendant versions, overriding the + * type specification added in a parent version. + * + * @return The already registered additional modded block entity types. + */ + public Map> getEntityTypes() + { + return entityTypesView; + } + + /** + * An unmodifiable map of all registered additional modded entity types. + * Keys may be mapped to a null value if they are supposed to be removed from the schema and its descendant versions, overriding the + * type specification added in a parent version. + * + * @return The already registered additional modded entity types. + */ + public Map> getBlockEntityTypes() + { + return blockEntityTypesView; + } +}