diff --git a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/api/event/registry/FabricRegistry.java b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/api/event/registry/FabricRegistry.java new file mode 100644 index 0000000000..1021921e09 --- /dev/null +++ b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/api/event/registry/FabricRegistry.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.api.event.registry; + +import org.jetbrains.annotations.ApiStatus; + +import net.minecraft.registry.Registry; +import net.minecraft.util.Identifier; + +/** + * General-purpose Fabric-provided extensions for {@link Registry} objects. + * + *

Note: This interface is automatically implemented on all registries via Mixin and interface injection.

+ */ +@ApiStatus.NonExtendable +public interface FabricRegistry { + /** + * Adds an alias for an entry in this registry. Once added, all queries to this registry that refer to the {@code old} + * {@link Identifier} will be redirected towards {@code newId}. This is useful if a mod wants to change an ID without + * breaking compatibility with existing worlds. + * @param old the {@link Identifier} that will become an alias for {@code newId} + * @param newId the {@link Identifier} for which {@code old} will become an alias + */ + default void addAlias(Identifier old, Identifier newId) { + throw new UnsupportedOperationException("implemented via mixin"); + } +} diff --git a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/RegistryMixin.java b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/RegistryMixin.java new file mode 100644 index 0000000000..4aeeb24a70 --- /dev/null +++ b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/RegistryMixin.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.mixin.registry.sync; + +import org.spongepowered.asm.mixin.Mixin; + +import net.minecraft.registry.Registry; + +import net.fabricmc.fabric.api.event.registry.FabricRegistry; + +@Mixin(Registry.class) +public interface RegistryMixin extends FabricRegistry { +} diff --git a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/SimpleRegistryMixin.java b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/SimpleRegistryMixin.java index 7f678d5a8a..54f4ef6fa2 100644 --- a/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/SimpleRegistryMixin.java +++ b/fabric-registry-sync-v0/src/main/java/net/fabricmc/fabric/mixin/registry/sync/SimpleRegistryMixin.java @@ -18,8 +18,10 @@ import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -43,6 +45,7 @@ import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; @@ -56,6 +59,7 @@ import net.fabricmc.fabric.api.event.Event; import net.fabricmc.fabric.api.event.EventFactory; +import net.fabricmc.fabric.api.event.registry.FabricRegistry; import net.fabricmc.fabric.api.event.registry.RegistryAttribute; import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder; import net.fabricmc.fabric.api.event.registry.RegistryEntryAddedCallback; @@ -67,7 +71,7 @@ import net.fabricmc.fabric.impl.registry.sync.RemappableRegistry; @Mixin(SimpleRegistry.class) -public abstract class SimpleRegistryMixin implements MutableRegistry, RemappableRegistry, ListenableRegistry { +public abstract class SimpleRegistryMixin implements MutableRegistry, RemappableRegistry, ListenableRegistry, FabricRegistry { // Namespaces used by the vanilla game. "brigadier" is used by command argument type registry. // While Realms use "realms" namespace, it is irrelevant for Registry Sync. @Unique @@ -108,6 +112,22 @@ public abstract class SimpleRegistryMixin implements MutableRegistry, Rema private Object2IntMap fabric_prevIndexedEntries; @Unique private BiMap> fabric_prevEntries; + @Unique + // invariant: the sets of keys and values are disjoint (every alias points to a 'deepest' non-alias ID) + private Map aliases = new HashMap<>(); + + @Shadow + public abstract boolean containsId(Identifier id); + + @Shadow + public abstract String toString(); + + @Shadow + @Final + private RegistryKey> key; + + @Shadow + protected abstract void assertNotFrozen(); @Override public Event> fabric_getAddObjectEvent() { @@ -128,6 +148,18 @@ private void init(RegistryKey key, Lifecycle lifecycle, boolean intrusive, Ca } } ); + // aliasing: check that no new entries use the id of an alias + fabric_addObjectEvent.register((rawId, id, object) -> { + if (aliases.containsKey(id)) { + throw new IllegalArgumentException( + "Tried registering %s to registry %s, but it is already an alias (for %s)".formatted( + id, + this.key, + aliases.get(id) + ) + ); + } + }); fabric_postRemapEvent = EventFactory.createArrayBacked(RegistryIdRemapCallback.class, (callbacks) -> (a) -> { for (RegistryIdRemapCallback callback : callbacks) { @@ -171,7 +203,7 @@ public void remap(Object2IntMap remoteIndexedEntries, RemapMode mode List strings = null; for (Identifier remoteId : remoteIndexedEntries.keySet()) { - if (!idToEntry.containsKey(remoteId)) { + if (!this.containsId(remoteId)) { if (strings == null) { strings = new ArrayList<>(); } @@ -352,4 +384,94 @@ public void unmap() throws RemapException { fabric_prevEntries = null; } } + + @Override + public void addAlias(Identifier old, Identifier newId) { + Objects.requireNonNull(old, "alias cannot be null"); + Objects.requireNonNull(newId, "aliased id cannot be null"); + + if (aliases.containsKey(old)) { + throw new IllegalArgumentException( + "Tried adding %s as an alias for %s, but it is already an alias (for %s) in registry %s".formatted( + old, + newId, + aliases.get(old), + this.key + ) + ); + } + + if (this.idToEntry.containsKey(old)) { + throw new IllegalArgumentException( + "Tried adding %s as an alias, but it is already present in registry %s".formatted( + old, + this.key + ) + ); + } + + if (old.equals(aliases.get(newId))) { + // since an alias corresponds to at most one identifier, this is the only way to create a cycle + // that doesn't already fall under the first condition + throw new IllegalArgumentException( + "Making %1$s an alias of %2$s would create a cycle, as %2$s is already an alias of %1$s (registry %3$s)".formatted( + old, + newId, + this.key + ) + ); + } + + if (!this.idToEntry.containsKey(newId)) { + FABRIC_LOGGER.warn( + "Adding {} as an alias for {}, but the latter doesn't exist in registry {}", + old, + newId, + this.key + ); + } + + assertNotFrozen(); + + // recompute alias map to preserve invariant, i.e. make sure all keys point to a non-alias ID + Identifier deepest = aliases.getOrDefault(newId, newId); + + for (Map.Entry entry : aliases.entrySet()) { + if (old.equals(entry.getValue())) { + entry.setValue(deepest); + } + } + + aliases.put(old, deepest); + FABRIC_LOGGER.debug("Adding alias {} for {} in registry {}", old, newId, this.key); + } + + @ModifyVariable( + method = { + "getEntry(Lnet/minecraft/util/Identifier;)Ljava/util/Optional;", + "get(Lnet/minecraft/util/Identifier;)Ljava/lang/Object;", + "containsId" + }, + at = @At("HEAD"), + argsOnly = true + ) + private Identifier aliasIdentifierParameter(Identifier original) { + return aliases.getOrDefault(original, original); + } + + @ModifyVariable( + method = { + "get(Lnet/minecraft/registry/RegistryKey;)Ljava/lang/Object;", + "getOptional(Lnet/minecraft/registry/RegistryKey;)Ljava/util/Optional;", + "getOrCreateEntry", + "contains", + "getEntryInfo" + }, + at = @At("HEAD"), + argsOnly = true + ) + private RegistryKey aliasRegistryKeyParameter(RegistryKey original) { + Identifier aliased = aliases.get(original.getValue()); + return aliased == null ? original : RegistryKey.of(original.getRegistryRef(), aliased); + } } diff --git a/fabric-registry-sync-v0/src/main/resources/fabric-registry-sync-v0.mixins.json b/fabric-registry-sync-v0/src/main/resources/fabric-registry-sync-v0.mixins.json index d8d12369ef..c51948bd22 100644 --- a/fabric-registry-sync-v0/src/main/resources/fabric-registry-sync-v0.mixins.json +++ b/fabric-registry-sync-v0/src/main/resources/fabric-registry-sync-v0.mixins.json @@ -14,6 +14,7 @@ "RegistriesMixin", "RegistryKeysMixin", "RegistryLoaderMixin", + "RegistryMixin", "SaveLoadingMixin", "SerializableRegistriesMixin", "SimpleRegistryAccessor", diff --git a/fabric-registry-sync-v0/src/main/resources/fabric.mod.json b/fabric-registry-sync-v0/src/main/resources/fabric.mod.json index f7b3ca53d6..f0ccb6ca02 100644 --- a/fabric-registry-sync-v0/src/main/resources/fabric.mod.json +++ b/fabric-registry-sync-v0/src/main/resources/fabric.mod.json @@ -38,6 +38,9 @@ }, "accessWidener": "fabric-registry-sync-v0.accesswidener", "custom": { - "fabric-api:module-lifecycle": "stable" + "fabric-api:module-lifecycle": "stable", + "loom:injected_interfaces": { + "net/minecraft/class_2378": ["net/fabricmc/fabric/api/event/registry/FabricRegistry"] + } } } diff --git a/fabric-registry-sync-v0/src/test/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java b/fabric-registry-sync-v0/src/test/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java new file mode 100644 index 0000000000..346c09914f --- /dev/null +++ b/fabric-registry-sync-v0/src/test/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.test.registry.sync; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import net.minecraft.Bootstrap; +import net.minecraft.SharedConstants; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.util.Identifier; + +import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder; + +public class RegistryAliasTest { + private static final Identifier OBSOLETE_ID = id("obsolete"); + private static final Identifier NEW_ID = id("new"); + private static final Identifier OTHER = id("other"); + private RegistryKey> testRegistryKey; + private Registry testRegistry; + + @BeforeAll + static void beforeAll() { + SharedConstants.createGameVersion(); + Bootstrap.initialize(); + } + + private static Identifier id(String s) { + return Identifier.of("registry_sync_test_alias_test", s); + } + + @BeforeEach + void beforeEach() { + testRegistryKey = RegistryKey.ofRegistry(id(UUID.randomUUID().toString())); + testRegistry = Mockito.spy(FabricRegistryBuilder.createSimple(testRegistryKey).buildAndRegister()); + + Registry.register(testRegistry, NEW_ID, "entry"); + Registry.register(testRegistry, OTHER, "other"); + testRegistry.addAlias(OBSOLETE_ID, NEW_ID); + } + + @Test + void testAlias() { + RegistryKey obsoleteKey = RegistryKey.of(testRegistryKey, OBSOLETE_ID); + + assertTrue(testRegistry.containsId(OBSOLETE_ID)); + assertFalse(testRegistry.getIds().contains(OBSOLETE_ID)); + assertEquals("entry", testRegistry.get(OBSOLETE_ID)); + assertEquals("entry", testRegistry.get(obsoleteKey)); + + Identifier moreObsolete = id("more_obsolete"); + assertFalse(testRegistry.containsId(moreObsolete)); + + testRegistry.addAlias(moreObsolete, OBSOLETE_ID); + + assertTrue(testRegistry.containsId(moreObsolete)); + assertEquals("entry", testRegistry.get(moreObsolete)); + } + + @Test + void forbidAmbiguousAlias() { + assertThrows(IllegalArgumentException.class, () -> testRegistry.addAlias(OBSOLETE_ID, OTHER)); + } + + @Test + void forbidCircularAliases() { + assertThrows(IllegalArgumentException.class, () -> testRegistry.addAlias(NEW_ID, OBSOLETE_ID)); + } + + @Test + void forbidExistingIdAsAlias() { + assertThrows(IllegalArgumentException.class, () -> testRegistry.addAlias(NEW_ID, OTHER)); + } + + @Test + void forbidOverridingAliasWithEntry() { + assertThrows(IllegalArgumentException.class, () -> Registry.register(testRegistry, OBSOLETE_ID, "obsolete")); + } +} diff --git a/fabric-registry-sync-v0/src/testmod/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java b/fabric-registry-sync-v0/src/testmod/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java new file mode 100644 index 0000000000..2f35e2da2a --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/java/net/fabricmc/fabric/test/registry/sync/RegistryAliasTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2016, 2017, 2018, 2019 FabricMC + * + * 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 net.fabricmc.fabric.test.registry.sync; + +import com.mojang.logging.LogUtils; +import org.slf4j.Logger; + +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.Block; +import net.minecraft.item.BlockItem; +import net.minecraft.item.Item; +import net.minecraft.registry.Registries; +import net.minecraft.registry.Registry; +import net.minecraft.registry.RegistryKey; +import net.minecraft.registry.RegistryKeys; +import net.minecraft.util.Identifier; + +import net.fabricmc.api.ModInitializer; + +public class RegistryAliasTest implements ModInitializer { + private static final Logger LOGGER = LogUtils.getLogger(); + public static final boolean USE_OLD_IDS = Boolean.parseBoolean(System.getProperty("fabric.registry.sync.test.alias.use_old_ids", "true")); + public static final Identifier OLD_TEST_INGOT = id("test_ingot_old"); + public static final Identifier TEST_INGOT = id("test_ingot"); + public static final Identifier OLD_TEST_BLOCK = id("test_block_old"); + public static final Identifier TEST_BLOCK = id("test_block"); + + @Override + public void onInitialize() { + if (USE_OLD_IDS) { + LOGGER.info("Registering old IDs"); + register(OLD_TEST_BLOCK, OLD_TEST_INGOT); + } else { + LOGGER.info("Registering new IDs"); + register(TEST_BLOCK, TEST_INGOT); + LOGGER.info("Adding aliases"); + Registries.BLOCK.addAlias(OLD_TEST_BLOCK, TEST_BLOCK); + Registries.ITEM.addAlias(OLD_TEST_BLOCK, TEST_BLOCK); + Registries.ITEM.addAlias(OLD_TEST_INGOT, TEST_INGOT); + } + + Registries.ITEM.addAlias(Identifier.of("old_stone"), Identifier.of("stone")); + } + + private static void register(Identifier blockId, Identifier itemId) { + Block block = new Block(AbstractBlock.Settings.create().registryKey(RegistryKey.of(RegistryKeys.BLOCK, blockId))); + Registry.register(Registries.BLOCK, blockId, block); + BlockItem blockItem = new BlockItem(block, new Item.Settings().registryKey(RegistryKey.of(RegistryKeys.ITEM, blockId))); + Registry.register(Registries.ITEM, blockId, blockItem); + Item item = new Item(new Item.Settings().registryKey(RegistryKey.of(RegistryKeys.ITEM, itemId))); + Registry.register(Registries.ITEM, itemId, item); + } + + private static Identifier id(String path) { + return Identifier.of("registry_sync_alias_test", path); + } +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block.json new file mode 100644 index 0000000000..cb3897dc29 --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block.json @@ -0,0 +1,3 @@ +{ + "parent": "minecraft:block/iron_block" +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block_old.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block_old.json new file mode 100644 index 0000000000..cb3897dc29 --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/block/test_block_old.json @@ -0,0 +1,3 @@ +{ + "parent": "minecraft:block/iron_block" +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block.json new file mode 100644 index 0000000000..cb3897dc29 --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block.json @@ -0,0 +1,3 @@ +{ + "parent": "minecraft:block/iron_block" +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block_old.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block_old.json new file mode 100644 index 0000000000..cb3897dc29 --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_block_old.json @@ -0,0 +1,3 @@ +{ + "parent": "minecraft:block/iron_block" +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot.json new file mode 100644 index 0000000000..eea043008a --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "minecraft:item/copper_ingot" + } +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot_old.json b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot_old.json new file mode 100644 index 0000000000..eea043008a --- /dev/null +++ b/fabric-registry-sync-v0/src/testmod/resources/assets/fabric-registry-sync-v0-testmod/models/item/test_ingot_old.json @@ -0,0 +1,6 @@ +{ + "parent": "minecraft:item/handheld", + "textures": { + "layer0": "minecraft:item/copper_ingot" + } +} diff --git a/fabric-registry-sync-v0/src/testmod/resources/fabric.mod.json b/fabric-registry-sync-v0/src/testmod/resources/fabric.mod.json index 486e0bba14..0ed2f135fe 100644 --- a/fabric-registry-sync-v0/src/testmod/resources/fabric.mod.json +++ b/fabric-registry-sync-v0/src/testmod/resources/fabric.mod.json @@ -14,7 +14,8 @@ "entrypoints": { "main": [ "net.fabricmc.fabric.test.registry.sync.CustomDynamicRegistryTest", - "net.fabricmc.fabric.test.registry.sync.RegistrySyncTest" + "net.fabricmc.fabric.test.registry.sync.RegistrySyncTest", + "net.fabricmc.fabric.test.registry.sync.RegistryAliasTest" ], "client": [ "net.fabricmc.fabric.test.registry.sync.client.DynamicRegistryClientTest",