Hooks to allow registering and managing custom DFU schemes and types. (#8242)

This commit is contained in:
Marc Hermans authored and GitHub committed 2022-01-20 12:20:39 -08:00
1 parent 643b972e97
commit 0ea593c50e
8 files changed
+1206 -2

No files matched your search

@@ -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<IModLoadingState> 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<T extends Event & IModBusEvent>(Supplier<Stream<EventGenerator<?>>> eventStream, BiFunction<Executor, ? extends EventGenerator<T>, CompletableFuture<List<Throwable>>> preDispatchHook, BiFunction<Executor, ? extends EventGenerator<T>, CompletableFuture<List<Throwable>>> postDispatchHook, BiFunction<Executor, CompletableFuture<List<Throwable>>, CompletableFuture<List<Throwable>>> finalActivityGenerator) implements IModStateTransition {
public static <T extends Event & IModBusEvent> SerialTransition<T> of(Supplier<Stream<EventGenerator<?>>> eventStream) {
return new SerialTransition<T>(eventStream, (t, f)->CompletableFuture.completedFuture(Collections.emptyList()), (t, f)->CompletableFuture.completedFuture(Collections.emptyList()), (e, prev) ->prev.thenApplyAsync(Function.identity(), e));
}
private static SerialTransition<ConfigureDataFixSchemaEvent> forDFU() {
return new SerialTransition<>(
ForgeDataFixerEventHandler::generateConfigureEvents,
ForgeDataFixerEventHandler::preDispatch,
ForgeDataFixerEventHandler::postDispatch,
ForgeDataFixerEventHandler::rebuildFixer
);
}
@Override
public Supplier<Stream<EventGenerator<?>>> eventFunctionStream() {
return eventStream;
@@ -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();
}
}
@@ -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.
* <p>
* 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.
* <p>
* 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<ForgeSchema> allSchemas = new ArrayList<>();
private final Int2ObjectSortedMap<ForgeSchema> schemas = new Int2ObjectAVLTreeMap<>();
private final List<ForgeDataFixDelegate> 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<Integer, Schema, Schema> 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<ForgeSchema> 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());
}
}
@@ -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<ForgeSchema> allSchemas;
private final Int2ObjectSortedMap<ForgeSchema> schemas;
private final List<ForgeDataFixDelegate> globalList;
private final IntSortedSet fixerVersions;
private InnerFixer activeFixer;
ForgeDataFixerDelegateHandler(
final int dataVersion,
final Int2ObjectSortedMap<ForgeSchema> schemas,
final List<ForgeSchema> allSchemas,
final List<ForgeDataFixDelegate> 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<Schema>(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 <T> Dynamic<T> update(
final DSL.TypeReference type, final Dynamic<T> 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<ForgeSchema> 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<Schema> schemas, final List<DataFix> globalList, final IntSortedSet fixerVersions)
{
super(schemas, globalList, fixerVersions);
}
@Override
public <T> Dynamic<T> update(final DSL.TypeReference type, final Dynamic<T> 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();
}
}
}
@@ -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<ForgeSchema> 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<IModStateTransition.EventGenerator<?>> generateConfigureEvents()
{
//Grab the schemas
List<ForgeSchema> 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<List<Throwable>> preDispatch(
final Executor executor, final IModStateTransition.EventGenerator<ConfigureDataFixSchemaEvent> 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<List<Throwable>> postDispatch(
final Executor executor, final IModStateTransition.EventGenerator<ConfigureDataFixSchemaEvent> 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<List<Throwable>> rebuildFixer(
final Executor executor, final CompletableFuture<List<Throwable>> 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);
}
}
@@ -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<String, Supplier<TypeTemplate>> TYPE_TEMPLATES = Maps.newConcurrentMap();
private Map<String, Type<?>> TYPES = Maps.newConcurrentMap();
private final Map<String, Integer> RECURSIVE_TYPES = Maps.newConcurrentMap();
private final Map<String, Supplier<TypeTemplate>> MODDED_ENTITY_TYPES = Maps.newConcurrentMap();
private final Map<String, Supplier<TypeTemplate>> 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<String, Type<?>> buildTypes()
{
//Result map.
final Map<String, Type<?>> types = Maps.newConcurrentMap();
//All the core templates into which recursion of data is possible.
final List<TypeTemplate> templates = Lists.newArrayList();
//Check all recursion types
for (final Map.Entry<String, Integer> 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<TypeTemplate> 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<String> 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<String, Supplier<TypeTemplate>> entityTypes, final Map<String, Supplier<TypeTemplate>> 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<String, Supplier<TypeTemplate>> 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<String, Supplier<TypeTemplate>> 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<TypeTemplate> 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<String, Supplier<TypeTemplate>> modEntityTypes,
final Map<String, Supplier<TypeTemplate>> 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<String, Supplier<TypeTemplate>> entityTypes = registerEntities(this);
final Map<String, Supplier<TypeTemplate>> 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();
}
}
@@ -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<String, Supplier<TypeTemplate>> entityTypes = Maps.newConcurrentMap();
private final Map<String, Supplier<TypeTemplate>> blockEntityTypes = Maps.newConcurrentMap();
//Some unmodifiable views of the above maps
private final Map<String, Supplier<TypeTemplate>> entityTypesView = Collections.unmodifiableMap(entityTypes);
private final Map<String, Supplier<TypeTemplate>> 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<String, TypeTemplate> 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<TypeTemplate> 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<String, TypeTemplate> 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<TypeTemplate> 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<String, Supplier<TypeTemplate>> 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<String, Supplier<TypeTemplate>> getBlockEntityTypes()
{
return blockEntityTypesView;
}
}