Skip to content

Commit

Permalink
Registry aliasing (#4231)
Browse files Browse the repository at this point in the history
* initial

* style

* tests + more input checking

* revert formatting

* requests + more tests

* testmod

* check not frozen

* Fixes

* throw on default impl

* optimize implementation

---------

Co-authored-by: modmuss50 <[email protected]>
  • Loading branch information
Syst3ms and modmuss50 authored Dec 12, 2024
1 parent a730659 commit e2e49f7
Show file tree
Hide file tree
Showing 14 changed files with 397 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>Note: This interface is automatically implemented on all registries via Mixin and interface injection.</p>
*/
@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");
}
}
Original file line number Diff line number Diff line change
@@ -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 {
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -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;
Expand All @@ -67,7 +71,7 @@
import net.fabricmc.fabric.impl.registry.sync.RemappableRegistry;

@Mixin(SimpleRegistry.class)
public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, RemappableRegistry, ListenableRegistry<T> {
public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, RemappableRegistry, ListenableRegistry<T>, 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
Expand Down Expand Up @@ -111,6 +115,22 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
private Object2IntMap<Identifier> fabric_prevIndexedEntries;
@Unique
private BiMap<Identifier, RegistryEntry.Reference<T>> fabric_prevEntries;
@Unique
// invariant: the sets of keys and values are disjoint (every alias points to a 'deepest' non-alias ID)
private Map<Identifier, Identifier> aliases = new HashMap<>();

@Shadow
public abstract boolean containsId(Identifier id);

@Shadow
public abstract String toString();

@Shadow
@Final
private RegistryKey<? extends Registry<T>> key;

@Shadow
protected abstract void assertNotFrozen();

@Override
public Event<RegistryEntryAddedCallback<T>> fabric_getAddObjectEvent() {
Expand All @@ -131,6 +151,18 @@ private void init(RegistryKey key, Lifecycle lifecycle, boolean intrusive, Callb
}
}
);
// 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<T> callback : callbacks) {
Expand Down Expand Up @@ -174,7 +206,7 @@ public void remap(String name, Object2IntMap<Identifier> remoteIndexedEntries, R
List<String> strings = null;

for (Identifier remoteId : remoteIndexedEntries.keySet()) {
if (!idToEntry.containsKey(remoteId)) {
if (!this.containsId(remoteId)) {
if (strings == null) {
strings = new ArrayList<>();
}
Expand Down Expand Up @@ -382,4 +414,94 @@ public void unmap(String name) 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<Identifier, Identifier> 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<T> aliasRegistryKeyParameter(RegistryKey<T> original) {
Identifier aliased = aliases.get(original.getValue());
return aliased == null ? original : RegistryKey.of(original.getRegistryRef(), aliased);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"RegistriesMixin",
"RegistryKeysMixin",
"RegistryLoaderMixin",
"RegistryMixin",
"SaveLoadingMixin",
"SerializableRegistriesMixin",
"SimpleRegistryAccessor",
Expand Down
5 changes: 4 additions & 1 deletion fabric-registry-sync-v0/src/main/resources/fabric.mod.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
}
}
Original file line number Diff line number Diff line change
@@ -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<Registry<String>> testRegistryKey;
private Registry<String> 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<String> 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"));
}
}
Loading

0 comments on commit e2e49f7

Please sign in to comment.