Add optional registries & refactor registry sync (#4233)

* Refactor registry sync

* Remove old file

* Checkstyle

* Fixes

* Improve error message

* Fix

* Fix test
This commit is contained in:
modmuss 2024-11-27 18:01:38 +00:00 committed by GitHub
parent b90fb141f3
commit cc0fa2fec8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 826 additions and 260 deletions

View file

@ -20,8 +20,11 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import net.minecraft.network.NetworkPhase; import net.minecraft.network.NetworkPhase;
import net.minecraft.network.PacketCallbacks; import net.minecraft.network.PacketCallbacks;
import net.minecraft.network.packet.BrandCustomPayload;
import net.minecraft.network.packet.CustomPayload; import net.minecraft.network.packet.CustomPayload;
import net.minecraft.network.packet.Packet; import net.minecraft.network.packet.Packet;
import net.minecraft.network.packet.s2c.common.CommonPingS2CPacket; import net.minecraft.network.packet.s2c.common.CommonPingS2CPacket;
@ -44,6 +47,8 @@ public final class ServerConfigurationNetworkAddon extends AbstractChanneledNetw
private final MinecraftServer server; private final MinecraftServer server;
private final ServerConfigurationNetworking.Context context; private final ServerConfigurationNetworking.Context context;
private RegisterState registerState = RegisterState.NOT_SENT; private RegisterState registerState = RegisterState.NOT_SENT;
@Nullable
private String clientBrand = null;
public ServerConfigurationNetworkAddon(ServerConfigurationNetworkHandler handler, MinecraftServer server) { public ServerConfigurationNetworkAddon(ServerConfigurationNetworkHandler handler, MinecraftServer server) {
super(ServerNetworkingImpl.CONFIGURATION, ((ServerCommonNetworkHandlerAccessor) handler).getConnection(), "ServerConfigurationNetworkAddon for " + handler.getDebugProfile().getName()); super(ServerNetworkingImpl.CONFIGURATION, ((ServerCommonNetworkHandlerAccessor) handler).getConnection(), "ServerConfigurationNetworkAddon for " + handler.getDebugProfile().getName());
@ -55,6 +60,16 @@ public final class ServerConfigurationNetworkAddon extends AbstractChanneledNetw
this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.CONFIGURATION); this.registerPendingChannels((ChannelInfoHolder) this.connection, NetworkPhase.CONFIGURATION);
} }
@Override
public boolean handle(CustomPayload payload) {
if (payload instanceof BrandCustomPayload brandCustomPayload) {
clientBrand = brandCustomPayload.brand();
return false;
}
return super.handle(payload);
}
@Override @Override
protected void invokeInitEvent() { protected void invokeInitEvent() {
} }
@ -169,6 +184,10 @@ public final class ServerConfigurationNetworkAddon extends AbstractChanneledNetw
handler.send(packet, callback); handler.send(packet, callback);
} }
public @Nullable String getClientBrand() {
return clientBrand;
}
private enum RegisterState { private enum RegisterState {
NOT_SENT, NOT_SENT,
SENT, SENT,

View file

@ -0,0 +1,240 @@
/*
* 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.impl.client.registry.sync;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.screen.ScreenTexts;
import net.minecraft.text.MutableText;
import net.minecraft.text.Text;
import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier;
import net.minecraft.util.thread.ThreadExecutor;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager;
import net.fabricmc.fabric.impl.registry.sync.RemapException;
import net.fabricmc.fabric.impl.registry.sync.RemappableRegistry;
import net.fabricmc.fabric.impl.registry.sync.packet.RegistryPacketHandler;
public final class ClientRegistrySyncHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(ClientRegistrySyncHandler.class);
private ClientRegistrySyncHandler() {
}
public static <T extends RegistryPacketHandler.RegistrySyncPayload> CompletableFuture<Boolean> receivePacket(ThreadExecutor<?> executor, RegistryPacketHandler<T> handler, T payload, boolean accept) {
handler.receivePayload(payload);
if (!handler.isPacketFinished()) {
return CompletableFuture.completedFuture(false);
}
if (RegistrySyncManager.DEBUG) {
String handlerName = handler.getClass().getSimpleName();
LOGGER.info("{} total packet: {}", handlerName, handler.getTotalPacketReceived());
LOGGER.info("{} raw size: {}", handlerName, handler.getRawBufSize());
LOGGER.info("{} deflated size: {}", handlerName, handler.getDeflatedBufSize());
}
RegistryPacketHandler.SyncedPacketData data = handler.getSyncedPacketData();
if (!accept) {
return CompletableFuture.completedFuture(true);
}
return executor.submit(() -> {
if (data == null) {
throw new CompletionException(new RemapException("Received null map in sync packet!"));
}
try {
apply(data);
return true;
} catch (RemapException e) {
throw new CompletionException(e);
}
});
}
public static void apply(RegistryPacketHandler.SyncedPacketData data) throws RemapException {
// First check that all of the data provided is valid before making any changes
checkRemoteRemap(data);
for (Map.Entry<Identifier, Object2IntMap<Identifier>> entry : data.idMap().entrySet()) {
final Identifier registryId = entry.getKey();
Registry<?> registry = Registries.REGISTRIES.get(registryId);
// Registry was not found on the client, is it optional?
// If so we can just ignore it.
// Otherwise we throw an exception and disconnect.
if (registry == null) {
if (isRegistryOptional(registryId, data)) {
LOGGER.info("Received registry data for unknown optional registry: {}", registryId);
continue;
}
}
if (registry instanceof RemappableRegistry remappableRegistry) {
remappableRegistry.remap(entry.getValue(), RemappableRegistry.RemapMode.REMOTE);
return;
}
throw new RemapException("Registry " + registryId + " is not remappable");
}
}
@VisibleForTesting
public static void checkRemoteRemap(RegistryPacketHandler.SyncedPacketData data) throws RemapException {
Map<Identifier, Object2IntMap<Identifier>> map = data.idMap();
ArrayList<Identifier> missingRegistries = new ArrayList<>();
Map<Identifier, List<Identifier>> missingEntries = new HashMap<>();
for (Identifier registryId : map.keySet()) {
final Object2IntMap<Identifier> remoteRegistry = map.get(registryId);
Registry<?> registry = Registries.REGISTRIES.get(registryId);
if (registry == null) {
if (!isRegistryOptional(registryId, data)) {
// Registry was not found on the client, and is not optional.
missingRegistries.add(registryId);
}
continue;
}
for (Identifier remoteId : remoteRegistry.keySet()) {
if (!registry.containsId(remoteId)) {
// Found a registry entry from the server that is missing on the client
missingEntries.computeIfAbsent(registryId, i -> new ArrayList<>()).add(remoteId);
}
}
}
if (missingRegistries.isEmpty() && missingEntries.isEmpty()) {
// All good :)
return;
}
// Print out details to the log
if (!missingRegistries.isEmpty()) {
LOGGER.error("Received unknown remote registries from server");
for (Identifier registryId : missingRegistries) {
LOGGER.error("Received unknown remote registry ({}) from server", registryId);
}
}
if (!missingEntries.isEmpty()) {
LOGGER.error("Received unknown remote registry entries from server");
for (Map.Entry<Identifier, List<Identifier>> entry : missingEntries.entrySet()) {
for (Identifier identifier : entry.getValue()) {
LOGGER.error("Registry entry ({}) is missing from local registry ({})", identifier, entry.getKey());
}
}
}
if (!missingRegistries.isEmpty()) {
throw new RemapException(missingRegistriesError(missingRegistries));
}
throw new RemapException(missingEntriesError(missingEntries));
}
private static Text missingRegistriesError(List<Identifier> missingRegistries) {
MutableText text = Text.empty();
final int count = missingRegistries.size();
if (count == 1) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-registry.title.singular"));
} else {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-registry.title.plural", count));
}
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-registry.subtitle.1").formatted(Formatting.GREEN));
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-registry.subtitle.2"));
final int toDisplay = 4;
for (int i = 0; i < Math.min(missingRegistries.size(), toDisplay); i++) {
text = text.append(Text.literal(missingRegistries.get(i).toString()).formatted(Formatting.YELLOW));
text = text.append(ScreenTexts.LINE_BREAK);
}
if (missingRegistries.size() > toDisplay) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-registry.footer", missingRegistries.size() - toDisplay));
}
return text;
}
private static Text missingEntriesError(Map<Identifier, List<Identifier>> missingEntries) {
MutableText text = Text.empty();
final int count = missingEntries.values().stream().mapToInt(List::size).sum();
if (count == 1) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.title.singular"));
} else {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.title.plural", count));
}
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.subtitle.1").formatted(Formatting.GREEN));
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.subtitle.2"));
final int toDisplay = 4;
// Get the distinct missing namespaces
final List<String> namespaces = missingEntries.values().stream()
.flatMap(List::stream)
.map(Identifier::getNamespace)
.distinct()
.sorted()
.toList();
for (int i = 0; i < Math.min(namespaces.size(), toDisplay); i++) {
text = text.append(Text.literal(namespaces.get(i)).formatted(Formatting.YELLOW));
text = text.append(ScreenTexts.LINE_BREAK);
}
if (namespaces.size() > toDisplay) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.footer", namespaces.size() - toDisplay));
}
return text;
}
private static boolean isRegistryOptional(Identifier registryId, RegistryPacketHandler.SyncedPacketData data) {
EnumSet<RegistryAttribute> registryAttributes = data.attributes().get(registryId);
return registryAttributes.contains(RegistryAttribute.OPTIONAL);
}
}

View file

@ -40,7 +40,7 @@ public class FabricRegistryClientInit implements ClientModInitializer {
private <T extends RegistryPacketHandler.RegistrySyncPayload> void registerSyncPacketReceiver(RegistryPacketHandler<T> packetHandler) { private <T extends RegistryPacketHandler.RegistrySyncPayload> void registerSyncPacketReceiver(RegistryPacketHandler<T> packetHandler) {
ClientConfigurationNetworking.registerGlobalReceiver(packetHandler.getPacketId(), (payload, context) -> { ClientConfigurationNetworking.registerGlobalReceiver(packetHandler.getPacketId(), (payload, context) -> {
RegistrySyncManager.receivePacket(context.client(), packetHandler, payload, RegistrySyncManager.DEBUG || !context.client().isInSingleplayer()) ClientRegistrySyncHandler.receivePacket(context.client(), packetHandler, payload, RegistrySyncManager.DEBUG || !context.client().isInSingleplayer())
.whenComplete((complete, throwable) -> { .whenComplete((complete, throwable) -> {
if (throwable != null) { if (throwable != null) {
LOGGER.error("Registry remapping failed!", throwable); LOGGER.error("Registry remapping failed!", throwable);

View file

@ -20,6 +20,7 @@ import org.slf4j.Logger;
import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow; 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.At;
import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@ -28,9 +29,11 @@ import net.minecraft.client.MinecraftClient;
import net.minecraft.client.gui.screen.Screen; import net.minecraft.client.gui.screen.Screen;
import net.minecraft.item.ItemGroups; import net.minecraft.item.ItemGroups;
import net.minecraft.registry.Registries; import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager;
import net.fabricmc.fabric.impl.registry.sync.RemapException; import net.fabricmc.fabric.impl.registry.sync.RemapException;
import net.fabricmc.fabric.impl.registry.sync.RemappableRegistry;
import net.fabricmc.fabric.impl.registry.sync.trackers.vanilla.BlockInitTracker; import net.fabricmc.fabric.impl.registry.sync.trackers.vanilla.BlockInitTracker;
@Mixin(MinecraftClient.class) @Mixin(MinecraftClient.class)
@ -43,7 +46,7 @@ public class MinecraftClientMixin {
@Inject(at = @At("RETURN"), method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;Z)V") @Inject(at = @At("RETURN"), method = "disconnect(Lnet/minecraft/client/gui/screen/Screen;Z)V")
public void disconnectAfter(Screen disconnectionScreen, boolean bl, CallbackInfo ci) { public void disconnectAfter(Screen disconnectionScreen, boolean bl, CallbackInfo ci) {
try { try {
RegistrySyncManager.unmap(); unmap();
} catch (RemapException e) { } catch (RemapException e) {
LOGGER.warn("Failed to unmap Fabric registries!", e); LOGGER.warn("Failed to unmap Fabric registries!", e);
} }
@ -57,4 +60,15 @@ public class MinecraftClientMixin {
BlockInitTracker.postFreeze(); BlockInitTracker.postFreeze();
ItemGroups.collect(); ItemGroups.collect();
} }
@Unique
private static void unmap() throws RemapException {
for (Identifier registryId : Registries.REGISTRIES.getIds()) {
Registry<?> registry = Registries.REGISTRIES.get(registryId);
if (registry instanceof RemappableRegistry) {
((RemappableRegistry) registry).unmap();
}
}
}
} }

View file

@ -25,5 +25,10 @@ public enum RegistryAttribute {
/** /**
* Registry has been modded. * Registry has been modded.
*/ */
MODDED MODDED,
/**
* Registry is optional, any connecting client will not be disconnected if the registry is not present.
*/
OPTIONAL
} }

View file

@ -20,10 +20,13 @@ import java.util.EnumSet;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.jetbrains.annotations.VisibleForTesting;
import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKey;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute; import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder; import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.loader.api.FabricLoader;
public final class RegistryAttributeImpl implements RegistryAttributeHolder { public final class RegistryAttributeImpl implements RegistryAttributeHolder {
private static final Map<RegistryKey<?>, RegistryAttributeHolder> HOLDER_MAP = new ConcurrentHashMap<>(); private static final Map<RegistryKey<?>, RegistryAttributeHolder> HOLDER_MAP = new ConcurrentHashMap<>();
@ -43,6 +46,15 @@ public final class RegistryAttributeImpl implements RegistryAttributeHolder {
return this; return this;
} }
@VisibleForTesting
public void removeAttribute(RegistryAttribute attribute) {
if (!FabricLoader.getInstance().isDevelopmentEnvironment()) {
throw new AssertionError();
}
attributes.remove(attribute);
}
@Override @Override
public boolean hasAttribute(RegistryAttribute attribute) { public boolean hasAttribute(RegistryAttribute attribute) {
return attributes.contains(attribute); return attributes.contains(attribute);

View file

@ -20,31 +20,24 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.function.Consumer; import java.util.function.Consumer;
import com.google.common.base.Joiner;
import com.google.common.collect.Sets;
import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet; import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntLinkedOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import net.minecraft.network.packet.Packet; import net.minecraft.network.packet.Packet;
import net.minecraft.registry.Registries; import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry; import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.screen.ScreenTexts; import net.minecraft.screen.ScreenTexts;
import net.minecraft.server.MinecraftServer; import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerConfigurationNetworkHandler; import net.minecraft.server.network.ServerConfigurationNetworkHandler;
@ -53,13 +46,12 @@ import net.minecraft.text.MutableText;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Formatting; import net.minecraft.util.Formatting;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.thread.ThreadExecutor;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute; import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder; import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking; import net.fabricmc.fabric.api.networking.v1.ServerConfigurationNetworking;
import net.fabricmc.fabric.impl.networking.server.ServerNetworkingImpl;
import net.fabricmc.fabric.impl.registry.sync.packet.DirectRegistryPacketHandler; import net.fabricmc.fabric.impl.registry.sync.packet.DirectRegistryPacketHandler;
import net.fabricmc.fabric.impl.registry.sync.packet.RegistryPacketHandler;
public final class RegistrySyncManager { public final class RegistrySyncManager {
public static final boolean DEBUG = Boolean.getBoolean("fabric.registry.debug"); public static final boolean DEBUG = Boolean.getBoolean("fabric.registry.debug");
@ -80,11 +72,6 @@ public final class RegistrySyncManager {
return; return;
} }
if (!ServerConfigurationNetworking.canSend(handler, DIRECT_PACKET_HANDLER.getPacketId())) {
// Don't send if the client cannot receive
return;
}
final Map<Identifier, Object2IntMap<Identifier>> map = RegistrySyncManager.createAndPopulateRegistryMap(); final Map<Identifier, Object2IntMap<Identifier>> map = RegistrySyncManager.createAndPopulateRegistryMap();
if (map == null) { if (map == null) {
@ -92,9 +79,62 @@ public final class RegistrySyncManager {
return; return;
} }
if (!ServerConfigurationNetworking.canSend(handler, DIRECT_PACKET_HANDLER.getPacketId())) {
if (areAllRegistriesOptional(map)) {
// Allow the client to connect if all of the registries we want to sync are optional
return;
}
// Disconnect incompatible clients
Text message = getIncompatibleClientText(ServerNetworkingImpl.getAddon(handler).getClientBrand(), map);
handler.disconnect(message);
return;
}
handler.addTask(new SyncConfigurationTask(handler, map)); handler.addTask(new SyncConfigurationTask(handler, map));
} }
private static Text getIncompatibleClientText(@Nullable String brand, Map<Identifier, Object2IntMap<Identifier>> map) {
String brandText = switch (brand) {
case "fabric" -> "Fabric API";
case null, default -> "Fabric Loader and Fabric API";
};
final int toDisplay = 4;
List<String> namespaces = map.values().stream()
.map(Object2IntMap::keySet)
.flatMap(Set::stream)
.map(Identifier::getNamespace)
.filter(s -> !s.equals(Identifier.DEFAULT_NAMESPACE))
.distinct()
.sorted()
.toList();
MutableText text = Text.literal("The following registry entry namespaces may be related:\n\n");
for (int i = 0; i < Math.min(namespaces.size(), toDisplay); i++) {
text = text.append(Text.literal(namespaces.get(i)).formatted(Formatting.YELLOW));
text = text.append(ScreenTexts.LINE_BREAK);
}
if (namespaces.size() > toDisplay) {
text = text.append(Text.literal("And %d more...".formatted(namespaces.size() - toDisplay)));
}
return Text.literal("This server requires ").append(Text.literal(brandText).formatted(Formatting.GREEN)).append(" installed on your client!")
.append(ScreenTexts.LINE_BREAK).append(text)
.append(ScreenTexts.LINE_BREAK).append(ScreenTexts.LINE_BREAK).append(Text.literal("Contact the server's administrator for more information!").formatted(Formatting.GOLD));
}
private static boolean areAllRegistriesOptional(Map<Identifier, Object2IntMap<Identifier>> map) {
return map.keySet().stream()
.map(Registries.REGISTRIES::get)
.filter(Objects::nonNull)
.map(RegistryAttributeHolder::get)
.allMatch(attributes -> attributes.hasAttribute(RegistryAttribute.OPTIONAL));
}
public record SyncConfigurationTask( public record SyncConfigurationTask(
ServerConfigurationNetworkHandler handler, ServerConfigurationNetworkHandler handler,
Map<Identifier, Object2IntMap<Identifier>> map Map<Identifier, Object2IntMap<Identifier>> map
@ -112,40 +152,6 @@ public final class RegistrySyncManager {
} }
} }
public static <T extends RegistryPacketHandler.RegistrySyncPayload> CompletableFuture<Boolean> receivePacket(ThreadExecutor<?> executor, RegistryPacketHandler<T> handler, T payload, boolean accept) {
handler.receivePayload(payload);
if (!handler.isPacketFinished()) {
return CompletableFuture.completedFuture(false);
}
if (DEBUG) {
String handlerName = handler.getClass().getSimpleName();
LOGGER.info("{} total packet: {}", handlerName, handler.getTotalPacketReceived());
LOGGER.info("{} raw size: {}", handlerName, handler.getRawBufSize());
LOGGER.info("{} deflated size: {}", handlerName, handler.getDeflatedBufSize());
}
Map<Identifier, Object2IntMap<Identifier>> map = handler.getSyncedRegistryMap();
if (!accept) {
return CompletableFuture.completedFuture(true);
}
return executor.submit(() -> {
if (map == null) {
throw new CompletionException(new RemapException("Received null map in sync packet!"));
}
try {
apply(map, RemappableRegistry.RemapMode.REMOTE);
return true;
} catch (RemapException e) {
throw new CompletionException(e);
}
});
}
/** /**
* Creates a {@link Map} used to sync the registry ids. * Creates a {@link Map} used to sync the registry ids.
* *
@ -254,121 +260,6 @@ public final class RegistrySyncManager {
return map; return map;
} }
public static void apply(Map<Identifier, Object2IntMap<Identifier>> map, RemappableRegistry.RemapMode mode) throws RemapException {
if (mode == RemappableRegistry.RemapMode.REMOTE) {
checkRemoteRemap(map);
}
Set<Identifier> containedRegistries = Sets.newHashSet(map.keySet());
for (Identifier registryId : Registries.REGISTRIES.getIds()) {
if (!containedRegistries.remove(registryId)) {
continue;
}
Object2IntMap<Identifier> registryMap = map.get(registryId);
Registry<?> registry = Registries.REGISTRIES.get(registryId);
RegistryAttributeHolder attributeHolder = RegistryAttributeHolder.get(registry.getKey());
if (!attributeHolder.hasAttribute(RegistryAttribute.MODDED)) {
LOGGER.debug("Not applying registry data to vanilla registry {}", registryId.toString());
continue;
}
if (registry instanceof RemappableRegistry remappableRegistry) {
remappableRegistry.remap(registryId.toString(), registryMap, mode);
} else {
throw new RemapException("Registry " + registryId + " is not remappable");
}
}
if (!containedRegistries.isEmpty()) {
LOGGER.warn("[fabric-registry-sync] Could not find the following registries: " + Joiner.on(", ").join(containedRegistries));
}
}
@VisibleForTesting
public static void checkRemoteRemap(Map<Identifier, Object2IntMap<Identifier>> map) throws RemapException {
Map<Identifier, List<Identifier>> missingEntries = new HashMap<>();
for (Map.Entry<? extends RegistryKey<? extends Registry<?>>, ? extends Registry<?>> entry : Registries.REGISTRIES.getEntrySet()) {
final Registry<?> registry = entry.getValue();
final Identifier registryId = entry.getKey().getValue();
final Object2IntMap<Identifier> remoteRegistry = map.get(registryId);
if (remoteRegistry == null) {
// Registry sync does not contain data for this registry, will print a warning when applying.
continue;
}
for (Identifier remoteId : remoteRegistry.keySet()) {
if (!registry.containsId(remoteId)) {
// Found a registry entry from the server that is
missingEntries.computeIfAbsent(registryId, i -> new ArrayList<>()).add(remoteId);
}
}
}
if (missingEntries.isEmpty()) {
// All good :)
return;
}
// Print out details to the log
LOGGER.error("Received unknown remote registry entries from server");
for (Map.Entry<Identifier, List<Identifier>> entry : missingEntries.entrySet()) {
for (Identifier identifier : entry.getValue()) {
LOGGER.error("Registry entry ({}) is missing from local registry ({})", identifier, entry.getKey());
}
}
// Create a nice user friendly error message.
MutableText text = Text.empty();
final int count = missingEntries.values().stream().mapToInt(List::size).sum();
if (count == 1) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.title.singular"));
} else {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.title.plural", count));
}
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.subtitle.1").formatted(Formatting.GREEN));
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.subtitle.2"));
final int toDisplay = 4;
// Get the distinct missing namespaces
final List<String> namespaces = missingEntries.values().stream()
.flatMap(List::stream)
.map(Identifier::getNamespace)
.distinct()
.sorted()
.toList();
for (int i = 0; i < Math.min(namespaces.size(), toDisplay); i++) {
text = text.append(Text.literal(namespaces.get(i)).formatted(Formatting.YELLOW));
text = text.append(ScreenTexts.LINE_BREAK);
}
if (namespaces.size() > toDisplay) {
text = text.append(Text.translatable("fabric-registry-sync-v0.unknown-remote.footer", namespaces.size() - toDisplay));
}
throw new RemapException(text);
}
public static void unmap() throws RemapException {
for (Identifier registryId : Registries.REGISTRIES.getIds()) {
Registry registry = Registries.REGISTRIES.get(registryId);
if (registry instanceof RemappableRegistry) {
((RemappableRegistry) registry).unmap(registryId.toString());
}
}
}
public static void bootstrapRegistries() { public static void bootstrapRegistries() {
postBootstrap = true; postBootstrap = true;
} }

View file

@ -37,13 +37,9 @@ public interface RemappableRegistry {
* client). * client).
*/ */
REMOTE, REMOTE,
/**
* No differences in entry sets are allowed.
*/
EXACT
} }
void remap(String name, Object2IntMap<Identifier> remoteIndexedEntries, RemapMode mode) throws RemapException; void remap(Object2IntMap<Identifier> remoteIndexedEntries, RemapMode mode) throws RemapException;
void unmap(String name) throws RemapException; void unmap() throws RemapException;
} }

View file

@ -17,7 +17,9 @@
package net.fabricmc.fabric.impl.registry.sync.packet; package net.fabricmc.fabric.impl.registry.sync.packet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.EnumSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
@ -33,8 +35,12 @@ import org.jetbrains.annotations.Nullable;
import net.minecraft.network.PacketByteBuf; import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.codec.PacketCodec; import net.minecraft.network.codec.PacketCodec;
import net.minecraft.network.packet.CustomPayload; import net.minecraft.network.packet.CustomPayload;
import net.minecraft.registry.Registries;
import net.minecraft.registry.Registry;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
/** /**
@ -65,6 +71,9 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
@Nullable @Nullable
private Map<Identifier, Object2IntMap<Identifier>> syncedRegistryMap; private Map<Identifier, Object2IntMap<Identifier>> syncedRegistryMap;
@Nullable
private Map<Identifier, EnumSet<RegistryAttribute>> syncedRegistryAttributes;
private boolean isPacketFinished = false; private boolean isPacketFinished = false;
private int totalPacketReceived = 0; private int totalPacketReceived = 0;
@ -89,6 +98,7 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
for (Identifier regId : regIds) { for (Identifier regId : regIds) {
buf.writeString(regId.getPath()); buf.writeString(regId.getPath());
buf.writeByte(encodeRegistryAttributes(regId));
Object2IntMap<Identifier> idMap = registryMap.get(regId); Object2IntMap<Identifier> idMap = registryMap.get(regId);
@ -181,6 +191,7 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
computeBufSize(combinedBuf); computeBufSize(combinedBuf);
syncedRegistryMap = new LinkedHashMap<>(); syncedRegistryMap = new LinkedHashMap<>();
syncedRegistryAttributes = new LinkedHashMap<>();
int regNamespaceGroupAmount = combinedBuf.readVarInt(); int regNamespaceGroupAmount = combinedBuf.readVarInt();
for (int i = 0; i < regNamespaceGroupAmount; i++) { for (int i = 0; i < regNamespaceGroupAmount; i++) {
@ -189,6 +200,7 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
for (int j = 0; j < regNamespaceGroupLength; j++) { for (int j = 0; j < regNamespaceGroupLength; j++) {
String regPath = combinedBuf.readString(); String regPath = combinedBuf.readString();
EnumSet<RegistryAttribute> attributes = decodeRegistryAttributes(combinedBuf.readByte());
Object2IntMap<Identifier> idMap = new Object2IntLinkedOpenHashMap<>(); Object2IntMap<Identifier> idMap = new Object2IntLinkedOpenHashMap<>();
int idNamespaceGroupAmount = combinedBuf.readVarInt(); int idNamespaceGroupAmount = combinedBuf.readVarInt();
@ -214,7 +226,9 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
} }
} }
syncedRegistryMap.put(Identifier.of(regNamespace, regPath), idMap); Identifier registryId = Identifier.of(regNamespace, regPath);
syncedRegistryMap.put(registryId, idMap);
syncedRegistryAttributes.put(registryId, attributes);
} }
} }
@ -235,13 +249,22 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
@Override @Override
@Nullable @Nullable
public Map<Identifier, Object2IntMap<Identifier>> getSyncedRegistryMap() { public SyncedPacketData getSyncedPacketData() {
Preconditions.checkState(isPacketFinished); Preconditions.checkState(isPacketFinished);
Map<Identifier, Object2IntMap<Identifier>> map = syncedRegistryMap;
if (syncedRegistryMap == null || syncedRegistryAttributes == null) {
return null;
}
Map<Identifier, Object2IntMap<Identifier>> map = Collections.unmodifiableMap(syncedRegistryMap);
Map<Identifier, EnumSet<RegistryAttribute>> attributes = Collections.unmodifiableMap(syncedRegistryAttributes);
isPacketFinished = false; isPacketFinished = false;
totalPacketReceived = 0; totalPacketReceived = 0;
syncedRegistryMap = null; syncedRegistryMap = null;
return map; syncedRegistryAttributes = null;
return new SyncedPacketData(map, attributes);
} }
private DirectRegistryPacketHandler.Payload createPayload(PacketByteBuf buf) { private DirectRegistryPacketHandler.Payload createPayload(PacketByteBuf buf) {
@ -283,4 +306,32 @@ public class DirectRegistryPacketHandler extends RegistryPacketHandler<DirectReg
return ID; return ID;
} }
} }
private static byte encodeRegistryAttributes(Identifier identifier) {
Registry<?> registry = Registries.REGISTRIES.get(identifier);
if (registry == null) {
return 0;
}
RegistryAttributeHolder holder = RegistryAttributeHolder.get(registry);
byte encoded = 0;
// Only send the optional marker.
if (holder.hasAttribute(RegistryAttribute.OPTIONAL)) {
encoded |= 0x1;
}
return encoded;
}
private static EnumSet<RegistryAttribute> decodeRegistryAttributes(byte encoded) {
EnumSet<RegistryAttribute> attributes = EnumSet.noneOf(RegistryAttribute.class);
if ((encoded & 0x1) != 0) {
attributes.add(RegistryAttribute.OPTIONAL);
}
return attributes;
}
} }

View file

@ -16,6 +16,7 @@
package net.fabricmc.fabric.impl.registry.sync.packet; package net.fabricmc.fabric.impl.registry.sync.packet;
import java.util.EnumSet;
import java.util.Map; import java.util.Map;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.zip.Deflater; import java.util.zip.Deflater;
@ -28,6 +29,7 @@ import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.packet.CustomPayload; import net.minecraft.network.packet.CustomPayload;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs; import net.fabricmc.fabric.api.networking.v1.PacketByteBufs;
import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager; import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager;
@ -46,7 +48,7 @@ public abstract class RegistryPacketHandler<T extends RegistryPacketHandler.Regi
public abstract boolean isPacketFinished(); public abstract boolean isPacketFinished();
@Nullable @Nullable
public abstract Map<Identifier, Object2IntMap<Identifier>> getSyncedRegistryMap(); public abstract SyncedPacketData getSyncedPacketData();
protected final void computeBufSize(PacketByteBuf buf) { protected final void computeBufSize(PacketByteBuf buf) {
if (!RegistrySyncManager.DEBUG) { if (!RegistrySyncManager.DEBUG) {
@ -92,4 +94,9 @@ public abstract class RegistryPacketHandler<T extends RegistryPacketHandler.Regi
public interface RegistrySyncPayload extends CustomPayload { public interface RegistrySyncPayload extends CustomPayload {
} }
public record SyncedPacketData(
Map<Identifier, Object2IntMap<Identifier>> idMap,
Map<Identifier, EnumSet<RegistryAttribute>> attributes
) { }
} }

View file

@ -73,9 +73,6 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
@Unique @Unique
private static final Set<String> VANILLA_NAMESPACES = Set.of("minecraft", "brigadier"); private static final Set<String> VANILLA_NAMESPACES = Set.of("minecraft", "brigadier");
@Unique
private static final Logger LOGGER = LoggerFactory.getLogger("FabricRegistrySync");
@Shadow @Shadow
@Final @Final
private ObjectList<RegistryEntry.Reference<T>> rawIdToEntry; private ObjectList<RegistryEntry.Reference<T>> rawIdToEntry;
@ -123,7 +120,7 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
} }
@Inject(method = "<init>(Lnet/minecraft/registry/RegistryKey;Lcom/mojang/serialization/Lifecycle;Z)V", at = @At("RETURN")) @Inject(method = "<init>(Lnet/minecraft/registry/RegistryKey;Lcom/mojang/serialization/Lifecycle;Z)V", at = @At("RETURN"))
private void init(RegistryKey key, Lifecycle lifecycle, boolean intrusive, CallbackInfo ci) { private void init(RegistryKey<?> key, Lifecycle lifecycle, boolean intrusive, CallbackInfo ci) {
fabric_addObjectEvent = EventFactory.createArrayBacked(RegistryEntryAddedCallback.class, fabric_addObjectEvent = EventFactory.createArrayBacked(RegistryEntryAddedCallback.class,
(callbacks) -> (rawId, id, object) -> { (callbacks) -> (rawId, id, object) -> {
for (RegistryEntryAddedCallback<T> callback : callbacks) { for (RegistryEntryAddedCallback<T> callback : callbacks) {
@ -165,7 +162,7 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
} }
@Override @Override
public void remap(String name, Object2IntMap<Identifier> remoteIndexedEntries, RemapMode mode) throws RemapException { public void remap(Object2IntMap<Identifier> remoteIndexedEntries, RemapMode mode) throws RemapException {
// Throw on invalid conditions. // Throw on invalid conditions.
switch (mode) { switch (mode) {
case AUTHORITATIVE: case AUTHORITATIVE:
@ -184,34 +181,7 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
} }
if (strings != null) { if (strings != null) {
StringBuilder builder = new StringBuilder("Received ID map for " + name + " contains IDs unknown to the receiver!"); StringBuilder builder = new StringBuilder("Received ID map for " + getKey() + " contains IDs unknown to the receiver!");
for (String s : strings) {
builder.append('\n').append(s);
}
throw new RemapException(builder.toString());
}
break;
}
case EXACT: {
if (!idToEntry.keySet().equals(remoteIndexedEntries.keySet())) {
List<String> strings = new ArrayList<>();
for (Identifier remoteId : remoteIndexedEntries.keySet()) {
if (!idToEntry.containsKey(remoteId)) {
strings.add(" - " + remoteId + " (missing on local)");
}
}
for (Identifier localId : getIds()) {
if (!remoteIndexedEntries.containsKey(localId)) {
strings.add(" - " + localId + " (missing on remote)");
}
}
StringBuilder builder = new StringBuilder("Local and remote ID sets for " + name + " do not match!");
for (String s : strings) { for (String s : strings) {
builder.append('\n').append(s); builder.append('\n').append(s);
@ -350,7 +320,7 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
} }
@Override @Override
public void unmap(String name) throws RemapException { public void unmap() throws RemapException {
if (fabric_prevIndexedEntries != null) { if (fabric_prevIndexedEntries != null) {
List<Identifier> addedIds = new ArrayList<>(); List<Identifier> addedIds = new ArrayList<>();
@ -372,7 +342,7 @@ public abstract class SimpleRegistryMixin<T> implements MutableRegistry<T>, Rema
keyToEntry.put(entryKey, entry.getValue()); keyToEntry.put(entryKey, entry.getValue());
} }
remap(name, fabric_prevIndexedEntries, RemapMode.AUTHORITATIVE); remap(fabric_prevIndexedEntries, RemapMode.AUTHORITATIVE);
for (Identifier id : addedIds) { for (Identifier id : addedIds) {
fabric_getAddObjectEvent().invoker().onEntryAdded(entryToRawId.getInt(idToEntry.get(id)), id, get(id)); fabric_getAddObjectEvent().invoker().onEntryAdded(entryToRawId.getInt(idToEntry.get(id)), id, get(id));

View file

@ -3,5 +3,10 @@
"fabric-registry-sync-v0.unknown-remote.title.plural" : "Received %d registry entries that are unknown to this client.\n", "fabric-registry-sync-v0.unknown-remote.title.plural" : "Received %d registry entries that are unknown to this client.\n",
"fabric-registry-sync-v0.unknown-remote.subtitle.1" : "This is usually caused by a mismatched mod set between the client and server.", "fabric-registry-sync-v0.unknown-remote.subtitle.1" : "This is usually caused by a mismatched mod set between the client and server.",
"fabric-registry-sync-v0.unknown-remote.subtitle.2" : " See the client logs for more details.\nThe following registry entry namespaces may be related:\n\n", "fabric-registry-sync-v0.unknown-remote.subtitle.2" : " See the client logs for more details.\nThe following registry entry namespaces may be related:\n\n",
"fabric-registry-sync-v0.unknown-remote.footer" : "And %d more..." "fabric-registry-sync-v0.unknown-remote.footer" : "And %d more...",
"fabric-registry-sync-v0.unknown-registry.title.singular" : "Received a registry that is unknown to this client.\n",
"fabric-registry-sync-v0.unknown-registry.title.plural" : "Received %d registries that are unknown to this client.\n",
"fabric-registry-sync-v0.unknown-registry.subtitle.1" : "This is usually caused by a mismatched mod set between the client and server.",
"fabric-registry-sync-v0.unknown-registry.subtitle.2" : " See the client logs for more details.\nThe following registries are not present on the client:\n\n",
"fabric-registry-sync-v0.unknown-registry.footer" : "And %d more..."
} }

View file

@ -24,13 +24,22 @@ import java.util.Map;
import it.unimi.dsi.fastutil.objects.Object2IntMap; import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import net.minecraft.Bootstrap;
import net.minecraft.SharedConstants;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.fabricmc.fabric.impl.registry.sync.packet.DirectRegistryPacketHandler; import net.fabricmc.fabric.impl.registry.sync.packet.DirectRegistryPacketHandler;
public class DirectRegistryPacketHandlerTest { public class DirectRegistryPacketHandlerTest {
@BeforeAll
static void beforeAll() {
SharedConstants.createGameVersion();
Bootstrap.initialize();
}
@Test @Test
void emptyRegistrySync() { void emptyRegistrySync() {
DirectRegistryPacketHandler handler = new DirectRegistryPacketHandler(); DirectRegistryPacketHandler handler = new DirectRegistryPacketHandler();
@ -48,7 +57,7 @@ public class DirectRegistryPacketHandlerTest {
handler.receivePayload(payload); handler.receivePayload(payload);
} }
assertMatchesDeep(registry, handler.getSyncedRegistryMap()); assertMatchesDeep(registry, handler.getSyncedPacketData().idMap());
} }
@Test @Test
@ -69,7 +78,7 @@ public class DirectRegistryPacketHandlerTest {
handler.receivePayload(payload); handler.receivePayload(payload);
} }
assertMatchesDeep(registry, handler.getSyncedRegistryMap()); assertMatchesDeep(registry, handler.getSyncedPacketData().idMap());
} }
@Test @Test
@ -93,7 +102,7 @@ public class DirectRegistryPacketHandlerTest {
handler.receivePayload(payload); handler.receivePayload(payload);
} }
assertMatchesDeep(registry, handler.getSyncedRegistryMap()); assertMatchesDeep(registry, handler.getSyncedPacketData().idMap());
} }
private static Object2IntMap<Identifier> createRegistry(int size) { private static Object2IntMap<Identifier> createRegistry(int size) {

View file

@ -0,0 +1,310 @@
/*
* 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.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ExecutionException;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import net.minecraft.Bootstrap;
import net.minecraft.SharedConstants;
import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.SimpleRegistry;
import net.minecraft.util.Identifier;
import net.minecraft.util.thread.ThreadExecutor;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.fabric.impl.client.registry.sync.ClientRegistrySyncHandler;
import net.fabricmc.fabric.impl.registry.sync.RegistryAttributeImpl;
import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager;
import net.fabricmc.fabric.impl.registry.sync.RemapException;
import net.fabricmc.fabric.impl.registry.sync.RemappableRegistry;
import net.fabricmc.fabric.impl.registry.sync.packet.DirectRegistryPacketHandler;
public class RegistryRemapTest {
private RegistryKey<Registry<String>> testRegistryKey;
private SimpleRegistry<String> testRegistry;
@BeforeAll
static void beforeAll() {
SharedConstants.createGameVersion();
Bootstrap.initialize();
}
@BeforeEach
void beforeEach() {
testRegistryKey = RegistryKey.ofRegistry(id(UUID.randomUUID().toString()));
testRegistry = FabricRegistryBuilder.createSimple(testRegistryKey)
.attribute(RegistryAttribute.SYNCED)
.buildAndRegister();
Registry.register(testRegistry, id("zero"), "zero");
Registry.register(testRegistry, id("one"), "one");
Registry.register(testRegistry, id("two"), "two");
}
@AfterEach
void afterEach() throws RemapException {
// If a test fails, make sure we unmap the registry to avoid affecting other tests
RemappableRegistry remappableRegistry = (RemappableRegistry) testRegistry;
remappableRegistry.unmap();
}
@Test
void remapRegistry() throws RemapException {
RemappableRegistry remappableRegistry = (RemappableRegistry) testRegistry;
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
Map<Identifier, Integer> idMap = Map.of(
id("zero"), 2,
id("one"), 1,
id("two"), 0
);
remappableRegistry.remap(asFastMap(idMap), RemappableRegistry.RemapMode.AUTHORITATIVE);
assertEquals(2, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(0, testRegistry.getRawId("two"));
remappableRegistry.unmap();
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
}
@Test
void remapRegistryViaPacket() throws RemapException {
RemappableRegistry remappableRegistry = (RemappableRegistry) testRegistry;
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
Map<Identifier, Integer> idMap = Map.of(
id("two"), 0,
id("one"), 1,
id("zero"), 2
);
var payloads = new ArrayList<DirectRegistryPacketHandler.Payload>();
RegistrySyncManager.DIRECT_PACKET_HANDLER.sendPacket(
payloads::add,
Map.of(testRegistryKey.getValue(), asFastMap(idMap))
);
List<Boolean> results = receivePayloads(payloads);
// Expect 2 packets, 1 with the data (as it fits in one packet) and 1 empty packet to signal the end
assertEquals(2, results.size());
assertFalse(results.getFirst());
assertTrue(results.get(1));
assertEquals(2, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(0, testRegistry.getRawId("two"));
remappableRegistry.unmap();
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
}
@Test
void unknownEntry() {
Map<Identifier, Integer> idMap = Map.of(
id("two"), 0,
id("one"), 1,
id("zero"), 2,
id("unknown"), 3
);
var payloads = new ArrayList<DirectRegistryPacketHandler.Payload>();
RegistrySyncManager.DIRECT_PACKET_HANDLER.sendPacket(
payloads::add,
Map.of(testRegistryKey.getValue(), asFastMap(idMap))
);
RemapException remapException = assertThrows(RemapException.class, () -> receivePayloads(payloads));
assertTrue(remapException.getMessage().contains("unknown-remote"));
}
@Test
void unknownRegistry() {
Map<Identifier, Integer> idMap = Map.of(
id("two"), 0,
id("one"), 1,
id("zero"), 2
);
var payloads = new ArrayList<DirectRegistryPacketHandler.Payload>();
RegistrySyncManager.DIRECT_PACKET_HANDLER.sendPacket(
payloads::add,
Map.of(id("unknown"), asFastMap(idMap))
);
RemapException remapException = assertThrows(RemapException.class, () -> receivePayloads(payloads));
assertTrue(remapException.getMessage().contains("unknown-registry"));
}
@Test
void unknownOptionalRegistry() throws RemapException {
Map<Identifier, Integer> idMap = Map.of(
id("two"), 0,
id("one"), 1,
id("zero"), 2
);
RegistryAttributeImpl holder = (RegistryAttributeImpl) RegistryAttributeHolder.get(testRegistryKey);
holder.addAttribute(RegistryAttribute.OPTIONAL);
var payloads = new ArrayList<DirectRegistryPacketHandler.Payload>();
RegistrySyncManager.DIRECT_PACKET_HANDLER.sendPacket(
payloads::add,
Map.of(testRegistryKey.getValue(), asFastMap(idMap))
);
// Packet should be handled without issue.
List<Boolean> results = receivePayloads(payloads);
assertEquals(2, results.size());
assertFalse(results.getFirst());
assertTrue(results.get(1));
holder.removeAttribute(RegistryAttribute.OPTIONAL);
}
@Test
void missingRemoteEntries() throws RemapException {
RemappableRegistry remappableRegistry = (RemappableRegistry) testRegistry;
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
Map<Identifier, Integer> idMap = Map.of(
id("two"), 0,
id("zero"), 1
);
var payloads = new ArrayList<DirectRegistryPacketHandler.Payload>();
RegistrySyncManager.DIRECT_PACKET_HANDLER.sendPacket(
payloads::add,
Map.of(testRegistryKey.getValue(), asFastMap(idMap))
);
receivePayloads(payloads);
assertEquals(0, testRegistry.getRawId("two"));
assertEquals(1, testRegistry.getRawId("zero"));
// assigned an ID at the end of the registry
assertEquals(2, testRegistry.getRawId("one"));
remappableRegistry.unmap();
assertEquals(0, testRegistry.getRawId("zero"));
assertEquals(1, testRegistry.getRawId("one"));
assertEquals(2, testRegistry.getRawId("two"));
}
private static List<Boolean> receivePayloads(List<DirectRegistryPacketHandler.Payload> payloads) throws RemapException {
var results = new ArrayList<Boolean>();
try {
for (DirectRegistryPacketHandler.Payload payload : payloads) {
CompletableFuture<Boolean> future = ClientRegistrySyncHandler.receivePacket(
ThisThreadExecutor.INSTANCE,
RegistrySyncManager.DIRECT_PACKET_HANDLER,
payload,
true
);
results.add(future.get());
}
} catch (CompletionException e) {
if (e.getCause() instanceof RemapException remapException) {
throw remapException;
}
throw e;
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
return results;
}
private static Object2IntMap<Identifier> asFastMap(Map<Identifier, Integer> map) {
var fastMap = new Object2IntOpenHashMap<Identifier>();
fastMap.putAll(map);
return fastMap;
}
private static Identifier id(String path) {
return Identifier.of("registry_sync_test", path);
}
// Run the task on the current thread instantly
private static class ThisThreadExecutor extends ThreadExecutor<Runnable> {
public static final ThisThreadExecutor INSTANCE = new ThisThreadExecutor();
private ThisThreadExecutor() {
super("Test thread executor");
}
@Override
protected boolean canExecute(Runnable task) {
return true;
}
@Override
protected Thread getThread() {
return Thread.currentThread();
}
@Override
public Runnable createTask(Runnable runnable) {
return runnable;
}
}
}

View file

@ -18,13 +18,9 @@ package net.fabricmc.fabric.test.registry.sync;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
import com.mojang.logging.LogUtils; import com.mojang.logging.LogUtils;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import org.apache.commons.lang3.Validate; import org.apache.commons.lang3.Validate;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -37,19 +33,14 @@ import net.minecraft.registry.Registry;
import net.minecraft.registry.RegistryKey; import net.minecraft.registry.RegistryKey;
import net.minecraft.registry.RegistryKeys; import net.minecraft.registry.RegistryKeys;
import net.minecraft.registry.SimpleRegistry; import net.minecraft.registry.SimpleRegistry;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.fabricmc.api.ModInitializer; import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.fabricmc.fabric.api.event.registry.DynamicRegistrySetupCallback; import net.fabricmc.fabric.api.event.registry.DynamicRegistrySetupCallback;
import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder; import net.fabricmc.fabric.api.event.registry.FabricRegistryBuilder;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute; import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder; import net.fabricmc.fabric.api.event.registry.RegistryAttributeHolder;
import net.fabricmc.fabric.impl.registry.sync.RegistrySyncManager;
import net.fabricmc.fabric.impl.registry.sync.RemapException;
public class RegistrySyncTest implements ModInitializer { public class RegistrySyncTest implements ModInitializer {
private static final Logger LOGGER = LogUtils.getLogger(); private static final Logger LOGGER = LogUtils.getLogger();
@ -112,28 +103,6 @@ public class RegistrySyncTest implements ModInitializer {
// Vanilla status effects don't have an entry for the int id 0, test we can handle this. // Vanilla status effects don't have an entry for the int id 0, test we can handle this.
RegistryAttributeHolder.get(Registries.STATUS_EFFECT).addAttribute(RegistryAttribute.MODDED); RegistryAttributeHolder.get(Registries.STATUS_EFFECT).addAttribute(RegistryAttribute.MODDED);
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
dispatcher.register(CommandManager.literal("remote_remap_error_test").executes(context -> {
Map<Identifier, Object2IntMap<Identifier>> registryData = Map.of(
RegistryKeys.BLOCK.getValue(), createFakeRegistryEntries(),
RegistryKeys.ITEM.getValue(), createFakeRegistryEntries()
);
try {
RegistrySyncManager.checkRemoteRemap(registryData);
} catch (RemapException e) {
final ServerPlayerEntity player = context.getSource().getPlayer();
if (player != null) {
player.networkHandler.disconnect(Objects.requireNonNull(e.getText()));
}
return 1;
}
throw new IllegalStateException();
})));
} }
public static void checkSyncedRegistry(RegistryKey<? extends Registry<?>> registry) { public static void checkSyncedRegistry(RegistryKey<? extends Registry<?>> registry) {
@ -169,14 +138,4 @@ public class RegistrySyncTest implements ModInitializer {
} }
} }
} }
private static Object2IntMap<Identifier> createFakeRegistryEntries() {
Object2IntMap<Identifier> map = new Object2IntOpenHashMap<>();
for (int i = 0; i < 12; i++) {
map.put(Identifier.of("mod_" + i, "entry"), 0);
}
return map;
}
} }

View file

@ -17,7 +17,8 @@
"net.fabricmc.fabric.test.registry.sync.RegistrySyncTest" "net.fabricmc.fabric.test.registry.sync.RegistrySyncTest"
], ],
"client": [ "client": [
"net.fabricmc.fabric.test.registry.sync.client.DynamicRegistryClientTest" "net.fabricmc.fabric.test.registry.sync.client.DynamicRegistryClientTest",
"net.fabricmc.fabric.test.registry.sync.client.RegistrySyncClientTest"
] ]
} }
} }

View file

@ -0,0 +1,77 @@
/*
* 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.client;
import java.util.EnumSet;
import java.util.Map;
import java.util.Objects;
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
import net.minecraft.registry.RegistryKeys;
import net.minecraft.server.command.CommandManager;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback;
import net.fabricmc.fabric.api.event.registry.RegistryAttribute;
import net.fabricmc.fabric.impl.client.registry.sync.ClientRegistrySyncHandler;
import net.fabricmc.fabric.impl.registry.sync.RemapException;
import net.fabricmc.fabric.impl.registry.sync.packet.RegistryPacketHandler;
public class RegistrySyncClientTest implements ClientModInitializer {
@Override
public void onInitializeClient() {
CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) ->
dispatcher.register(CommandManager.literal("remote_remap_error_test").executes(context -> {
Map<Identifier, Object2IntMap<Identifier>> registryData = Map.of(
RegistryKeys.BLOCK.getValue(), createFakeRegistryEntries(),
RegistryKeys.ITEM.getValue(), createFakeRegistryEntries()
);
Map<Identifier, EnumSet<RegistryAttribute>> attributes = Map.of(
RegistryKeys.BLOCK.getValue(), EnumSet.noneOf(RegistryAttribute.class),
RegistryKeys.ITEM.getValue(), EnumSet.noneOf(RegistryAttribute.class)
);
try {
ClientRegistrySyncHandler.checkRemoteRemap(new RegistryPacketHandler.SyncedPacketData(registryData, attributes));
} catch (RemapException e) {
final ServerPlayerEntity player = context.getSource().getPlayer();
if (player != null) {
player.networkHandler.disconnect(Objects.requireNonNull(e.getText()));
}
return 1;
}
throw new IllegalStateException();
})));
}
private static Object2IntMap<Identifier> createFakeRegistryEntries() {
Object2IntMap<Identifier> map = new Object2IntOpenHashMap<>();
for (int i = 0; i < 12; i++) {
map.put(Identifier.of("mod_" + i, "entry"), 0);
}
return map;
}
}