From 997d8e0aa28b831edcd712e59a96181d3b2117d4 Mon Sep 17 00:00:00 2001 From: Joseph B Date: Sun, 5 Jun 2022 10:27:30 -0400 Subject: [PATCH] [1.18.x] Allow Modders to add custom data-pack registries through RegistryBuilder (#8630) Co-authored-by: Silverminer007 <66484505+Silverminer007@users.noreply.github.com> Co-authored-by: Curle <42079760+TheCurle@users.noreply.github.com> --- build.gradle | 1 + .../minecraft/core/RegistryAccess.java.patch | 11 + .../RegistryResourceAccess.java.patch | 9 + src/generated_test/resources/.cache/cache | 1 + .../unsyncable/datagen_test.json | 3 + .../network/HandshakeHandler.java | 22 ++ .../network/HandshakeMessages.java | 31 ++- .../network/NetworkConstants.java | 6 +- .../network/NetworkRegistry.java | 4 +- .../registries/DataPackRegistriesHooks.java | 60 +++++ .../registries/NewRegistryEvent.java | 13 +- .../registries/RegistryBuilder.java | 73 ++++++ .../registries/RegistryManager.java | 15 +- .../server/ServerLifecycleHooks.java | 26 +- .../resources/assets/forge/lang/en_us.json | 1 + .../debug/misc/DataPackRegistriesTest.java | 239 ++++++++++++++++++ src/test/resources/META-INF/mods.toml | 2 + .../syncable/test.json | 3 + .../unsyncable/test.json | 3 + .../syncable/test.json | 6 + .../unsyncable/test.json | 6 + 21 files changed, 525 insertions(+), 10 deletions(-) create mode 100644 patches/minecraft/net/minecraft/core/RegistryAccess.java.patch create mode 100644 src/generated_test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json create mode 100644 src/main/java/net/minecraftforge/registries/DataPackRegistriesHooks.java create mode 100644 src/test/java/net/minecraftforge/debug/misc/DataPackRegistriesTest.java create mode 100644 src/test/resources/data/data_pack_registries_test/data_pack_registries_test/syncable/test.json create mode 100644 src/test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/test.json create mode 100644 src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/syncable/test.json create mode 100644 src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/unsyncable/test.json diff --git a/build.gradle b/build.gradle index f82e69da7d..1d944207c4 100644 --- a/build.gradle +++ b/build.gradle @@ -1044,6 +1044,7 @@ project(':forge') { '--mod', 'remove_tag_datagen_test', '--mod', 'tag_based_tool_types', '--mod', 'custom_transformtype_test', + '--mod', 'data_pack_registries_test', '--output', rootProject.file('src/generated_test/resources/'), '--existing', sourceSets.main.resources.srcDirs[0], '--existing', sourceSets.test.resources.srcDirs[0] diff --git a/patches/minecraft/net/minecraft/core/RegistryAccess.java.patch b/patches/minecraft/net/minecraft/core/RegistryAccess.java.patch new file mode 100644 index 0000000000..304303480a --- /dev/null +++ b/patches/minecraft/net/minecraft/core/RegistryAccess.java.patch @@ -0,0 +1,11 @@ +--- a/net/minecraft/core/RegistryAccess.java ++++ b/net/minecraft/core/RegistryAccess.java +@@ -58,7 +_,7 @@ + m_123053_(builder, Registry.f_122878_, NoiseGeneratorSettings.f_64430_); + m_123053_(builder, Registry.f_194568_, NormalNoise.NoiseParameters.f_192851_); + m_123053_(builder, Registry.f_211074_, DensityFunction.f_208216_); +- return builder.build(); ++ return net.minecraftforge.registries.DataPackRegistriesHooks.grabBuiltinRegistries(builder); // FORGE: Keep the map so custom registries can be added later + }); + Codec f_206151_ = m_206205_(); + Supplier f_123049_ = Suppliers.memoize(() -> { diff --git a/patches/minecraft/net/minecraft/resources/RegistryResourceAccess.java.patch b/patches/minecraft/net/minecraft/resources/RegistryResourceAccess.java.patch index d9c66ccf91..86df6b3e81 100644 --- a/patches/minecraft/net/minecraft/resources/RegistryResourceAccess.java.patch +++ b/patches/minecraft/net/minecraft/resources/RegistryResourceAccess.java.patch @@ -8,6 +8,15 @@ optional = Optional.of(p_195896_.parse(p_195893_, jsonelement).map(RegistryResourceAccess.ParsedEntry::m_195956_)); } catch (Throwable throwable2) { try { +@@ -102,6 +_,8 @@ + } + + private static String m_195909_(ResourceKey> p_195910_) { ++ if (net.minecraftforge.registries.RegistryManager.FROZEN.getRegistry(p_195910_.m_135782_()) != null && !(p_195910_.m_135782_().m_135827_().equals("minecraft"))) ++ return p_195910_.m_135782_().m_135827_() + "/" + p_195910_.m_135782_().m_135815_(); // FORGE: add non-vanilla registry namespace to loader directory, same format as tag directory (see net.minecraft.tags.TagManager#getTagDir(ResourceKey)) + return p_195910_.m_135782_().m_135815_(); + } + @@ -138,6 +_,8 @@ public Optional>> m_183313_(DynamicOps p_195917_, ResourceKey> p_195918_, ResourceKey p_195919_, Decoder p_195920_) { diff --git a/src/generated_test/resources/.cache/cache b/src/generated_test/resources/.cache/cache index 18d1277855..a0b810a135 100644 --- a/src/generated_test/resources/.cache/cache +++ b/src/generated_test/resources/.cache/cache @@ -117,6 +117,7 @@ d65c425e740dc833f29d16606a1171825876be0d data/data_gen_test/tags/blocks/thing/on 1f70ed4ddc878bada5e43d3c44a34157806beda8 data/data_gen_test/tags/blocks/thing/three.json 2c648bca262caaa826ddbbde796d35bb2ee5f03a data/data_gen_test/tags/blocks/thing/two.json b0565ce75fc7880be7517e86a218d1ed9d9e346f data/data_gen_test/tags/blocks/things.json +e6215cc7edbba7c10b8d40dbdb837239a37b46b3 data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json f4d3a59005dafa334c835e43ed0047487e17324d data/forge/loot_modifiers/global_loot_modifiers.json 5bd8a05038d2633f89b3b1c5ac319eb4b347b335 data/global_loot_test/loot_modifiers/dungeon_loot.json f65e40b06ce7ad18e053ec88058b951f98ae5f40 data/global_loot_test/loot_modifiers/smelting.json diff --git a/src/generated_test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json b/src/generated_test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json new file mode 100644 index 0000000000..5ca03ce3ca --- /dev/null +++ b/src/generated_test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json @@ -0,0 +1,3 @@ +{ + "value": "Datagen Success" +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/network/HandshakeHandler.java b/src/main/java/net/minecraftforge/network/HandshakeHandler.java index 48f81ca64f..1afec1c9d1 100644 --- a/src/main/java/net/minecraftforge/network/HandshakeHandler.java +++ b/src/main/java/net/minecraftforge/network/HandshakeHandler.java @@ -7,17 +7,22 @@ package net.minecraftforge.network; import com.google.common.collect.Multimap; import com.mojang.authlib.GameProfile; + +import net.minecraft.core.Registry; import net.minecraft.network.Connection; import net.minecraft.network.protocol.handshake.ClientIntentionPacket; import net.minecraft.network.protocol.login.ClientboundCustomQueryPacket; import net.minecraft.network.protocol.login.ServerboundCustomQueryPacket; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.network.chat.TextComponent; +import net.minecraft.network.chat.TranslatableComponent; import net.minecraft.server.network.ServerLoginPacketListenerImpl; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.common.util.LogMessageAdapter; import net.minecraftforge.event.entity.player.PlayerNegotiationEvent; import net.minecraftforge.network.simple.SimpleChannel; +import net.minecraftforge.registries.DataPackRegistriesHooks; import net.minecraftforge.registries.ForgeRegistry; import net.minecraftforge.registries.GameData; import org.apache.logging.log4j.LogManager; @@ -180,6 +185,23 @@ public class HandshakeHandler c.get().getNetworkManager().disconnect(new TextComponent("Connection closed - mismatched mod channel list")); return; } + // Validate synced custom datapack registries, client cannot be missing any present on the server. + List missingDataPackRegistries = new ArrayList<>(); + Set>> clientDataPackRegistries = DataPackRegistriesHooks.getSyncedCustomRegistries(); + for (ResourceKey> key : serverModList.getCustomDataPackRegistries()) + { + if (!clientDataPackRegistries.contains(key)) + { + ResourceLocation location = key.location(); + LOGGER.error(FMLHSMARKER, "Missing required datapack registry: {}", location); + missingDataPackRegistries.add(key.location().toString()); + } + } + if (!missingDataPackRegistries.isEmpty()) + { + c.get().getNetworkManager().disconnect(new TranslatableComponent("fml.menu.multiplayer.missingdatapackregistries", String.join(", ", missingDataPackRegistries))); + return; + } NetworkConstants.handshakeChannel.reply(new HandshakeMessages.C2SModListReply(), c.get()); LOGGER.debug(FMLHSMARKER, "Accepted server connection"); diff --git a/src/main/java/net/minecraftforge/network/HandshakeMessages.java b/src/main/java/net/minecraftforge/network/HandshakeMessages.java index 5b7397d949..63afa9fbfd 100644 --- a/src/main/java/net/minecraftforge/network/HandshakeMessages.java +++ b/src/main/java/net/minecraftforge/network/HandshakeMessages.java @@ -5,11 +5,14 @@ package net.minecraftforge.network; +import net.minecraft.core.Registry; import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.ModList; import net.minecraftforge.forgespi.language.IModInfo; import net.minecraftforge.network.NetworkRegistry; +import net.minecraftforge.registries.DataPackRegistriesHooks; import net.minecraftforge.registries.ForgeRegistry; import net.minecraftforge.registries.RegistryManager; @@ -17,6 +20,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.IntSupplier; import java.util.stream.Collectors; @@ -51,19 +55,22 @@ public class HandshakeMessages private List mods; private Map channels; private List registries; + private final List>> dataPackRegistries; public S2CModList() { this.mods = ModList.get().getMods().stream().map(IModInfo::getModId).collect(Collectors.toList()); this.channels = NetworkRegistry.buildChannelVersions(); this.registries = RegistryManager.getRegistryNamesForSyncToClient(); + this.dataPackRegistries = List.copyOf(DataPackRegistriesHooks.getSyncedCustomRegistries()); } - private S2CModList(List mods, Map channels, List registries) + private S2CModList(List mods, Map channels, List registries, List>> dataPackRegistries) { this.mods = mods; this.channels = channels; this.registries = registries; + this.dataPackRegistries = dataPackRegistries; } public static S2CModList decode(FriendlyByteBuf input) @@ -83,7 +90,12 @@ public class HandshakeMessages for (int x = 0; x < len; x++) registries.add(input.readResourceLocation()); - return new S2CModList(mods, channels, registries); + // Datapack Registries may or may not be sent in 1.18.2 due to netcode changes. + // TODO 1.19: Remove optionalness of datapack registry list in the mod list packet. + List>> dataPackRegistries = input.isReadable() + ? input.readCollection(ArrayList::new, buf -> ResourceKey.createRegistryKey(buf.readResourceLocation())) + : List.of(); + return new S2CModList(mods, channels, registries, dataPackRegistries); } public void encode(FriendlyByteBuf output) @@ -99,6 +111,14 @@ public class HandshakeMessages output.writeVarInt(registries.size()); registries.forEach(output::writeResourceLocation); + + // The list of synced datapack registry names is not sent in 1.18.2 if the list is empty. + // TODO 1.19: should send an empty list if the list is empty. + Set>> dataPackRegistries = DataPackRegistriesHooks.getSyncedCustomRegistries(); + if (!dataPackRegistries.isEmpty()) + { + output.writeCollection(dataPackRegistries, (buf,key) -> buf.writeResourceLocation(key.location())); + } } public List getModList() { @@ -112,6 +132,13 @@ public class HandshakeMessages public Map getChannels() { return this.channels; } + + /** + * @return list of ids of non-vanilla syncable datapack registries on the server. + */ + public List>> getCustomDataPackRegistries() { + return this.dataPackRegistries; + } } public static class C2SModListReply extends LoginIndexedMessage diff --git a/src/main/java/net/minecraftforge/network/NetworkConstants.java b/src/main/java/net/minecraftforge/network/NetworkConstants.java index 3b7823860b..26cf2c93a5 100644 --- a/src/main/java/net/minecraftforge/network/NetworkConstants.java +++ b/src/main/java/net/minecraftforge/network/NetworkConstants.java @@ -8,6 +8,7 @@ package net.minecraftforge.network; import io.netty.util.AttributeKey; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.fml.IExtensionPoint.DisplayTest; +import net.minecraftforge.network.HandshakeMessages.S2CModList; import net.minecraftforge.network.event.EventNetworkChannel; import net.minecraftforge.network.simple.SimpleChannel; import org.apache.logging.log4j.Marker; @@ -20,7 +21,10 @@ import java.util.List; public class NetworkConstants { public static final String FMLNETMARKER = "FML"; - public static final int FMLNETVERSION = 2; + /** + * Netversion 3: {@link S2CModList} packet may include a list of non-vanilla synced datapack registry ids. + */ + public static final int FMLNETVERSION = 3; public static final String NETVERSION = FMLNETMARKER + FMLNETVERSION; public static final String NOVERSION = "NONE"; diff --git a/src/main/java/net/minecraftforge/network/NetworkRegistry.java b/src/main/java/net/minecraftforge/network/NetworkRegistry.java index 52ff308d12..751c414b0b 100644 --- a/src/main/java/net/minecraftforge/network/NetworkRegistry.java +++ b/src/main/java/net/minecraftforge/network/NetworkRegistry.java @@ -10,6 +10,8 @@ import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.network.event.EventNetworkChannel; import net.minecraftforge.network.simple.SimpleChannel; +import net.minecraftforge.registries.DataPackRegistriesHooks; + import org.apache.commons.lang3.tuple.Pair; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -80,7 +82,7 @@ public class NetworkRegistry } public static boolean acceptsVanillaClientConnections() { - return instances.isEmpty() || getServerNonVanillaNetworkMods().isEmpty(); + return (instances.isEmpty() || getServerNonVanillaNetworkMods().isEmpty()) && DataPackRegistriesHooks.getSyncedCustomRegistries().isEmpty(); } public static boolean canConnectToVanillaServer() { diff --git a/src/main/java/net/minecraftforge/registries/DataPackRegistriesHooks.java b/src/main/java/net/minecraftforge/registries/DataPackRegistriesHooks.java new file mode 100644 index 0000000000..117ec09881 --- /dev/null +++ b/src/main/java/net/minecraftforge/registries/DataPackRegistriesHooks.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.registries; + +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +import com.google.common.collect.ImmutableMap; + +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; + +@ApiStatus.Internal +public final class DataPackRegistriesHooks +{ + private DataPackRegistriesHooks() {} // utility class + + private static Map>, RegistryAccess.RegistryData> REGISTRY_ACCESS_REGISTRIES_COPY; + private static final Set>> SYNCED_CUSTOM_REGISTRIES = new HashSet<>(); + private static final Set>> SYNCED_CUSTOM_REGISTRIES_VIEW = Collections.unmodifiableSet(SYNCED_CUSTOM_REGISTRIES); + + /* Internal forge hook for retaining mutable access to RegistryAccess's codec registry when it bootstraps. */ + public static Map>, RegistryAccess.RegistryData> grabBuiltinRegistries(ImmutableMap.Builder>, RegistryAccess.RegistryData> builder) + { + REGISTRY_ACCESS_REGISTRIES_COPY = new HashMap<>(builder.build()); + SYNCED_CUSTOM_REGISTRIES.clear(); + return Collections.unmodifiableMap(REGISTRY_ACCESS_REGISTRIES_COPY); + } + + /* Internal forge method, registers a datapack registry codec and folder. */ + public static void addRegistryCodec(@NotNull RegistryAccess.RegistryData data) + { + ResourceKey> registryKey = data.key(); + REGISTRY_ACCESS_REGISTRIES_COPY.put(registryKey, data); + if (data.networkCodec() != null) + { + SYNCED_CUSTOM_REGISTRIES.add(registryKey); + } + } + + /** + * {@return unmodifiable view of the set of synced non-vanilla datapack registry IDs} + * Clients must have each of a server's synced datapack registries to be able to connect to that server; + * vanilla clients therefore cannot connect if this list is non-empty on the server. + */ + public static Set>> getSyncedCustomRegistries() + { + return SYNCED_CUSTOM_REGISTRIES_VIEW; + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/registries/NewRegistryEvent.java b/src/main/java/net/minecraftforge/registries/NewRegistryEvent.java index a4cedaafb5..2f842f4f1d 100644 --- a/src/main/java/net/minecraftforge/registries/NewRegistryEvent.java +++ b/src/main/java/net/minecraftforge/registries/NewRegistryEvent.java @@ -7,6 +7,8 @@ package net.minecraftforge.registries; import com.mojang.logging.LogUtils; import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.data.BuiltinRegistries; import net.minecraftforge.eventbus.api.Event; import net.minecraftforge.fml.event.IModBusEvent; import org.jetbrains.annotations.Nullable; @@ -76,7 +78,16 @@ public class NewRegistryEvent extends Event implements IModBusEvent builtRegistries.forEach((builder, reg) -> { try { - if (builder.getHasWrapper() && !Registry.REGISTRY.containsKey(reg.getRegistryName())) + RegistryAccess.RegistryData dataPackRegistryData = builder.getDataPackRegistryData(); + if (dataPackRegistryData != null) // if this is a datapack registry + { + if (!BuiltinRegistries.REGISTRY.containsKey(reg.getRegistryName())) + { + DataPackRegistriesHooks.addRegistryCodec(dataPackRegistryData); + RegistryManager.registerToBuiltinRegistry((ForgeRegistry) reg); + } + } + else if (builder.getHasWrapper() && !Registry.REGISTRY.containsKey(reg.getRegistryName())) RegistryManager.registerToRootRegistry((ForgeRegistry) reg); } catch (Exception e) { diff --git a/src/main/java/net/minecraftforge/registries/RegistryBuilder.java b/src/main/java/net/minecraftforge/registries/RegistryBuilder.java index 91c9a68cd7..1ed93a4077 100644 --- a/src/main/java/net/minecraftforge/registries/RegistryBuilder.java +++ b/src/main/java/net/minecraftforge/registries/RegistryBuilder.java @@ -9,10 +9,17 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; +import java.util.function.Supplier; +import com.google.common.base.Suppliers; import com.google.common.collect.Lists; +import com.mojang.serialization.Codec; +import net.minecraft.client.multiplayer.ClientPacketListener; import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraftforge.registries.IForgeRegistry.*; @@ -38,6 +45,7 @@ public class RegistryBuilder> private boolean allowOverrides = true; private boolean allowModifications = false; private boolean hasWrapper = false; + private Supplier> dataPackRegistryData = () -> null; // If present, implies this is a datapack registry. private DummyFactory dummyFactory; private MissingFactory missingFactory; private Set legacyNames = new HashSet<>(); @@ -175,6 +183,11 @@ public class RegistryBuilder> return this; } + /** + * Prevents the registry from being synced to clients. Does *not* affect datapack registries, datapack registries are unsynced by default + * unless a non-null network codec is registered via {@link #dataPackRegistry(Codec, Codec)} + * @return this + */ public RegistryBuilder disableSync() { this.sync = false; @@ -230,6 +243,66 @@ public class RegistryBuilder> return this; } + /** + *

Register this registry as an unsynced datapack registry, which will cause data to be loaded from + * a datapack folder based on the registry's name. The mod that registers this registry does not need to exist + * on the client to connect to servers with the mod/registry.

+ *

Data JSONs will be loaded from {@code data//modid/registryname/}, where modid is the mod that registered this registry.

+ * + * @param codec the codec to be used for loading data from datapacks on servers + * @return this builder + * + * @see #dataPackRegistry(Codec, Codec) + */ + public RegistryBuilder dataPackRegistry(Codec codec) + { + return this.dataPackRegistry(codec, null); + } + + /** + *

Register this registry as a datapack registry, which will cause data to be loaded from + * a datapack folder based on the registry's name.

+ *

Data JSONs will be loaded from {@code data//modid/registryname/}, where modid is the mod that registered this registry.

+ * + * @param codec the codec to be used for loading data from datapacks on servers + * @param networkCodec the codec to be used for syncing loaded data to clients.
+ * If networkCodec is null, data will not be synced, and clients without the mod that registered this registry can + * connect to servers with the mod.
+ * If networkCodec is not null, then data will be synced (accessible via {@link ClientPacketListener#registryAccess()}), + * and the mod must be present on a client to connect to servers with the mod. + * @return this builder + * + * @see #dataPackRegistry(Codec) + */ + public RegistryBuilder dataPackRegistry(Codec codec, @Nullable Codec networkCodec) + { + this.hasWrapper(); // A wrapper is required for data pack registries. + this.disableSync(); // Datapack registries are synced using a different system than static registries. + // Supplier averts having to set the registry name before calling this. + this.dataPackRegistryData = Suppliers.memoize(() -> { + // Validate registry key. + if (this.registryName == null) + throw new IllegalStateException("Registry builder cannot build a datapack registry: registry name not set"); + + ResourceKey> registryKey = ResourceKey.createRegistryKey(this.registryName); + return new RegistryAccess.RegistryData<>(registryKey, codec, networkCodec); + }); + return this; + } + + /** + * Retrieves datapack registry information, if any. + * + * @return RegistryData containing the registry's key and codec(s). If returned data is null, this has not been marked as a datapack registry. + * + * @throws IllegalStateException if this has been marked as a datapack registry, but registry name has not been set. + */ + @Nullable + RegistryAccess.RegistryData getDataPackRegistryData() + { + return this.dataPackRegistryData.get(); + } + /** * Modders: Use {@link NewRegistryEvent#create(RegistryBuilder)} instead */ diff --git a/src/main/java/net/minecraftforge/registries/RegistryManager.java b/src/main/java/net/minecraftforge/registries/RegistryManager.java index 486b0fb405..c6b48be50f 100644 --- a/src/main/java/net/minecraftforge/registries/RegistryManager.java +++ b/src/main/java/net/minecraftforge/registries/RegistryManager.java @@ -22,6 +22,7 @@ import com.google.common.collect.Sets.SetView; import com.mojang.serialization.Lifecycle; import net.minecraft.core.WritableRegistry; +import net.minecraft.data.BuiltinRegistries; import net.minecraft.resources.ResourceKey; import net.minecraft.resources.ResourceLocation; import net.minecraft.core.Registry; @@ -161,10 +162,20 @@ public class RegistryManager return getRegistry(name); } - @SuppressWarnings("unchecked") static > void registerToRootRegistry(ForgeRegistry forgeReg) { - WritableRegistry> registry = (WritableRegistry>) Registry.REGISTRY; + injectForgeRegistry(forgeReg, Registry.REGISTRY); + } + + static > void registerToBuiltinRegistry(ForgeRegistry forgeReg) + { + injectForgeRegistry(forgeReg, BuiltinRegistries.REGISTRY); + } + + @SuppressWarnings("unchecked") + private static > void injectForgeRegistry(ForgeRegistry forgeReg, Registry> rootRegistry) + { + WritableRegistry> registry = (WritableRegistry>) rootRegistry; Registry wrapper = forgeReg.getWrapper(); if (wrapper != null) registry.register(forgeReg.getRegistryKey(), wrapper, Lifecycle.experimental()); diff --git a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java index 2dd2d0d9a5..63a4f43dd1 100644 --- a/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java +++ b/src/main/java/net/minecraftforge/server/ServerLifecycleHooks.java @@ -11,10 +11,12 @@ import java.nio.file.Path; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; +import java.util.Set; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; +import net.minecraft.core.Registry; import net.minecraft.gametest.framework.GameTestServer; import net.minecraft.server.packs.repository.PackSource; import net.minecraft.server.packs.repository.RepositorySource; @@ -41,6 +43,7 @@ import net.minecraft.network.Connection; import net.minecraft.network.ConnectionProtocol; import net.minecraft.network.protocol.handshake.ClientIntentionPacket; import net.minecraft.network.protocol.login.ClientboundLoginDisconnectPacket; +import net.minecraft.resources.ResourceKey; import net.minecraft.server.packs.repository.Pack; import net.minecraft.server.MinecraftServer; import net.minecraft.network.chat.TextComponent; @@ -55,6 +58,7 @@ import net.minecraftforge.event.server.ServerStoppedEvent; import net.minecraftforge.event.server.ServerStoppingEvent; import net.minecraftforge.fml.loading.FileUtils; import net.minecraftforge.forgespi.language.IModInfo; +import net.minecraftforge.registries.DataPackRegistriesHooks; import net.minecraftforge.registries.GameData; public class ServerLifecycleHooks @@ -147,9 +151,25 @@ public class ServerLifecycleHooks final ConnectionType connectionType = ConnectionType.forVersionFlag(packet.getFMLVersion()); final int versionNumber = connectionType.getFMLVersionNumber(packet.getFMLVersion()); - if (connectionType == ConnectionType.MODDED && versionNumber != NetworkConstants.FMLNETVERSION) { - rejectConnection(manager, connectionType, "This modded server is not impl compatible with your modded client. Please verify your Forge version closely matches the server. Got net version "+ versionNumber + " this server is net version "+ NetworkConstants.FMLNETVERSION); - return false; + if (connectionType == ConnectionType.MODDED) + { + // Allow clients with incorrect netcode version to connect to netversion 3 servers, + // if and only if client is netversion 2 and server has no syncable non-vanilla datapack registries. + // TODO 1.19: Remove netcode backwards-compatability in 1.19, as there will be no clients on netversion 2 in 1.19. + Set>> customDatapackRegistries = DataPackRegistriesHooks.getSyncedCustomRegistries(); + if (versionNumber == 2) + { + if (!customDatapackRegistries.isEmpty()) + { + rejectConnection(manager, connectionType, "Missing required datapack registries: " + String.join(", ", customDatapackRegistries.stream().map(key -> key.location().toString()).toList())); + return false; + } + } + else if (versionNumber != NetworkConstants.FMLNETVERSION) + { + rejectConnection(manager, connectionType, "This modded server is not impl compatible with your modded client. Please verify your Forge version closely matches the server. Got net version "+ versionNumber + " this server is net version "+ NetworkConstants.FMLNETVERSION); + return false; + } } if (connectionType == ConnectionType.VANILLA && !NetworkRegistry.acceptsVanillaClientConnections()) { diff --git a/src/main/resources/assets/forge/lang/en_us.json b/src/main/resources/assets/forge/lang/en_us.json index 1c8300841b..7bbdcc378b 100644 --- a/src/main/resources/assets/forge/lang/en_us.json +++ b/src/main/resources/assets/forge/lang/en_us.json @@ -35,6 +35,7 @@ "fml.menu.multiplayer.extraservermods":"Server has additional mods that may be needed on the client", "fml.menu.multiplayer.modsincompatible":"Server mod list is not compatible", "fml.menu.multiplayer.networkincompatible":"Server network message list is not compatible", + "fml.menu.multiplayer.missingdatapackregistries":"Missing required datapack registries: {0}", "fml.menu.loadingmods": "{0,choice,0#No mods|1#1 mod|1<{0} mods} loaded", "fml.menu.notification.title": "Startup Notification", "fml.menu.accessdenied.title": "Server Access Denied", diff --git a/src/test/java/net/minecraftforge/debug/misc/DataPackRegistriesTest.java b/src/test/java/net/minecraftforge/debug/misc/DataPackRegistriesTest.java new file mode 100644 index 0000000000..c9a95670c8 --- /dev/null +++ b/src/test/java/net/minecraftforge/debug/misc/DataPackRegistriesTest.java @@ -0,0 +1,239 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ + +package net.minecraftforge.debug.misc; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Locale; + +import org.slf4j.Logger; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.mojang.logging.LogUtils; +import com.mojang.serialization.Codec; +import com.mojang.serialization.JsonOps; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.player.LocalPlayer; +import net.minecraft.core.Holder; +import net.minecraft.core.Registry; +import net.minecraft.core.RegistryAccess; +import net.minecraft.data.DataGenerator; +import net.minecraft.data.DataProvider; +import net.minecraft.data.HashCache; +import net.minecraft.resources.RegistryOps; +import net.minecraft.resources.ResourceKey; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.packs.PackType; +import net.minecraft.tags.TagKey; +import net.minecraftforge.api.distmarker.Dist; +import net.minecraftforge.common.MinecraftForge; +import net.minecraftforge.event.TagsUpdatedEvent; +import net.minecraftforge.event.server.ServerStartingEvent; +import net.minecraftforge.eventbus.api.IEventBus; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; +import net.minecraftforge.fml.loading.FMLEnvironment; +import net.minecraftforge.fml.util.thread.EffectiveSide; +import net.minecraftforge.forge.event.lifecycle.GatherDataEvent; +import net.minecraftforge.registries.DeferredRegister; +import net.minecraftforge.registries.ForgeRegistryEntry; +import net.minecraftforge.registries.RegistryBuilder; +import net.minecraftforge.registries.RegistryObject; + +/** + *

This test class shows an example of how to register unsynced and synced datapack registries, + * and how to use a dataprovider to generate json files for them. + * It also verifies that data and tags are loaded and synced correctly. + * Data is loaded from the following folders in test resources:

+ *
    + *
  • data/data_pack_registries_test/data_pack_registries_test/unsyncable/test.json
  • + *
  • data/data_pack_registries_test/tags/data_pack_registries_test/unsyncable/test.json
  • + *
  • data/data_pack_registries_test/data_pack_registries_test/syncable/test.json
  • + *
  • data/data_pack_registries_test/tags/data_pack_registries_test/syncable/test.json
  • + *
+ */ +@Mod(DataPackRegistriesTest.MODID) +public class DataPackRegistriesTest +{ + private static final boolean ENABLED = true; + public static final String MODID = "data_pack_registries_test"; + private static final Logger LOGGER = LogUtils.getLogger(); + private static final ResourceLocation TEST_RL = new ResourceLocation(MODID, "test"); + + private final RegistryObject datagenTestObject; + + public DataPackRegistriesTest() + { + if (!ENABLED) + return; + + final IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); + final IEventBus forgeBus = MinecraftForge.EVENT_BUS; + + // Deferred Registers can be created for datapack registries in static init or mod constructor. + // (We'll do it in mod constructor, as when doing the ENABLED check it's less verbose than static init.) + // As with static registries, any mod can make a Deferred Register for a given datapack registry, + // but only one mod can register the internal registry with makeRegistry. + final DeferredRegister unsyncables = DeferredRegister.create(Unsyncable.REGISTRY_KEY, MODID); + final DeferredRegister syncables = DeferredRegister.create(Syncable.REGISTRY_KEY, MODID); + + // RegistryBuilder#dataPackRegistry marks the registry as a datapack registry rather than a static registry. + unsyncables.makeRegistry(Unsyncable.class, + () -> new RegistryBuilder().disableSaving().dataPackRegistry(Unsyncable.DIRECT_CODEC)); + // The overload of #dataPackRegistry that takes a second codec marks the datapack registry as syncable. + syncables.makeRegistry(Syncable.class, + () -> new RegistryBuilder().disableSaving().dataPackRegistry(Syncable.DIRECT_CODEC, Syncable.DIRECT_CODEC)); + + // Datapack registry elements can be datagenerated, but they must be registered as builtin objects first. + this.datagenTestObject = unsyncables.register("datagen_test", () -> new Unsyncable("Datagen Success")); + + unsyncables.register(modBus); + syncables.register(modBus); + + modBus.addListener(this::onGatherData); + forgeBus.addListener(this::onServerStarting); + + if (FMLEnvironment.dist == Dist.CLIENT) + { + ClientEvents.subscribeClientEvents(); + } + } + + private void onGatherData(final GatherDataEvent event) + { + if (!event.includeServer()) + return; + // Example of how to datagen datapack registry objects. + // Objects to be datagenerated must be registered (e.g. via DeferredRegister above). + // This outputs to data/data_pack_registries_test/data_pack_registries_test/unsyncable/datagen_test.json + final DataGenerator generator = event.getGenerator(); + final Path outputFolder = generator.getOutputFolder(); + final RegistryOps ops = RegistryOps.create(JsonOps.INSTANCE, RegistryAccess.BUILTIN.get()); + final Gson gson = new GsonBuilder().setPrettyPrinting().create(); + final ResourceLocation registryId = Unsyncable.REGISTRY_KEY.location(); + final ResourceLocation id = this.datagenTestObject.getId(); + final Unsyncable element = this.datagenTestObject.get(); + final String pathString = String.join("/", PackType.SERVER_DATA.getDirectory(), id.getNamespace(), registryId.getNamespace(), registryId.getPath(), id.getPath()+".json"); + final Path path = outputFolder.resolve(pathString); + generator.addProvider(new DataProvider() + { + @Override + public void run(final HashCache cache) throws IOException + { + Unsyncable.DIRECT_CODEC.encodeStart(ops, element) + .resultOrPartial(msg -> LOGGER.error("Failed to encode {}: {}", path, msg)) // Log error on encode failure. + .ifPresent(json -> // Output to file on encode success. + { + try + { + DataProvider.save(gson, cache, json, path); + } + catch (IOException e) // The throws can't deal with this exception, because we're inside the ifPresent. + { + LOGGER.error("Failed to save " + pathString, e); + } + }); + } + + @Override + public String getName() + { + return String.format("%s provider for %s", registryId, MODID); + } + }); + } + + private void onServerStarting(final ServerStartingEvent event) + { + // Assert existence of json objects and tags. + final RegistryAccess registries = event.getServer().registryAccess(); + final Registry registry = registries.registryOrThrow(Unsyncable.REGISTRY_KEY); + final ResourceKey key = ResourceKey.create(Unsyncable.REGISTRY_KEY, TEST_RL); + final Holder holder = registry.getHolderOrThrow(key); + final Unsyncable testObject = registry.get(TEST_RL); + if (!testObject.value().equals("success")) + throw new IllegalStateException("Incorrect value loaded: " + testObject.value()); + final TagKey tag = TagKey.create(Unsyncable.REGISTRY_KEY, TEST_RL); + if (!registry.getTag(tag).get().contains(holder)) + throw new IllegalStateException(String.format(Locale.ENGLISH, "Tag %s does not contain %s", tag, TEST_RL)); + + LOGGER.info("DataPackRegistriesTest server data loaded successfully!"); + } + + public static class ClientEvents + { + private static void subscribeClientEvents() + { + MinecraftForge.EVENT_BUS.addListener(ClientEvents::onClientTagsUpdated); + } + + private static void onClientTagsUpdated(final TagsUpdatedEvent event) + { + // We want to check whether tags have been synced after the player logs in. + // Tags are synced late in the login process and many relevant events fire before tags are synced. + // TagsUpdatedEvent has the correct timing, but fires on both server and render thread, + // so we need to make sure we're on the render thread. + LocalPlayer player = Minecraft.getInstance().player; + if (player == null || EffectiveSide.get().isServer()) + return; + + // Assert existence of synced objects and tags. The TagsUpdatedEvent has its own RegistryAccess, + // but we should check the player's connection's RegistryAccess as that's where the client's copy + // lives during game runtime, and where mods should be querying on the client in most cases. + RegistryAccess registries = player.connection.registryAccess(); + final Registry registry = registries.registryOrThrow(Syncable.REGISTRY_KEY); + final ResourceKey key = ResourceKey.create(Syncable.REGISTRY_KEY, TEST_RL); + final Holder holder = registry.getHolderOrThrow(key); + final Syncable testObject = registry.get(TEST_RL); + if (!testObject.value().equals("success")) + throw new IllegalStateException("Incorrect value synced: " + testObject.value()); + final TagKey tag = TagKey.create(Syncable.REGISTRY_KEY, TEST_RL); + if (!registry.getTag(tag).get().contains(holder)) + throw new IllegalStateException(String.format(Locale.ENGLISH, "Tag %s does not contain %s", tag, TEST_RL)); + + LOGGER.info("DataPackRegistriesTest client data synced successfully!"); + } + } + + public static class Unsyncable extends ForgeRegistryEntry + { + public static final ResourceKey> REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation(MODID, "unsyncable")); + public static final Codec DIRECT_CODEC = Codec.STRING.fieldOf("value").codec().xmap(Unsyncable::new, Unsyncable::value); + + private final String value; + + public Unsyncable(final String stringValue) + { + this.value = stringValue; + } + + public String value() + { + return this.value; + } + } + + public static class Syncable extends ForgeRegistryEntry + { + public static final ResourceKey> REGISTRY_KEY = ResourceKey.createRegistryKey(new ResourceLocation(MODID, "syncable")); + public static final Codec DIRECT_CODEC = Codec.STRING.fieldOf("value").codec().xmap(Syncable::new, Syncable::value); + + private final String value; + + public Syncable(final String value) + { + this.value = value; + } + + public String value() + { + return this.value; + } + } +} \ No newline at end of file diff --git a/src/test/resources/META-INF/mods.toml b/src/test/resources/META-INF/mods.toml index fbe3785e97..1b9cc294ea 100644 --- a/src/test/resources/META-INF/mods.toml +++ b/src/test/resources/META-INF/mods.toml @@ -224,5 +224,7 @@ license="LGPL v2.1" modId="custom_transformtype_test" [[mods]] modId="custom_rarity_test" +[[mods]] + modId="data_pack_registries_test" # ADD ABOVE THIS LINE diff --git a/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/syncable/test.json b/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/syncable/test.json new file mode 100644 index 0000000000..40cbca4dfd --- /dev/null +++ b/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/syncable/test.json @@ -0,0 +1,3 @@ +{ + "value": "success" +} \ No newline at end of file diff --git a/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/test.json b/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/test.json new file mode 100644 index 0000000000..40cbca4dfd --- /dev/null +++ b/src/test/resources/data/data_pack_registries_test/data_pack_registries_test/unsyncable/test.json @@ -0,0 +1,3 @@ +{ + "value": "success" +} \ No newline at end of file diff --git a/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/syncable/test.json b/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/syncable/test.json new file mode 100644 index 0000000000..23584f44fb --- /dev/null +++ b/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/syncable/test.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "data_pack_registries_test:test" + ] +} \ No newline at end of file diff --git a/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/unsyncable/test.json b/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/unsyncable/test.json new file mode 100644 index 0000000000..23584f44fb --- /dev/null +++ b/src/test/resources/data/data_pack_registries_test/tags/data_pack_registries_test/unsyncable/test.json @@ -0,0 +1,6 @@ +{ + "replace": false, + "values": [ + "data_pack_registries_test:test" + ] +} \ No newline at end of file