diff --git a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicMetaRegistry.java b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicMetaRegistry.java index d5a5337240..bafe264f3a 100644 --- a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicMetaRegistry.java +++ b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicMetaRegistry.java @@ -36,6 +36,57 @@ * This registry will be frozen at the same time as static registries. */ public final class DynamicMetaRegistry { + /** + * Registers a server-side dynamic registry. + *

+ * Entries will be loaded from {@code "data///"} for every datapack + * {@code namespace}, where {@code registry_namespace} and {@code registry_path}'s values are respectively + * {@code key.getLocation().getNamespace()} and {@code key.getLocation().getPath()}. + * + * @param the type of elements in the dynamic registry + * @param key a {@link RegistryKey#ofRegistry(Identifier) key for the new dynamic registry} + * @param entryCodec the codec used to deserialize entries from datapacks + * @throws IllegalStateException if this registry of registries already got frozen + */ + public static void register(RegistryKey> key, Codec entryCodec, DynamicRegistryFlag... flags) { + DynamicMetaRegistryImpl.register(key, entryCodec, flags); + } + + /** + * Registers a dynamic registry which contents get synced between the server and connected clients. + *

+ * Entries will be loaded from {@code "data///"} for every datapack + * {@code namespace}, where {@code registry_namespace} and {@code registry_path}'s values are respectively + * {@code key.getLocation().getNamespace()} and {@code key.getLocation().getPath()}. + * + * @param the type of elements in the dynamic registry + * @param key a {@link RegistryKey#ofRegistry(Identifier) key for the new dynamic registry} + * @param entryCodec the codec used to both deserialize entries from datapacks and (de)serialize entries to and from packets + * @throws IllegalStateException if this registry of registries already got frozen + * @see #registerSynced(RegistryKey, Codec, Codec, DynamicRegistryFlag...) + */ + public static void registerSynced(RegistryKey> key, Codec entryCodec, DynamicRegistryFlag... flags) { + DynamicMetaRegistryImpl.registerSynced(key, entryCodec, entryCodec, flags); + } + + /** + * Registers a dynamic registry which contents get synced between the server and connected clients. + *

+ * Entries will be loaded from {@code "data///"} for every datapack + * {@code namespace}, where {@code registry_namespace} and {@code registry_path}'s values are respectively + * {@code key.getLocation().getNamespace()} and {@code key.getLocation().getPath()}. + * + * @param the type of elements in the dynamic registry + * @param key a {@link RegistryKey#ofRegistry(Identifier) key for the new dynamic registry} + * @param entryCodec the codec used to deserialize entries from datapacks + * @param syncCodec the codec used to (de)serialize entries to and from packets - may be the same as {@code entryCodec} + * @throws IllegalStateException if this registry of registries already got frozen + * @see #registerSynced(RegistryKey, Codec, DynamicRegistryFlag...) + */ + public static void registerSynced(RegistryKey> key, Codec entryCodec, Codec syncCodec, DynamicRegistryFlag... flags) { + DynamicMetaRegistryImpl.registerSynced(key, entryCodec, syncCodec, flags); + } + /** * Registers a server-side dynamic registry. *

diff --git a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicRegistryFlag.java b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicRegistryFlag.java new file mode 100644 index 0000000000..dc9b3a56d2 --- /dev/null +++ b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/api/dynamic/DynamicRegistryFlag.java @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Quilt Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.registry.api.dynamic; + +import net.minecraft.registry.RegistryKey; +import net.minecraft.util.Identifier; + +import org.quiltmc.qsl.registry.impl.dynamic.DynamicMetaRegistryImpl; +import org.quiltmc.qsl.registry.impl.dynamic.DynamicRegistryFlagManager; + +/** + * Flags that can be set on dynamic registries to define their behavior. + *

+ * All flags are off by default, and can be enabled using static methods in this class or on dynamic registry creation via varargs. + * + * @see org.quiltmc.qsl.registry.api.sync.RegistrySynchronization org.quiltmc.qsl.registry.api.sync.RegistrySynchronization, + * which contains similar flag setters/getters for static registries + */ +public enum DynamicRegistryFlag { + /** + * Indicates that this registry (and the entries within) do not necessarily need to be sent + * to (logical) clients for synchronization in multiplayer contexts. + *

+ * Note: This flag is intended only for synchronized dynamic registries. On non-synced dynamic registries, this flag does nothing. + *

+ * One use-case for this flag is for creating mods that are entirely compatible with vanilla, and thus do not + * require the dynamic registry to exist clientside to connect to the server. + * This allows for both vanilla clients/clients without the mod to connect and for clients with the mod + * supplying the registry to connect, with the latter being able to see the contents of the registry and possibly + * enable extra clientside features accordingly. + */ + OPTIONAL; + + /** + * Enables a specific flag on a dynamic registry. + * + * @param registryId the value id ({@link RegistryKey#getValue()}) of the target dynamic registry + * @param flag the flag value to enable on the dynamic registry + */ + public static void setFlag(Identifier registryId, DynamicRegistryFlag flag) { + try { + DynamicRegistryFlagManager.setFlag(registryId, flag); + } catch (Exception e) { + logFlagModifyException(registryId, flag, e); + } + } + + /** + * Checks if a dynamic registry has the {@link DynamicRegistryFlag#OPTIONAL} flag enabled on it. + * + * @param registryId the value id ({@link RegistryKey#getValue()}) of the dynamic registry to check + * @return whether the checked dynamic registry has the {@link DynamicRegistryFlag#OPTIONAL} flag enabled + */ + public static boolean isOptional(Identifier registryId) { + return DynamicRegistryFlagManager.isOptional(registryId); + } + + /** + * Helper method for logging exceptions to avoid code duplication. + */ + private static void logFlagModifyException(Identifier registryId, DynamicRegistryFlag flag, Exception e) { + DynamicMetaRegistryImpl.LOGGER.error("Caught exception while attempting to enable flag {} on registry id {}: {}", flag.toString(), registryId, e.toString()); + } +} diff --git a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicMetaRegistryImpl.java b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicMetaRegistryImpl.java index 34721eaa8b..b6304e1548 100644 --- a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicMetaRegistryImpl.java +++ b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicMetaRegistryImpl.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableMap; import com.mojang.serialization.Codec; import org.jetbrains.annotations.ApiStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.minecraft.registry.Registry; import net.minecraft.registry.RegistryKey; @@ -29,24 +31,30 @@ import net.minecraft.util.Identifier; import org.quiltmc.qsl.registry.mixin.DynamicRegistrySyncAccessor; +import org.quiltmc.qsl.registry.api.dynamic.DynamicRegistryFlag; @ApiStatus.Internal public class DynamicMetaRegistryImpl { private static boolean frozen; private static final Set MODDED_REGISTRY_IDS = new HashSet<>(); + public static final Logger LOGGER = LoggerFactory.getLogger("quilt_dynamic_registries"); + public static boolean isModdedRegistryId(Identifier id) { return MODDED_REGISTRY_IDS.contains(id); } - public static void register(RegistryKey> ref, Codec entryCodec) { + public static void register(RegistryKey> ref, Codec entryCodec, DynamicRegistryFlag... flags) { if (frozen) throw new IllegalStateException("Registry is already frozen"); MODDED_REGISTRY_IDS.add(ref.getValue()); RegistryLoader.WORLDGEN_REGISTRIES.add(new RegistryLoader.DecodingData<>(ref, entryCodec)); + for (DynamicRegistryFlag flag : flags) { + DynamicRegistryFlagManager.setFlag(ref.getValue(), flag); + } } - public static void registerSynced(RegistryKey> ref, Codec entryCodec, Codec syncCodec) { - register(ref, entryCodec); + public static void registerSynced(RegistryKey> ref, Codec entryCodec, Codec syncCodec, DynamicRegistryFlag... flags) { + register(ref, entryCodec, flags); var builder = ImmutableMap.>, Object>builder().putAll(DynamicRegistrySyncAccessor.quilt$getSyncedCodecs()); DynamicRegistrySyncAccessor.quilt$invokeAddSyncedRegistry(builder, ref, syncCodec); DynamicRegistrySyncAccessor.quilt$setSyncedCodecs(builder.build()); @@ -55,4 +63,8 @@ public static void registerSynced(RegistryKey> ref, Co public static void freeze() { frozen = true; } + + static boolean isFrozen() { + return frozen; + } } diff --git a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicRegistryFlagManager.java b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicRegistryFlagManager.java new file mode 100644 index 0000000000..a92f7a6ddd --- /dev/null +++ b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/impl/dynamic/DynamicRegistryFlagManager.java @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Quilt Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.quiltmc.qsl.registry.impl.dynamic; + +import org.jetbrains.annotations.ApiStatus; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; + +import net.minecraft.util.Identifier; + +import org.quiltmc.qsl.registry.api.dynamic.DynamicRegistryFlag; + +@ApiStatus.Internal +public final class DynamicRegistryFlagManager { + private static final Multimap DYNAMIC_REGISTRY_FLAGS = + MultimapBuilder.hashKeys().enumSetValues(DynamicRegistryFlag.class).build(); + + public static void setFlag(Identifier registryId, DynamicRegistryFlag flag) throws IllegalStateException { + if (DynamicMetaRegistryImpl.isFrozen()) { + throw new IllegalStateException("Dynamic registries are frozen, and thus flags cannot be changed!"); + } + + if (!DynamicMetaRegistryImpl.isModdedRegistryId(registryId)) return; + + DYNAMIC_REGISTRY_FLAGS.put(registryId, flag); + } + + public static boolean isOptional(Identifier registryId) { + if (DYNAMIC_REGISTRY_FLAGS.containsKey(registryId)) { + return DYNAMIC_REGISTRY_FLAGS.get(registryId).contains(DynamicRegistryFlag.OPTIONAL); + } + + return false; + } +} diff --git a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/mixin/DynamicRegistrySyncMixin.java b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/mixin/DynamicRegistrySyncMixin.java index 30e643e9c6..20f1c4e415 100644 --- a/library/core/registry/src/main/java/org/quiltmc/qsl/registry/mixin/DynamicRegistrySyncMixin.java +++ b/library/core/registry/src/main/java/org/quiltmc/qsl/registry/mixin/DynamicRegistrySyncMixin.java @@ -1,4 +1,5 @@ /* + * Copyright 2016, 2017, 2018, 2019 FabricMC * Copyright 2023 The Quilt Project * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -17,16 +18,24 @@ package org.quiltmc.qsl.registry.mixin; import java.util.Map; +import java.util.stream.Stream; +import org.spongepowered.asm.mixin.Dynamic; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mutable; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; +import net.minecraft.registry.DynamicRegistryManager; import net.minecraft.registry.DynamicRegistrySync; import net.minecraft.registry.Registry; import net.minecraft.registry.RegistryKey; +import org.quiltmc.qsl.registry.impl.dynamic.DynamicRegistryFlagManager; + @Mixin(DynamicRegistrySync.class) public abstract class DynamicRegistrySyncMixin { @SuppressWarnings("unused") // makes the field mutable for use by the accessor @@ -34,4 +43,28 @@ public abstract class DynamicRegistrySyncMixin { @Final @Mutable private static Map>, ?> SYNCED_CODECS; + @Unique + private static boolean filterRegistryEntry(DynamicRegistryManager.RegistryEntry entry) { + // OPTIONAL + if (DynamicRegistryFlagManager.isOptional(entry.key().getValue())) { + return entry.value().size() > 0; + } + + return true; // If no flags apply, always return true + } + + @Shadow + private static Stream> streamSyncedRegistries(DynamicRegistryManager registryManager) { + throw new IllegalStateException("Mixin injection failed."); + } + + /** + * This redirect mixin's annotation was taken directly from Fabric API (and adapted for Quilt Mappings), the rest of this file was not. + */ + @Dynamic("method_45961: Codec.xmap in buildManagerCodec") + @Redirect(method = "method_45961", + at = @At(value = "INVOKE", target = "Lnet/minecraft/registry/DynamicRegistrySync;streamSyncedRegistries(Lnet/minecraft/registry/DynamicRegistryManager;)Ljava/util/stream/Stream;")) + private static Stream> filterNonSyncedEntries(DynamicRegistryManager drm) { + return streamSyncedRegistries(drm).filter(DynamicRegistrySyncMixin::filterRegistryEntry); + } } diff --git a/library/core/registry/src/testmod/java/org/quiltmc/qsl/registry/test/dynamic/RegistryLibDynamicRegistryTest.java b/library/core/registry/src/testmod/java/org/quiltmc/qsl/registry/test/dynamic/RegistryLibDynamicRegistryTest.java index 945fbbc1d3..149a647882 100644 --- a/library/core/registry/src/testmod/java/org/quiltmc/qsl/registry/test/dynamic/RegistryLibDynamicRegistryTest.java +++ b/library/core/registry/src/testmod/java/org/quiltmc/qsl/registry/test/dynamic/RegistryLibDynamicRegistryTest.java @@ -34,6 +34,7 @@ import org.quiltmc.loader.api.ModContainer; import org.quiltmc.qsl.base.api.entrypoint.ModInitializer; import org.quiltmc.qsl.registry.api.dynamic.DynamicMetaRegistry; +import org.quiltmc.qsl.registry.api.dynamic.DynamicRegistryFlag; import org.quiltmc.qsl.registry.api.event.RegistryEvents; import org.quiltmc.qsl.tag.api.TagRegistry; import org.quiltmc.qsl.tag.api.TagRegistry.TagValues; @@ -49,7 +50,7 @@ public class RegistryLibDynamicRegistryTest implements QuiltGameTest, ModInitial @Override public void onInitialize(ModContainer mod) { - DynamicMetaRegistry.registerSynced(Greetings.REGISTRY_KEY, Greetings.CODEC); + DynamicMetaRegistry.registerSynced(Greetings.REGISTRY_KEY, Greetings.CODEC, DynamicRegistryFlag.OPTIONAL); RegistryEvents.DYNAMIC_REGISTRY_SETUP.register(context -> context.register(Greetings.REGISTRY_KEY, GREETING_B_ID, () -> GREETING_B)); } @@ -58,6 +59,7 @@ public void greetingsGetLoaded(QuiltTestContext ctx) { Registry greetingsRegistry = ctx.getWorld().getRegistryManager().get(Greetings.REGISTRY_KEY); ctx.succeedIf(() -> { + ctx.assertTrue(DynamicRegistryFlag.isOptional(Greetings.REGISTRY_KEY.getValue()), "Registry should always have the OPTIONAL flag enabled"); ctx.assertTrue(greetingsRegistry.containsId(GREETING_A_ID), "Registry should contain modded data value from datapack"); ctx.assertTrue(Objects.requireNonNull(greetingsRegistry.get(GREETING_A_ID)).equals(GREETING_A), "Modded value should be properly parsed from data file"); ctx.assertTrue(GREETING_B.equals(greetingsRegistry.get(GREETING_B_ID)), "Registry should contain modded data value from event");