/*
* Copyright (c) Forge Development LLC and contributors
* SPDX-License-Identifier: LGPL-2.1-only
*/
package net.minecraftforge.registries;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Multimap;
import com.mojang.serialization.MapCodec;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.stream.Collectors;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import net.minecraft.core.DefaultedRegistry;
import net.minecraft.core.Holder;
import net.minecraft.core.IdMapper;
import net.minecraft.core.MappedRegistry;
import net.minecraft.core.Registry;
import net.minecraft.core.WritableRegistry;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.network.syncher.EntityDataSerializer;
import net.minecraft.resources.ResourceKey;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.SpawnPlacements;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.DefaultAttributes;
import net.minecraft.world.entity.ai.village.poi.PoiType;
import net.minecraft.world.item.BlockItem;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemDisplayContext;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntityType;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.block.state.StateDefinition;
import net.minecraft.world.level.levelgen.DebugLevelSource;
import net.minecraft.world.level.material.Fluid;
import net.minecraftforge.common.CreativeModeTabRegistry;
import net.minecraftforge.common.ForgeHooks;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.loot.IGlobalLootModifier;
import net.minecraftforge.common.util.LogMessageAdapter;
import net.minecraftforge.fluids.FluidType;
import net.minecraftforge.fml.ModLoader;
import net.minecraftforge.fml.ModLoadingContext;
import net.minecraftforge.fml.StartupMessageManager;
import net.minecraftforge.fml.util.EnhancedRuntimeException;
import net.minecraftforge.fml.util.thread.EffectiveSide;
import net.minecraftforge.registries.ForgeRegistries.Keys;
import net.minecraftforge.registries.IForgeRegistry.AddCallback;
import net.minecraftforge.registries.IForgeRegistry.BakeCallback;
import net.minecraftforge.registries.IForgeRegistry.ClearCallback;
import net.minecraftforge.registries.IForgeRegistry.CreateCallback;
import net.minecraftforge.registries.IForgeRegistry.SlaveKey;
import net.minecraftforge.registries.IForgeRegistry.ValidateCallback;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Nullable;
/**
* INTERNAL ONLY
* MODDERS SHOULD HAVE NO REASON TO USE THIS CLASS
*
Use the public {@link IForgeRegistry} and {@link ForgeRegistries} APIs to get the data
*/
@ApiStatus.Internal
public class GameData {
private static final Logger LOGGER = LogManager.getLogger();
private static final Marker REGISTRIES = ForgeRegistry.REGISTRIES;
private static final int MAX_VARINT = Integer.MAX_VALUE - 1; //We were told it is their intention to have everything in a reg be unlimited, so assume that until we find cases where it isnt.
private static boolean hasInit = false;
private static final boolean DISABLE_VANILLA_REGISTRIES = Boolean.parseBoolean(System.getProperty("forge.disableVanillaGameData", "false")); // Use for unit tests/debugging
private static final BiConsumer> LOCK_VANILLA = (name, reg) -> reg.slaves.values().stream().filter(o -> o instanceof ILockableRegistry).forEach(o -> ((ILockableRegistry)o).lock());
private static Set vanillaRegistryOrder = null;
static {
init();
}
@SuppressWarnings("deprecation")
public static void init() {
if (DISABLE_VANILLA_REGISTRIES) {
LOGGER.warn(REGISTRIES, "DISABLING VANILLA REGISTRY CREATION AS PER SYSTEM VARIABLE SETTING! forge.disableVanillaGameData");
return;
}
if (hasInit)
return;
hasInit = true;
// Game objects
makeRegistry(Keys.BLOCKS, "air").addCallback(BlockCallbacks.INSTANCE).legacyName("blocks").intrusiveHolderCallback(Block::builtInRegistryHolder).create();
makeRegistry(Keys.FLUIDS, "empty").intrusiveHolderCallback(Fluid::builtInRegistryHolder).create();
makeRegistry(Keys.ITEMS, "air").addCallback(ItemCallbacks.INSTANCE).legacyName("items").intrusiveHolderCallback(Item::builtInRegistryHolder).create();
makeRegistry(Keys.MOB_EFFECTS).create();
makeRegistry(Keys.SOUND_EVENTS).create();
makeRegistry(Keys.POTIONS).create();
makeRegistry(Keys.ENTITY_TYPES, "pig").legacyName("entities").intrusiveHolderCallback(EntityType::builtInRegistryHolder).create();
makeRegistry(Keys.BLOCK_ENTITY_TYPES).disableSaving().legacyName("blockentities").intrusiveHolderCallback(BlockEntityType::builtInRegistryHolder).create();
makeRegistry(Keys.PARTICLE_TYPES).disableSaving().create();
makeRegistry(Keys.MENU_TYPES).disableSaving().create();
makeRegistry(Keys.PAINTING_VARIANTS, "kebab").create();
makeRegistry(Keys.RECIPE_TYPES).disableSaving().disableSync().create();
makeRegistry(Keys.RECIPE_SERIALIZERS).disableSaving().create();
makeRegistry(Keys.ATTRIBUTES).onValidate(AttributeCallbacks.INSTANCE).disableSaving().disableSync().create();
makeRegistry(Keys.STAT_TYPES).create();
makeRegistry(Keys.COMMAND_ARGUMENT_TYPES).disableSaving().create();
makeRegistry(Registries.DATA_COMPONENT_TYPE).disableSaving().create();
// Villagers
makeRegistry(Keys.VILLAGER_PROFESSIONS, "none").create();
makeRegistry(Keys.POI_TYPES).addCallback(PoiTypeCallbacks.INSTANCE).disableSync().create();
makeRegistry(Keys.MEMORY_MODULE_TYPES, "dummy").disableSync().create();
makeRegistry(Keys.SENSOR_TYPES, "dummy").disableSaving().disableSync().create();
makeRegistry(Keys.SCHEDULES).disableSaving().disableSync().create();
makeRegistry(Keys.ACTIVITIES).disableSaving().disableSync().create();
// Worldgen
makeRegistry(Keys.WORLD_CARVERS).disableSaving().disableSync().create();
makeRegistry(Keys.FEATURES).disableSaving().disableSync().create();
makeRegistry(Keys.CHUNK_STATUS, "empty").disableSaving().disableSync().create();
makeRegistry(Keys.BLOCK_STATE_PROVIDER_TYPES).disableSaving().disableSync().create();
makeRegistry(Keys.FOLIAGE_PLACER_TYPES).disableSaving().disableSync().create();
makeRegistry(Keys.TREE_DECORATOR_TYPES).disableSaving().disableSync().create();
// Dynamic Worldgen
makeRegistry(Keys.BIOMES).disableSync().create();
}
static RegistryBuilder> getDataSerializersRegistryBuilder() {
return makeRegistry(Keys.ENTITY_DATA_SERIALIZERS, 256 /*vanilla space*/, MAX_VARINT).disableSaving().disableOverrides();
}
static RegistryBuilder> getGLMSerializersRegistryBuilder() {
return makeRegistry(Keys.GLOBAL_LOOT_MODIFIER_SERIALIZERS).disableSaving().disableSync();
}
static RegistryBuilder getFluidTypeRegistryBuilder() {
return makeRegistry(Keys.FLUID_TYPES).disableSaving();
}
static RegistryBuilder makeUnsavedAndUnsynced() {
return RegistryBuilder.of().disableSaving().disableSync();
}
static RegistryBuilder getItemDisplayContextRegistryBuilder() {
return new RegistryBuilder()
.setMaxID(128 * 2) /* 0 -> 127 gets positive ID, 128 -> 256 gets negative ID */.disableOverrides().disableSaving()
.setDefaultKey(ResourceLocation.withDefaultNamespace("none"))
.onAdd(ItemDisplayContext.ADD_CALLBACK);
}
private static RegistryBuilder makeRegistry(ResourceKey extends Registry> key) {
return new RegistryBuilder().setName(key.location()).setMaxID(MAX_VARINT).hasWrapper();
}
private static RegistryBuilder makeRegistry(ResourceKey extends Registry> key, int min, int max) {
return new RegistryBuilder().setName(key.location()).setIDRange(min, max).hasWrapper();
}
private static RegistryBuilder makeRegistry(ResourceKey extends Registry> key, String _default) {
return new RegistryBuilder().setName(key.location()).setMaxID(MAX_VARINT).hasWrapper().setDefaultKey(ResourceLocation.parse(_default));
}
@SuppressWarnings("unchecked")
public static > R getWrapper(ResourceKey extends Registry> key, R vanilla) {
var reg = RegistryManager.ACTIVE.getRegistry(key.location());
if (reg == null)
return vanilla;
var wrapper = reg.getSlaveMap(WrapperFactory.WRAPPER);
if (wrapper == null)
return vanilla;
var vanillaD = vanilla instanceof DefaultedRegistry tmp ? tmp : null;
var wrapperD = wrapper instanceof DefaultedRegistry tmp ? tmp : null;
if (vanillaD == null && wrapperD != null)
throw new IllegalStateException("Invalid wrapper " + key.location() + " was defaulted when not expected");
if (vanillaD != null && wrapperD == null)
throw new IllegalStateException("Invalid wrapper " + key.location() + " was normal when should be expected");
if (vanillaD != null && !vanillaD.getDefaultKey().equals(wrapperD.getDefaultKey()))
throw new IllegalStateException("Invalid wrapper " + key.location() + " mismatched default key " + vanillaD.getDefaultKey() + " != " + wrapperD.getDefaultKey());
return (R)wrapper;
}
static WrapperFactory createWrapperFactory(boolean defaulted) {
BiFunction, RegistryManager, WritableRegistry> factory = defaulted
? (reg, stage) -> new NamespacedDefaultedWrapper<>(reg, reg.getBuilder().getIntrusiveHolderCallback(), stage)
: (reg, stage) -> new NamespacedWrapper(reg, reg.getBuilder().getIntrusiveHolderCallback(), stage);
return new WrapperFactory<>(factory);
}
static record WrapperFactory(BiFunction, RegistryManager, WritableRegistry> factory) implements CreateCallback, AddCallback {
static SlaveKey> WRAPPER = SlaveKey.create("forge:vanilla_wrapper");
@Override
public void onCreate(IForgeRegistryInternal owner, RegistryManager stage) {
owner.setSlaveMap(WRAPPER, factory.apply((ForgeRegistry)owner, stage));
}
@Override
public void onAdd(IForgeRegistryInternal owner, RegistryManager stage, int id, ResourceKey key, V value, V oldValue) {
@SuppressWarnings("unchecked")
var wrapper = (NamespacedWrapper)owner.getSlaveMap(WRAPPER);
wrapper.onAdded(stage, id, key, value, oldValue);
}
}
public static void vanillaSnapshot() {
LOGGER.debug(REGISTRIES, "Creating vanilla freeze snapshot");
for (var r : RegistryManager.ACTIVE.registries.entrySet())
loadRegistry(r.getKey(), RegistryManager.ACTIVE, RegistryManager.VANILLA, true);
RegistryManager.VANILLA.registries.forEach((name, reg) -> {
reg.validateContent(name);
reg.freeze();
});
RegistryManager.VANILLA.registries.forEach(LOCK_VANILLA);
RegistryManager.ACTIVE.registries.forEach(LOCK_VANILLA);
// Capture the vanilla registry order.
vanillaRegistryOrder = new LinkedHashSet<>(MappedRegistry.getKnownRegistries());
LOGGER.debug(REGISTRIES, "Vanilla registry order:");
for (var key : vanillaRegistryOrder)
LOGGER.info(REGISTRIES, "\t" + key);
LOGGER.debug(REGISTRIES, "Vanilla freeze snapshot created");
}
@SuppressWarnings("deprecation")
public static void unfreezeData() {
LOGGER.debug(REGISTRIES, "Unfreezing vanilla registries");
for (var reg : BuiltInRegistries.REGISTRY) {
if (reg instanceof MappedRegistry mapped)
mapped.unfreeze();
}
}
public static void freezeData() {
LOGGER.debug(REGISTRIES, "Freezing registries");
for (var reg : BuiltInRegistries.REGISTRY) {
if (reg instanceof NamespacedWrapper> named) {
// This is called after we fire our register events, but before doesn't load any tags.
// So lets skip the validation for this pass and just make every unbound tag empty
// Tags will be bound later when tag data is reloaded/synced
named.bindAllUnboundTagsToEmpty();
named.freeze();
} else if (reg instanceof MappedRegistry maped)
maped.freeze();
}
for (var r : RegistryManager.ACTIVE.registries.entrySet())
loadRegistry(r.getKey(), RegistryManager.ACTIVE, RegistryManager.FROZEN, true);
RegistryManager.FROZEN.registries.forEach((name, reg) -> {
reg.validateContent(name);
reg.freeze();
});
RegistryManager.ACTIVE.registries.forEach((name, reg) -> {
reg.freeze();
reg.bake();
reg.dump(name);
});
// the id mapping is finalized, no ids actually changed but this is a good place to tell everyone to 'bake' their stuff.
fireRemapEvent(ImmutableMap.of(), true);
LOGGER.debug(REGISTRIES, "All registries frozen");
}
public static void revertToFrozen() {
revertTo(RegistryManager.FROZEN, true);
}
public static void revertTo(final RegistryManager target, boolean fireEvents) {
if (target.registries.isEmpty()) {
LOGGER.warn(REGISTRIES, "Can't revert to {} GameData state without a valid snapshot.", target.getName());
return;
}
RegistryManager.ACTIVE.registries.forEach((name, reg) -> reg.resetDelegates());
LOGGER.debug(REGISTRIES, "Reverting to {} data state.", target.getName());
for (var r : RegistryManager.ACTIVE.registries.entrySet())
loadRegistry(r.getKey(), target, RegistryManager.ACTIVE, true);
RegistryManager.ACTIVE.registries.forEach((name, reg) -> reg.bake());
// the id mapping has reverted, fire remap events for those that care about id changes
if (fireEvents) {
fireRemapEvent(ImmutableMap.of(), true);
ObjectHolderRegistry.applyObjectHolders();
}
// the id mapping has reverted, ensure we sync up the object holders
LOGGER.debug(REGISTRIES, "{} state restored.", target.getName());
}
public static void revert(RegistryManager state, ResourceLocation registry, boolean lock) {
LOGGER.debug(REGISTRIES, "Reverting {} to {}", registry, state.getName());
loadRegistry(registry, state, RegistryManager.ACTIVE, lock);
LOGGER.debug(REGISTRIES, "Reverting complete");
}
@SuppressWarnings("deprecation")
public static void postRegisterEvents() {
Set keySet = new HashSet<>(RegistryManager.ACTIVE.registries.keySet());
keySet.addAll(RegistryManager.getVanillaRegistryKeys());
Set ordered = new LinkedHashSet<>(vanillaRegistryOrder);
ordered.retainAll(keySet);
ordered.addAll(keySet.stream().sorted(ResourceLocation::compareNamespaced).toList());
RuntimeException aggregate = new RuntimeException();
for (ResourceLocation rootRegistryName : ordered) {
try {
ResourceKey extends Registry>> registryKey = ResourceKey.createRegistryKey(rootRegistryName);
ForgeRegistry> forgeRegistry = RegistryManager.ACTIVE.getRegistry(rootRegistryName);
Registry> vanillaRegistry = BuiltInRegistries.REGISTRY.getValue(rootRegistryName);
RegisterEvent registerEvent = new RegisterEvent(registryKey, forgeRegistry, vanillaRegistry);
StartupMessageManager.modLoaderConsumer().ifPresent(s -> s.accept("REGISTERING " + registryKey.location()));
if (forgeRegistry != null)
forgeRegistry.unfreeze();
ModLoader.get().postEventWrapContainerInModOrder(registerEvent);
if (forgeRegistry != null)
forgeRegistry.freeze();
LOGGER.debug(REGISTRIES, "Applying holder lookups: {}", registryKey.location());
ObjectHolderRegistry.applyObjectHolders(registryKey.location());
LOGGER.debug(REGISTRIES, "Holder lookups applied: {}", registryKey.location());
} catch (Throwable t) {
aggregate.addSuppressed(t);
}
}
if (aggregate.getSuppressed().length > 0) {
LOGGER.fatal("Failed to register some entries, see suppressed exceptions for details", aggregate);
LOGGER.fatal("Detected errors during registry event dispatch, rolling back to VANILLA state");
revertTo(RegistryManager.VANILLA, false);
LOGGER.fatal("Detected errors during registry event dispatch, roll back to VANILLA complete");
throw aggregate;
} else {
ForgeHooks.modifyAttributes();
SpawnPlacements.fireSpawnPlacementEvent();
CreativeModeTabRegistry.sortTabs();
}
}
//Lets us clear the map so we can rebuild it.
private static class ClearableObjectIntIdentityMap extends IdMapper {
void clear() {
this.tToId.clear();
this.idToT.clear();
this.nextId = 0;
}
@SuppressWarnings("unused")
void remove(I key) {
boolean hadId = this.tToId.containsKey(key);
int prev = this.tToId.removeInt(key);
if (hadId) {
this.idToT.set(prev, null);
}
}
}
public static class BlockCallbacks implements AddCallback, ClearCallback, BakeCallback, CreateCallback {
static final BlockCallbacks INSTANCE = new BlockCallbacks();
private static final SlaveKey> STATE_TO_ID = SlaveKey.create("state_to_id");
public static IdMapper getBlockStateIDMap() {
return RegistryManager.ACTIVE.getRegistry(Keys.BLOCKS).getSlaveMap(STATE_TO_ID);
}
@Override
public void onAdd(IForgeRegistryInternal owner, RegistryManager stage, int id, ResourceKey key, Block block, @Nullable Block oldBlock) {
if (oldBlock != null) {
StateDefinition oldContainer = oldBlock.getStateDefinition();
StateDefinition newContainer = block.getStateDefinition();
// Test vanilla blockstates, if the number matches, make sure they also match in their string representations
if (key.location().getNamespace().equals("minecraft") && !oldContainer.getProperties().equals(newContainer.getProperties())) {
String oldSequence = oldContainer.getProperties().stream()
.map(s -> String.format(Locale.ENGLISH, "%s={%s}", s.getName(),
s.getPossibleValues().stream().map(Object::toString).collect(Collectors.joining( "," ))))
.collect(Collectors.joining(";"));
String newSequence = newContainer.getProperties().stream()
.map(s -> String.format(Locale.ENGLISH, "%s={%s}", s.getName(),
s.getPossibleValues().stream().map(Object::toString).collect(Collectors.joining( "," ))))
.collect(Collectors.joining(";"));
LOGGER.error(REGISTRIES,()-> LogMessageAdapter.adapt(sb-> {
sb.append("Registry replacements for vanilla block '").append(key.location())
.append("' must not change the number or order of blockstates.\n");
sb.append("\tOld: ").append(oldSequence).append('\n');
sb.append("\tNew: ").append(newSequence);
}));
throw new RuntimeException("Invalid vanilla replacement. See log for details.");
}
}
}
@Override
public void onClear(IForgeRegistryInternal owner, RegistryManager stage) {
owner.getSlaveMap(STATE_TO_ID).clear();
}
@Override
public void onCreate(IForgeRegistryInternal owner, RegistryManager stage) {
var idMap = new ClearableObjectIntIdentityMap() {
@Override
public int getId(BlockState key) {
return this.tToId.containsKey(key) ? this.tToId.getInt(key) : -1;
}
};
owner.setSlaveMap(STATE_TO_ID, idMap);
}
@Override
public void onBake(IForgeRegistryInternal owner, RegistryManager stage) {
var blockstateMap = owner.getSlaveMap(STATE_TO_ID);
for (Block block : owner) {
for (BlockState state : block.getStateDefinition().getPossibleStates()) {
blockstateMap.add(state);
state.initCache();
}
block.getLootTable();
}
DebugLevelSource.initValidStates();
}
}
public static class ItemCallbacks implements AddCallback- , ClearCallback
- , CreateCallback
- {
static final ItemCallbacks INSTANCE = new ItemCallbacks();
private static final SlaveKey