Implement Sync Handler (#1214)

Co-authored-by: Martin Robertz <dream-master@gmx.net>
This commit is contained in:
Kogepan229andMartin Robertz authored and GitHub committed 2026-05-04 17:55:21 +02:00
1 parent 0ac43ad8fd
commit a2525ee44d
32 files changed
+2508 -121

No files matched your search

@@ -699,6 +699,14 @@ public abstract class AEBaseGui extends GuiContainer implements IGuiTooltipHandl
aeRenderItem.parent = null;
}
@Override
public void updateScreen() {
super.updateScreen();
if (this.inventorySlots instanceof AEBaseContainer container) {
container.tickClientSync();
}
}
@Nullable
protected Slot getSlot(final int mouseX, final int mouseY) {
final List<Slot> slots = this.getInventorySlots();
@@ -10,7 +10,6 @@
package appeng.client.gui.implementations;
import java.io.IOException;
import java.util.IdentityHashMap;
import java.util.Map;
@@ -29,7 +28,6 @@ import appeng.api.config.Settings;
import appeng.api.config.Upgrades;
import appeng.api.config.YesNo;
import appeng.api.parts.ILevelEmitter;
import appeng.api.storage.StorageName;
import appeng.api.storage.data.AEStackTypeRegistry;
import appeng.api.storage.data.IAEStackType;
import appeng.client.gui.slots.VirtualMEPhantomSlot;
@@ -39,11 +37,7 @@ import appeng.client.gui.widgets.MEGuiTextField;
import appeng.client.gui.widgets.TypeToggleButton;
import appeng.container.implementations.ContainerLevelEmitter;
import appeng.core.AEConfig;
import appeng.core.AELog;
import appeng.core.localization.GuiText;
import appeng.core.sync.network.NetworkHandler;
import appeng.core.sync.packets.PacketConfigButton;
import appeng.core.sync.packets.PacketValueConfig;
import appeng.util.calculators.ArithHelper;
import appeng.util.calculators.Calculator;
@@ -81,16 +75,17 @@ public class GuiLevelEmitter extends GuiUpgradeable {
this.amountTextField = new MEGuiTextField(90, 12);
this.amountTextField.x = this.guiLeft + 39;
this.amountTextField.y = this.guiTop + 44;
this.amountTextField.setText(Long.toString(this.container.getEmitterValue()));
this.amountTextField.setFocused(true);
((ContainerLevelEmitter) this.inventorySlots).setTextField(this.amountTextField);
this.container.setTextField(this.amountTextField);
this.validateText();
this.config = new VirtualMEPhantomSlot(
17,
42,
((ContainerLevelEmitter) inventorySlots).getLvlEmitter().getAEInventoryByName(StorageName.CONFIG),
0,
GuiLevelEmitter::acceptType);
GuiLevelEmitter::acceptType,
this.container::getConfigStack,
this.container::setConfigStack);
this.registerVirtualSlots(this.config);
}
@@ -281,17 +276,13 @@ public class GuiLevelEmitter extends GuiUpgradeable {
final boolean backwards = Mouse.isButtonDown(1);
if (btn == this.setButton && this.setButton.enabled) {
try {
final String amountString = Long.toString(this.getAmountLong());
this.amountTextField.setText(amountString);
NetworkHandler.instance.sendToServer(new PacketValueConfig("LevelEmitter.Value", amountString));
} catch (final IOException e) {
AELog.debug(e);
}
final String amountString = Long.toString(this.getAmountLong());
this.amountTextField.setText(amountString);
this.container.setLevel(Long.parseLong(amountString));
} else if (btn == this.craftingMode) {
NetworkHandler.instance.sendToServer(new PacketConfigButton(this.craftingMode.getSetting(), backwards));
this.container.rotateCraftingMode(backwards);
} else if (btn == this.levelMode) {
NetworkHandler.instance.sendToServer(new PacketConfigButton(this.levelMode.getSetting(), backwards));
this.container.rotateLevelMode(backwards);
} else {
final boolean isPlus = btn == this.plus1 || btn == this.plus10
|| btn == this.plus100
@@ -313,16 +304,9 @@ public class GuiLevelEmitter extends GuiUpgradeable {
return;
}
if (type != null) {
final boolean next = this.container.getTypeFilters().toggle(type);
tbtn.setEnabled(next);
try {
NetworkHandler.instance.sendToServer(new PacketValueConfig("LevelEmitter.TypeFilter", type.getId()));
} catch (final IOException e) {
AELog.debug(e);
}
this.container.toggleTypeFilter(type);
tbtn.setEnabled(this.container.getTypeFilters().isEnabled(type));
}
return;
}
}
@@ -35,25 +35,78 @@ public class VirtualMEPhantomSlot extends VirtualMESlot {
boolean test(VirtualMEPhantomSlot slot, IAEStackType<?> type, int mouseButton);
}
@FunctionalInterface
public interface StackGetter {
@Nullable
IAEStack<?> get();
}
@FunctionalInterface
public interface StackSetter {
void set(@Nullable IAEStack<?> stack);
}
@Nullable
private final IAEStackInventory inventory;
private final TypeAcceptPredicate acceptType;
@Nullable
private final StackGetter stackGetter;
@Nullable
private final StackSetter stackSetter;
public VirtualMEPhantomSlot(int x, int y, IAEStackInventory inventory, int slotIndex,
TypeAcceptPredicate acceptType) {
this(x, y, inventory, slotIndex, acceptType, null, null);
}
public VirtualMEPhantomSlot(int x, int y, TypeAcceptPredicate acceptType, StackGetter stackGetter,
StackSetter stackSetter) {
this(x, y, null, 0, acceptType, stackGetter, stackSetter);
}
private VirtualMEPhantomSlot(int x, int y, @Nullable IAEStackInventory inventory, int slotIndex,
TypeAcceptPredicate acceptType, @Nullable StackGetter stackGetter, @Nullable StackSetter stackSetter) {
super(x, y, slotIndex);
this.inventory = inventory;
this.showAmount = false;
this.acceptType = acceptType;
this.stackGetter = stackGetter;
this.stackSetter = stackSetter;
}
@Nullable
@Override
public IAEStack<?> getAEStack() {
return this.inventory.getAEStackInSlot(this.getSlotIndex());
if (this.stackGetter != null) {
return this.stackGetter.get();
}
return this.getInventory().getAEStackInSlot(this.getSlotIndex());
}
protected void setAEStack(@Nullable final IAEStack<?> stack) {
if (this.stackSetter != null) {
this.stackSetter.set(stack);
return;
}
this.getInventory().putAEStackInSlot(this.getSlotIndex(), stack);
NetworkHandler.instance.sendToServer(new PacketVirtualSlot(this.getStorageName(), this.getSlotIndex(), stack));
}
public StorageName getStorageName() {
return this.inventory.getStorageName();
return this.getInventory().getStorageName();
}
private IAEStackInventory getInventory() {
if (this.inventory == null) {
throw new IllegalStateException("Inventory is not available for synced virtual slots");
}
return this.inventory;
}
/**
@@ -150,10 +203,7 @@ public class VirtualMEPhantomSlot extends VirtualMESlot {
}
// Set on the client to avoid lag on slow networks
inventory.putAEStackInSlot(this.getSlotIndex(), currentStack);
NetworkHandler.instance
.sendToServer(new PacketVirtualSlot(this.getStorageName(), this.getSlotIndex(), currentStack));
this.setAEStack(currentStack);
}
@Override
@@ -75,6 +75,9 @@ import appeng.container.slot.SlotInaccessible;
import appeng.container.slot.SlotPatternTerm;
import appeng.container.slot.SlotPlayerHotBar;
import appeng.container.slot.SlotPlayerInv;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncRegistrar;
import appeng.core.AEConfig;
import appeng.core.AELog;
import appeng.core.localization.PlayerMessages;
@@ -131,6 +134,7 @@ public abstract class AEBaseContainer extends Container {
* GuiSync.Recurse requires lazy initialization
*/
private DataSynchronization dataSync;
private final SyncManager syncManager = new SyncManager(this);
@Deprecated
public AEBaseContainer(final InventoryPlayer ip, final TileEntity myTile, final IPart myPart) {
@@ -363,6 +367,29 @@ public abstract class AEBaseContainer extends Container {
}
}
public final SyncManager getSyncManager() {
return this.syncManager;
}
/**
* Returns the root registrar for the container synchronization system.
* <p>
* Call this from container constructors to register explicit synchronization handlers with stable, unique keys.
* Handlers must be registered before synchronization starts. See {@link appeng.container.sync.SyncRegistrar} for
* the available handler types.
*/
protected final SyncRegistrar syncRegistrar() {
return this.syncManager.root();
}
public final void receiveSyncData(final SyncEndpoint remoteEndpoint, final ByteBuf buf) {
this.syncManager.readIncoming(remoteEndpoint, buf);
}
public final void tickClientSync() {
this.syncManager.tickClient();
}
protected void bindPlayerInventory(final InventoryPlayer inventoryPlayer, final int offsetX, final int offsetY) {
// bind player inventory
for (int i = 0; i < 3; i++) {
@@ -413,6 +440,8 @@ public abstract class AEBaseContainer extends Container {
if (this.dataSync.hasChanges() && this.invPlayer.player instanceof EntityPlayerMP playerMP) {
NetworkHandler.instance.sendTo(new PacketGuiDataSync(this.dataSync::writeChanges), playerMP);
}
this.syncManager.tickServer();
}
portableSourceTick();
@@ -11,6 +11,14 @@ import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.ObjectObjectImmutablePair;
/**
* Legacy reflection-based synchronization for fields annotated with {@link GuiSync}.
*
* @deprecated Use {@link appeng.container.AEBaseContainer#syncRegistrar()} and
* {@link appeng.container.sync.SyncRegistrar} to register sync handlers explicitly. See
* {@link appeng.container.implementations.ContainerLevelEmitter} for an example.
*/
@Deprecated
public class DataSynchronization {
private final Map<Integer, SynchronizedField<?>> fields = new HashMap<>();
@@ -17,7 +17,12 @@ import java.lang.annotation.Target;
/**
* Annotates that this field should be synchronized between the server and client.
*
* @deprecated Use {@link appeng.container.AEBaseContainer#syncRegistrar()} and
* {@link appeng.container.sync.SyncRegistrar} to register sync handlers explicitly. See
* {@link appeng.container.implementations.ContainerLevelEmitter} for an example.
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface GuiSync {
@@ -26,7 +31,10 @@ public @interface GuiSync {
/**
* Recurse into the class in search of more @GuiSync-ed values. The child IDs are offset by the value.
*
* @deprecated Use {@link appeng.container.sync.SyncRegistrar#child(String)} to create scoped handler keys.
*/
@Deprecated
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public static @interface Recurse {
@@ -6,7 +6,13 @@ import io.netty.buffer.ByteBuf;
* Implement on classes to signal they can be synchronized to the client using {@link GuiSync}. <br/>
* For this to work fully, the class also needs to have a public constructor that takes a {@link ByteBuf} argument and
* be implemented equals to detect changes.
*
* @deprecated Use {@link appeng.container.sync.SyncCodec} and {@link appeng.container.sync.SyncCodecs}, and register
* them via {@link appeng.container.sync.SyncRegistrar}. See
* {@link appeng.container.sync.codecs.AEStackTypeFilterSyncCodec} and
* {@link appeng.container.implementations.ContainerLevelEmitter} for examples.
*/
@Deprecated
public interface IGuiPacketWritable {
void writeToPacket(ByteBuf buf);
@@ -10,15 +10,15 @@
package appeng.container.implementations;
import static appeng.util.Platform.isServer;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.IInventory;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.api.config.FuzzyMode;
import appeng.api.config.LevelType;
import appeng.api.config.RedstoneMode;
@@ -27,49 +27,88 @@ import appeng.api.config.Settings;
import appeng.api.config.YesNo;
import appeng.api.parts.ILevelEmitter;
import appeng.api.storage.StorageName;
import appeng.api.storage.data.AEStackTypeRegistry;
import appeng.api.storage.data.IAEStack;
import appeng.api.storage.data.IAEStackType;
import appeng.client.gui.IGuiSub;
import appeng.client.gui.implementations.GuiLevelEmitter;
import appeng.client.gui.widgets.MEGuiTextField;
import appeng.container.PrimaryGui;
import appeng.container.guisync.GuiSync;
import appeng.container.interfaces.IContainerSubGui;
import appeng.container.interfaces.IVirtualSlotHolder;
import appeng.container.slot.SlotInaccessible;
import appeng.container.slot.SlotRestrictedInput;
import appeng.container.sync.SyncCodecs;
import appeng.container.sync.SyncRegistrar;
import appeng.container.sync.codecs.AEStackTypeFilterSyncCodec;
import appeng.container.sync.deltas.TypeFilterDelta;
import appeng.container.sync.handlers.ConfigEnumSyncHandler;
import appeng.container.sync.handlers.DeltaObjectSyncHandler;
import appeng.container.sync.handlers.LongSyncHandler;
import appeng.container.sync.handlers.ObjectSyncHandler;
import appeng.tile.inventory.AppEngInternalInventory;
import appeng.util.AEStackTypeFilter;
import appeng.util.Platform;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
public class ContainerLevelEmitter extends ContainerUpgradeable implements IVirtualSlotHolder, IContainerSubGui {
public class ContainerLevelEmitter extends ContainerUpgradeable implements IContainerSubGui {
private final ILevelEmitter lvlEmitter;
@SideOnly(Side.CLIENT)
private MEGuiTextField textField;
@GuiSync(2)
public LevelType lvType;
@GuiSync(3)
public long EmitterValue = -1;
@GuiSync(4)
public YesNo cmType;
@GuiSync(10)
public AEStackTypeFilter typeFilters;
private final IAEStack<?>[] configClientSlot = new IAEStack[1];
private final LongSyncHandler emitterValueSync;
private final DeltaObjectSyncHandler<@NotNull AEStackTypeFilter, TypeFilterDelta> typeFiltersSync;
private final ConfigEnumSyncHandler<@NotNull LevelType> levelModeSync;
private final ConfigEnumSyncHandler<@NotNull YesNo> craftingModeSync;
private final ObjectSyncHandler<@Nullable IAEStack<?>> configStackSync;
public ContainerLevelEmitter(final InventoryPlayer ip, final ILevelEmitter te) {
super(ip, te);
this.lvlEmitter = te;
this.typeFilters = new AEStackTypeFilter(this.lvlEmitter.getTypeFilters());
final SyncRegistrar sync = this.syncRegistrar();
this.emitterValueSync = sync.longSync("emitterValue").onClientChange((oldValue, newValue) -> {
if (this.textField != null) {
this.textField.setText(String.valueOf(newValue));
this.textField.setCursorPositionEnd();
}
}).onServerChange((oldValue, newValue) -> this.lvlEmitter.setReportingValue(newValue));
this.typeFiltersSync = sync
.object("typeFilters", AEStackTypeFilterSyncCodec.INSTANCE, this.lvlEmitter.getTypeFilters())
.onClientChange((oldValue, newValue) -> {
if (Minecraft.getMinecraft().currentScreen instanceof GuiLevelEmitter guiLevelEmitter) {
guiLevelEmitter.onUpdateTypeFilters();
}
}).onServerChange((oldValue, newValue) -> {
if (newValue == null) {
return;
}
this.lvlEmitter.getTypeFilters().copyFrom(newValue);
this.lvlEmitter.onChangeTypeFilters();
});
this.levelModeSync = sync.configEnum(
"levelMode",
Settings.LEVEL_TYPE,
LevelType.class,
this.getUpgradeable().getConfigManager());
this.craftingModeSync = sync.configEnum(
"craftingMode",
Settings.CRAFT_VIA_REDSTONE,
YesNo.class,
this.getUpgradeable().getConfigManager());
this.configStackSync = sync
.object(
"configStack",
SyncCodecs.aeStack(),
this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG).getAEStackInSlot(0))
.onServerChange((oldValue, newValue) -> {
final IAEStack<?> stack = newValue == null ? null : newValue.copy();
this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG).putAEStackInSlot(0, stack);
});
// sub gui copy paste
this.primaryGuiButtonIcon = new SlotInaccessible(new AppEngInternalInventory(null, 1), 0, 0, -9000);
@@ -81,11 +120,6 @@ public class ContainerLevelEmitter extends ContainerUpgradeable implements IVirt
this.textField = level;
}
public void setLevel(final long l, final EntityPlayer player) {
this.lvlEmitter.setReportingValue(l);
this.EmitterValue = l;
}
@Override
protected void setupConfig() {
final IInventory upgrades = this.getUpgradeable().getInventoryByName("upgrades");
@@ -138,7 +172,6 @@ public class ContainerLevelEmitter extends ContainerUpgradeable implements IVirt
@Override
public int availableUpgrades() {
return 1;
}
@@ -147,94 +180,68 @@ public class ContainerLevelEmitter extends ContainerUpgradeable implements IVirt
this.verifyPermissions(SecurityPermissions.BUILD, false);
if (Platform.isServer()) {
this.EmitterValue = this.lvlEmitter.getReportingValue();
this.setCraftingMode(
(YesNo) this.getUpgradeable().getConfigManager().getSetting(Settings.CRAFT_VIA_REDSTONE));
this.setLevelMode((LevelType) this.getUpgradeable().getConfigManager().getSetting(Settings.LEVEL_TYPE));
this.emitterValueSync.set(this.lvlEmitter.getReportingValue());
this.typeFiltersSync.set(this.lvlEmitter.getTypeFilters());
this.craftingModeSync.syncFromConfig();
this.levelModeSync.syncFromConfig();
this.setFuzzyMode((FuzzyMode) this.getUpgradeable().getConfigManager().getSetting(Settings.FUZZY_MODE));
this.setRedStoneMode(
(RedstoneMode) this.getUpgradeable().getConfigManager().getSetting(Settings.REDSTONE_EMITTER));
this.updateVirtualSlots(
StorageName.CONFIG,
this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG),
this.configClientSlot);
this.configStackSync.set(this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG).getAEStackInSlot(0));
}
this.standardDetectAndSendChanges();
}
@Override
public void onUpdate(final String field, final Object oldValue, final Object newValue) {
if (field.equals("EmitterValue")) {
if (this.textField != null) {
this.textField.setText(String.valueOf(this.EmitterValue));
public long getEmitterValue() {
return this.emitterValueSync.get();
}
if (String.valueOf(oldValue).equals("-1")) {
this.textField.setCursorPositionEnd();
}
}
} else if (field.equals("typeFilters")) {
if (Minecraft.getMinecraft().currentScreen instanceof GuiLevelEmitter guiLevelEmitter) {
guiLevelEmitter.onUpdateTypeFilters();
}
}
public void setLevel(final long l) {
this.emitterValueSync.set(l);
}
@Override
public YesNo getCraftingMode() {
return this.cmType;
return this.craftingModeSync.get();
}
@Override
public void setCraftingMode(final YesNo cmType) {
this.cmType = cmType;
this.craftingModeSync.set(cmType);
}
public void rotateCraftingMode(final boolean backwards) {
this.craftingModeSync.rotate(backwards);
}
public LevelType getLevelMode() {
return this.lvType;
return this.levelModeSync.get();
}
private void setLevelMode(final LevelType lvType) {
this.lvType = lvType;
public void rotateLevelMode(final boolean backwards) {
this.levelModeSync.rotate(backwards);
}
@NotNull
public AEStackTypeFilter getTypeFilters() {
return this.typeFilters;
return this.typeFiltersSync.get();
}
public void toggleTypeFilter(@NotNull final IAEStackType<?> type) {
this.typeFiltersSync.applyAndQueueDelta(TypeFilterDelta.toggle(type));
}
public ILevelEmitter getLvlEmitter() {
return this.lvlEmitter;
}
public void toggleTypeFilter(String typeId, EntityPlayer player) {
if (typeId == null || typeId.isEmpty()) {
return;
}
final IAEStackType<?> type = AEStackTypeRegistry.getType(typeId);
if (type == null) {
return;
}
this.lvlEmitter.getTypeFilters().toggle(type);
// Create a new instance so that GuiSync can detect changes
this.typeFilters = new AEStackTypeFilter(this.lvlEmitter.getTypeFilters());
this.lvlEmitter.onChangeTypeFilters();
public @Nullable IAEStack<?> getConfigStack() {
return this.configStackSync.get();
}
@Override
public void receiveSlotStacks(StorageName invName, Int2ObjectMap<IAEStack<?>> slotStacks) {
for (var entry : slotStacks.int2ObjectEntrySet()) {
this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG)
.putAEStackInSlot(entry.getIntKey(), entry.getValue());
}
if (isServer()) {
this.updateVirtualSlots(
StorageName.CONFIG,
this.lvlEmitter.getAEInventoryByName(StorageName.CONFIG),
this.configClientSlot);
}
public void setConfigStack(@Nullable final IAEStack<?> stack) {
this.configStackSync.set(stack);
}
// for level terminal
@@ -0,0 +1,119 @@
package appeng.container.sync;
import java.io.IOException;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import io.netty.buffer.ByteBuf;
public abstract class AbstractSyncHandler {
protected final SyncManager manager;
protected final String key;
protected final String fullKey;
protected final SyncDirection direction;
protected boolean dirty;
private int id = -1;
private boolean fullResyncRequested;
private boolean remoteFullResyncRequested;
protected AbstractSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction) {
this.manager = Objects.requireNonNull(manager, "manager");
this.key = Objects.requireNonNull(key, "key");
this.fullKey = Objects.requireNonNull(fullKey, "fullKey");
this.direction = Objects.requireNonNull(direction, "direction");
}
public final String getKey() {
return this.key;
}
public final String getFullKey() {
return this.fullKey;
}
public final int getId() {
if (this.id < 0) {
throw new IllegalStateException("Sync handler ID has not been assigned yet: " + this.fullKey);
}
return this.id;
}
public final SyncDirection getDirection() {
return this.direction;
}
public final boolean canSendFrom(final SyncEndpoint endpoint) {
return this.direction.canSendFrom(endpoint);
}
public final boolean sendsInitialStateFrom(final SyncEndpoint endpoint) {
return this.direction.sendsInitialStateFrom(endpoint);
}
public final void readIncoming(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode == SyncMode.REQUEST_FULL) {
this.requestFullResync();
return;
}
if (!this.direction.canReceiveFrom(remoteEndpoint)) {
throw new IOException("Handler " + this.fullKey + " does not accept updates from " + remoteEndpoint + ".");
}
this.readUpdate(remoteEndpoint, mode, buf);
}
protected final void markDirtyInternal() {
this.dirty = true;
}
protected final void requestFullResync() {
this.fullResyncRequested = true;
this.dirty = true;
}
protected final void requestRemoteFullResync() {
this.remoteFullResyncRequested = true;
}
protected final boolean shouldForceFullResync() {
return this.fullResyncRequested;
}
protected final void clearFullResyncRequest() {
this.fullResyncRequested = false;
}
final boolean shouldRequestRemoteFullResync() {
return this.remoteFullResyncRequested;
}
final void clearRemoteFullResyncRequest() {
this.remoteFullResyncRequested = false;
}
final void assignId(final int id) {
if (id < 0) {
throw new IllegalArgumentException("Handler ID must be non-negative.");
}
if (this.id >= 0 && this.id != id) {
throw new IllegalStateException(
"Sync handler " + this.fullKey + " was already assigned a different ID: " + this.id);
}
this.id = id;
}
@Nullable
protected abstract SyncMode writeUpdate(SyncEndpoint localEndpoint, boolean initialSync, ByteBuf buf)
throws IOException;
protected abstract void readUpdate(SyncEndpoint remoteEndpoint, SyncMode mode, ByteBuf buf) throws IOException;
protected abstract String getTypeKey();
}
@@ -0,0 +1,37 @@
package appeng.container.sync;
import java.io.IOException;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import io.netty.buffer.ByteBuf;
/**
* Extends {@link SyncCodec} with an optional delta format for mutable values.
* <p>
* Delta updates are an optimization only. Callers must still be able to fall back to a full sync whenever a delta
* cannot be produced or cannot be applied.
*/
public interface DeltaSyncCodec<T, D> extends SyncCodec<T> {
/**
* Returns deltas from {@code previous} to {@code current}, or {@code null} or an empty list when this change should
* be sent as a full update instead.
*/
@Nullable
List<D> diff(T previous, T current);
void writeDelta(ByteBuf buf, D delta) throws IOException;
/**
* Applies one delta payload to {@code base} and returns the updated value.
*/
T applyDelta(@Nullable T base, ByteBuf buf) throws IOException;
default T applyDeltaLocally(@Nullable final T base, final D delta) throws IOException {
final ByteBuf buf = io.netty.buffer.Unpooled.buffer();
this.writeDelta(buf, delta);
return this.applyDelta(base, buf);
}
}
@@ -0,0 +1,47 @@
package appeng.container.sync;
import java.io.IOException;
import io.netty.buffer.ByteBuf;
/**
* Encodes and decodes values synchronized by {@link SyncRegistrar} object handlers.
* <p>
* Implementations define both the packet format and the value semantics used by the synchronization system to detect
* changes. Use {@link SyncCodecs} for common codecs and factory methods.
*/
public interface SyncCodec<T> {
@FunctionalInterface
interface Reader<T> {
T read(ByteBuf buf) throws IOException;
}
@FunctionalInterface
interface Writer<T> {
void write(ByteBuf buf, T value) throws IOException;
}
void write(ByteBuf buf, T value) throws IOException;
T read(ByteBuf buf) throws IOException;
/**
* Returns an isolated copy that can be retained as the last synchronized value.
*/
T copy(T value);
/**
* Returns whether two values should be treated as equal for change detection.
*/
boolean valuesEqual(T a, T b);
/**
* Returns the stable type identifier used when validating that both sides registered compatible handlers.
*/
default String getTypeKey() {
return this.getClass().getName();
}
}
@@ -0,0 +1,82 @@
package appeng.container.sync;
import java.util.Objects;
import java.util.function.BiPredicate;
import java.util.function.UnaryOperator;
import appeng.api.storage.data.IAEStack;
import appeng.container.guisync.IGuiPacketWritable;
import io.netty.buffer.ByteBuf;
public final class SyncCodecs {
private static final SyncCodec<IAEStack<?>> AE_STACK_CODEC = of(
IAEStack.class.getName(),
IAEStack::writeToPacketGeneric,
IAEStack::fromPacketGeneric,
value -> value == null ? null : value.copy(),
Objects::equals);
private SyncCodecs() {}
public static <T> SyncCodec<T> of(final String typeKey, final SyncCodec.Writer<T> writer,
final SyncCodec.Reader<T> reader, final UnaryOperator<T> copier, final BiPredicate<T, T> equality) {
Objects.requireNonNull(typeKey, "typeKey");
Objects.requireNonNull(writer, "writer");
Objects.requireNonNull(reader, "reader");
Objects.requireNonNull(copier, "copier");
Objects.requireNonNull(equality, "equality");
return new SyncCodec<>() {
@Override
public void write(final ByteBuf buf, final T value) throws java.io.IOException {
writer.write(buf, value);
}
@Override
public T read(final ByteBuf buf) throws java.io.IOException {
return reader.read(buf);
}
@Override
public T copy(final T value) {
return copier.apply(value);
}
@Override
public boolean valuesEqual(final T a, final T b) {
return equality.test(a, b);
}
@Override
public String getTypeKey() {
return typeKey;
}
};
}
public static <T extends IGuiPacketWritable> SyncCodec<T> packetWritable(final Class<T> type,
final SyncCodec.Reader<T> reader, final UnaryOperator<T> copier, final BiPredicate<T, T> equality) {
Objects.requireNonNull(type, "type");
return of(type.getName(), (buf, value) -> value.writeToPacket(buf), reader, copier, equality);
}
public static <E extends Enum<E>> SyncCodec<E> enumValue(final Class<E> enumClass) {
Objects.requireNonNull(enumClass, "enumClass");
final E[] values = enumClass.getEnumConstants();
return of(enumClass.getName(), (buf, value) -> buf.writeInt(value.ordinal()), buf -> {
final int ordinal = buf.readInt();
if (ordinal < 0 || ordinal >= values.length) {
throw new java.io.IOException("Invalid enum ordinal " + ordinal + " for " + enumClass.getName());
}
return values[ordinal];
}, UnaryOperator.identity(), Objects::equals);
}
public static SyncCodec<IAEStack<?>> aeStack() {
return AE_STACK_CODEC;
}
}
@@ -0,0 +1,24 @@
package appeng.container.sync;
public enum SyncDirection {
SERVER_TO_CLIENT,
CLIENT_TO_SERVER,
BIDIRECTIONAL;
public boolean canSendFrom(final SyncEndpoint endpoint) {
return switch (this) {
case SERVER_TO_CLIENT -> endpoint == SyncEndpoint.SERVER;
case CLIENT_TO_SERVER -> endpoint == SyncEndpoint.CLIENT;
case BIDIRECTIONAL -> true;
};
}
public boolean canReceiveFrom(final SyncEndpoint remoteEndpoint) {
return this.canSendFrom(remoteEndpoint);
}
public boolean sendsInitialStateFrom(final SyncEndpoint endpoint) {
return endpoint == SyncEndpoint.SERVER && this != CLIENT_TO_SERVER;
}
}
@@ -0,0 +1,9 @@
package appeng.container.sync;
/**
* Identifies which side of the container sync channel a message is being sent from or received from.
*/
public enum SyncEndpoint {
CLIENT,
SERVER
}
@@ -0,0 +1,445 @@
package appeng.container.sync;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import net.minecraft.entity.player.EntityPlayerMP;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.api.config.Settings;
import appeng.api.util.IConfigManager;
import appeng.container.AEBaseContainer;
import appeng.container.sync.handlers.AEStackInventorySyncHandler;
import appeng.container.sync.handlers.BooleanSyncHandler;
import appeng.container.sync.handlers.ConfigEnumSyncHandler;
import appeng.container.sync.handlers.DeltaObjectSyncHandler;
import appeng.container.sync.handlers.DoubleSyncHandler;
import appeng.container.sync.handlers.IntSyncHandler;
import appeng.container.sync.handlers.LongSyncHandler;
import appeng.container.sync.handlers.ObjectSyncHandler;
import appeng.core.AELog;
import appeng.core.sync.network.NetworkHandler;
import appeng.core.sync.packets.PacketContainerSync;
import appeng.tile.inventory.IAEStackInventory;
import appeng.util.Platform;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
public final class SyncManager {
private final AEBaseContainer owner;
private final List<AbstractSyncHandler> handlers = new ArrayList<>();
private final Object2ObjectMap<String, AbstractSyncHandler> handlersByFullKey = new Object2ObjectOpenHashMap<>();
private final Int2ObjectMap<AbstractSyncHandler> handlersById = new Int2ObjectOpenHashMap<>();
private final SyncRegistrar rootRegistrar = new ScopedSyncRegistrar("");
private boolean initialServerSyncSent;
private boolean frozen;
private int layoutHash;
public SyncManager(final AEBaseContainer owner) {
this.owner = Objects.requireNonNull(owner, "owner");
}
public SyncRegistrar root() {
return this.rootRegistrar;
}
public void freezeLayout() {
if (this.frozen) {
return;
}
final List<AbstractSyncHandler> sortedHandlers = new ArrayList<>(this.handlers);
sortedHandlers.sort(Comparator.comparing(AbstractSyncHandler::getFullKey));
this.handlersById.clear();
int nextId = 0;
int hash = 1;
for (final AbstractSyncHandler handler : sortedHandlers) {
handler.assignId(nextId);
this.handlersById.put(nextId, handler);
nextId++;
hash = 31 * hash + handler.getFullKey().hashCode();
hash = 31 * hash + handler.getDirection().ordinal();
hash = 31 * hash + handler.getTypeKey().hashCode();
}
this.layoutHash = hash != 0 ? hash : 1;
this.frozen = true;
}
public void tickServer() {
if (!Platform.isServer()) {
return;
}
this.freezeLayout();
if (!this.initialServerSyncSent) {
this.sendUpdates(SyncEndpoint.SERVER, true);
this.initialServerSyncSent = true;
}
this.sendUpdates(SyncEndpoint.SERVER, false);
}
public void tickClient() {
if (!Platform.isClient()) {
return;
}
this.freezeLayout();
this.sendUpdates(SyncEndpoint.CLIENT, false);
}
public void readIncoming(final SyncEndpoint remoteEndpoint, final ByteBuf buf) {
this.freezeLayout();
try {
final int remoteLayoutHash = buf.readInt();
final boolean initialSync = buf.readBoolean();
if (remoteLayoutHash != this.layoutHash) {
AELog.warn(
"Rejected container sync for %s due to layout mismatch. local=%d remote=%d side=%s initial=%s",
this.owner.getClass().getName(),
this.layoutHash,
remoteLayoutHash,
remoteEndpoint,
initialSync);
return;
}
while (true) {
final int handlerId = buf.readInt();
if (handlerId == -1) {
return;
}
final AbstractSyncHandler handler = this.handlersById.get(handlerId);
if (handler == null) {
AELog.warn(
"Rejected container sync for %s due to unknown handler id %d.",
this.owner.getClass().getName(),
handlerId);
return;
}
final SyncMode mode = SyncMode.fromOrdinal(buf.readByte());
handler.readIncoming(remoteEndpoint, mode, buf);
}
} catch (final IOException e) {
AELog.warn(e, String.format("Failed to read container sync for %s.", this.owner.getClass().getName()));
}
}
private void sendUpdates(final SyncEndpoint localEndpoint, final boolean initialSync) {
if (this.handlers.isEmpty()) {
return;
}
final ByteBuf data = Unpooled.buffer();
data.writeInt(this.layoutHash);
data.writeBoolean(initialSync);
int updates = 0;
for (final AbstractSyncHandler handler : this.handlers) {
if (handler.shouldRequestRemoteFullResync()) {
updates++;
data.writeInt(handler.getId());
data.writeByte(SyncMode.REQUEST_FULL.ordinal());
handler.clearRemoteFullResyncRequest();
}
final ByteBuf payload = Unpooled.buffer();
try {
final SyncMode mode = handler.writeUpdate(localEndpoint, initialSync, payload);
if (mode == null) {
continue;
}
updates++;
data.writeInt(handler.getId());
data.writeByte(mode.ordinal());
data.writeBytes(payload, payload.readerIndex(), payload.readableBytes());
} catch (final IOException e) {
AELog.warn(
e,
String.format(
"Failed to serialize sync handler %s on %s.",
handler.getFullKey(),
localEndpoint));
}
}
if (updates == 0) {
return;
}
data.writeInt(-1);
final PacketContainerSync packet = new PacketContainerSync(this.owner.windowId, data);
if (localEndpoint == SyncEndpoint.SERVER) {
if (this.owner.getInventoryPlayer().player instanceof EntityPlayerMP playerMP) {
NetworkHandler.instance.sendTo(packet, playerMP);
}
} else {
NetworkHandler.instance.sendToServer(packet);
}
}
private <T extends AbstractSyncHandler> T register(final T handler) {
if (this.frozen) {
throw new IllegalStateException("Cannot register sync handler after synchronization has started.");
}
final AbstractSyncHandler existing = this.handlersByFullKey.putIfAbsent(handler.getFullKey(), handler);
if (existing != null) {
throw new IllegalArgumentException("Duplicate sync fullKey: " + handler.getFullKey());
}
this.handlers.add(handler);
return handler;
}
private static void validateKey(final String key) {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Sync key must not be empty.");
}
if (key.indexOf('.') >= 0) {
throw new IllegalArgumentException("Sync key must not contain '.': " + key);
}
}
private final class ScopedSyncRegistrar implements SyncRegistrar {
private final String scope;
private ScopedSyncRegistrar(final String scope) {
this.scope = scope;
}
@Override
public @NotNull SyncRegistrar child(final @NotNull String key) {
validateKey(key);
return new ScopedSyncRegistrar(this.qualify(key));
}
@Override
public @NotNull IntSyncHandler intS2C(final @NotNull String key) {
return register(
new IntSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.SERVER_TO_CLIENT));
}
@Override
public @NotNull IntSyncHandler intC2S(final @NotNull String key) {
return register(
new IntSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.CLIENT_TO_SERVER));
}
@Override
public @NotNull IntSyncHandler intSync(final @NotNull String key) {
return register(new IntSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.BIDIRECTIONAL));
}
@Override
public @NotNull LongSyncHandler longS2C(final @NotNull String key) {
return register(
new LongSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.SERVER_TO_CLIENT));
}
@Override
public @NotNull LongSyncHandler longC2S(final @NotNull String key) {
return register(
new LongSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.CLIENT_TO_SERVER));
}
@Override
public @NotNull LongSyncHandler longSync(final @NotNull String key) {
return register(new LongSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.BIDIRECTIONAL));
}
@Override
public @NotNull BooleanSyncHandler booleanS2C(final @NotNull String key) {
return register(
new BooleanSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.SERVER_TO_CLIENT));
}
@Override
public @NotNull BooleanSyncHandler booleanC2S(final @NotNull String key) {
return register(
new BooleanSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.CLIENT_TO_SERVER));
}
@Override
public @NotNull BooleanSyncHandler booleanSync(final @NotNull String key) {
return register(
new BooleanSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.BIDIRECTIONAL));
}
@Override
public @NotNull DoubleSyncHandler doubleS2C(final @NotNull String key) {
return register(
new DoubleSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.SERVER_TO_CLIENT));
}
@Override
public @NotNull DoubleSyncHandler doubleC2S(final @NotNull String key) {
return register(
new DoubleSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.CLIENT_TO_SERVER));
}
@Override
public @NotNull DoubleSyncHandler doubleSync(final @NotNull String key) {
return register(
new DoubleSyncHandler(SyncManager.this, key, this.qualify(key), SyncDirection.BIDIRECTIONAL));
}
@Override
public <T> @NotNull ObjectSyncHandler<T> objectS2C(final @NotNull String key, final @NotNull SyncCodec<T> codec,
final @Nullable T initialValue) {
return register(
new ObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.SERVER_TO_CLIENT,
codec,
initialValue));
}
@Override
public <T, D> @NotNull DeltaObjectSyncHandler<T, D> objectS2C(final @NotNull String key,
final @NotNull DeltaSyncCodec<T, D> codec, final @Nullable T initialValue) {
return register(
new DeltaObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.SERVER_TO_CLIENT,
codec,
initialValue));
}
@Override
public <T> @NotNull ObjectSyncHandler<T> objectC2S(final @NotNull String key, final @NotNull SyncCodec<T> codec,
final @Nullable T initialValue) {
return register(
new ObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.CLIENT_TO_SERVER,
codec,
initialValue));
}
@Override
public <T, D> @NotNull DeltaObjectSyncHandler<T, D> objectC2S(final @NotNull String key,
final @NotNull DeltaSyncCodec<T, D> codec, final @Nullable T initialValue) {
return register(
new DeltaObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.CLIENT_TO_SERVER,
codec,
initialValue));
}
@Override
public <T> @NotNull ObjectSyncHandler<T> object(final @NotNull String key, final @NotNull SyncCodec<T> codec,
final @Nullable T initialValue) {
return register(
new ObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.BIDIRECTIONAL,
codec,
initialValue));
}
@Override
public <T, D> @NotNull DeltaObjectSyncHandler<T, D> object(final @NotNull String key,
final @NotNull DeltaSyncCodec<T, D> codec, final @Nullable T initialValue) {
return register(
new DeltaObjectSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.BIDIRECTIONAL,
codec,
initialValue));
}
@Override
public <E extends Enum<E>> @NotNull ConfigEnumSyncHandler<E> configEnum(final @NotNull String key,
final @NotNull Settings setting, final @NotNull Class<E> enumClass,
final @NotNull IConfigManager configManager) {
return register(
new ConfigEnumSyncHandler<>(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.BIDIRECTIONAL,
setting,
enumClass,
configManager,
enumClass.cast(configManager.getSetting(setting))));
}
@Override
public @NotNull AEStackInventorySyncHandler aeStackInventoryS2C(final @NotNull String key,
final @NotNull IAEStackInventory inventory) {
return register(
new AEStackInventorySyncHandler(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.SERVER_TO_CLIENT,
inventory));
}
@Override
public @NotNull AEStackInventorySyncHandler aeStackInventoryC2S(final @NotNull String key,
final @NotNull IAEStackInventory inventory) {
return register(
new AEStackInventorySyncHandler(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.CLIENT_TO_SERVER,
inventory));
}
@Override
public @NotNull AEStackInventorySyncHandler aeStackInventory(final @NotNull String key,
final @NotNull IAEStackInventory inventory) {
return register(
new AEStackInventorySyncHandler(
SyncManager.this,
key,
this.qualify(key),
SyncDirection.BIDIRECTIONAL,
inventory));
}
private String qualify(final String key) {
validateKey(key);
return this.scope.isEmpty() ? key : this.scope + "." + key;
}
}
}
@@ -0,0 +1,18 @@
package appeng.container.sync;
import java.io.IOException;
public enum SyncMode {
FULL,
DELTA,
REQUEST_FULL;
public static SyncMode fromOrdinal(final int ordinal) throws IOException {
final SyncMode[] values = values();
if (ordinal < 0 || ordinal >= values.length) {
throw new IOException("Unknown sync mode ordinal: " + ordinal);
}
return values[ordinal];
}
}
@@ -0,0 +1,95 @@
package appeng.container.sync;
import org.jetbrains.annotations.NotNull;
import appeng.api.config.Settings;
import appeng.api.util.IConfigManager;
import appeng.container.sync.handlers.AEStackInventorySyncHandler;
import appeng.container.sync.handlers.BooleanSyncHandler;
import appeng.container.sync.handlers.ConfigEnumSyncHandler;
import appeng.container.sync.handlers.DeltaObjectSyncHandler;
import appeng.container.sync.handlers.DoubleSyncHandler;
import appeng.container.sync.handlers.IntSyncHandler;
import appeng.container.sync.handlers.LongSyncHandler;
import appeng.container.sync.handlers.ObjectSyncHandler;
import appeng.tile.inventory.IAEStackInventory;
public interface SyncRegistrar {
/**
* Creates a child registrar whose handlers are registered under the given key as an additional fullKey scope
* segment.
*/
@NotNull
SyncRegistrar child(@NotNull String key);
@NotNull
IntSyncHandler intS2C(@NotNull String key);
@NotNull
IntSyncHandler intC2S(@NotNull String key);
@NotNull
IntSyncHandler intSync(@NotNull String key);
@NotNull
LongSyncHandler longS2C(@NotNull String key);
@NotNull
LongSyncHandler longC2S(@NotNull String key);
@NotNull
LongSyncHandler longSync(@NotNull String key);
@NotNull
BooleanSyncHandler booleanS2C(@NotNull String key);
@NotNull
BooleanSyncHandler booleanC2S(@NotNull String key);
@NotNull
BooleanSyncHandler booleanSync(@NotNull String key);
@NotNull
DoubleSyncHandler doubleS2C(@NotNull String key);
@NotNull
DoubleSyncHandler doubleC2S(@NotNull String key);
@NotNull
DoubleSyncHandler doubleSync(@NotNull String key);
@NotNull
<T, D> DeltaObjectSyncHandler<T, D> objectS2C(@NotNull String key, @NotNull DeltaSyncCodec<T, D> codec,
T initialValue);
@NotNull
<T> ObjectSyncHandler<T> objectS2C(@NotNull String key, @NotNull SyncCodec<T> codec, T initialValue);
@NotNull
<T, D> DeltaObjectSyncHandler<T, D> objectC2S(@NotNull String key, @NotNull DeltaSyncCodec<T, D> codec,
T initialValue);
@NotNull
<T> ObjectSyncHandler<T> objectC2S(@NotNull String key, @NotNull SyncCodec<T> codec, T initialValue);
@NotNull
<T, D> DeltaObjectSyncHandler<T, D> object(@NotNull String key, @NotNull DeltaSyncCodec<T, D> codec,
T initialValue);
@NotNull
<T> ObjectSyncHandler<T> object(@NotNull String key, @NotNull SyncCodec<T> codec, T initialValue);
@NotNull
<E extends Enum<E>> ConfigEnumSyncHandler<E> configEnum(@NotNull String key, @NotNull Settings setting,
@NotNull Class<E> enumClass, @NotNull IConfigManager configManager);
@NotNull
AEStackInventorySyncHandler aeStackInventoryS2C(@NotNull String key, @NotNull IAEStackInventory inventory);
@NotNull
AEStackInventorySyncHandler aeStackInventoryC2S(@NotNull String key, @NotNull IAEStackInventory inventory);
@NotNull
AEStackInventorySyncHandler aeStackInventory(@NotNull String key, @NotNull IAEStackInventory inventory);
}
@@ -0,0 +1,100 @@
package appeng.container.sync.codecs;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.Nullable;
import appeng.api.storage.data.AEStackTypeRegistry;
import appeng.api.storage.data.IAEStackType;
import appeng.container.sync.DeltaSyncCodec;
import appeng.container.sync.deltas.TypeFilterDelta;
import appeng.util.AEStackTypeFilter;
import io.netty.buffer.ByteBuf;
public final class AEStackTypeFilterSyncCodec implements DeltaSyncCodec<AEStackTypeFilter, TypeFilterDelta> {
public static final AEStackTypeFilterSyncCodec INSTANCE = new AEStackTypeFilterSyncCodec();
private static final byte OP_TOGGLE = 0;
private AEStackTypeFilterSyncCodec() {}
@Override
public void write(final ByteBuf buf, final AEStackTypeFilter value) {
value.writeToPacket(buf);
}
@Override
public AEStackTypeFilter read(final ByteBuf buf) {
return new AEStackTypeFilter(buf);
}
@Override
public AEStackTypeFilter copy(final AEStackTypeFilter value) {
return new AEStackTypeFilter(value);
}
@Override
public boolean valuesEqual(final AEStackTypeFilter a, final AEStackTypeFilter b) {
return a.equals(b);
}
@Override
public @Nullable List<TypeFilterDelta> diff(final AEStackTypeFilter previous, final AEStackTypeFilter current) {
List<TypeFilterDelta> deltas = null;
for (final IAEStackType<?> type : AEStackTypeRegistry.getAllTypes()) {
if (previous.isEnabled(type) != current.isEnabled(type)) {
if (deltas == null) {
deltas = new ArrayList<>();
}
deltas.add(TypeFilterDelta.toggle(type));
}
}
return deltas;
}
@Override
public void writeDelta(final ByteBuf buf, final TypeFilterDelta delta) throws IOException {
if (delta instanceof final TypeFilterDelta.Toggle toggle) {
buf.writeByte(OP_TOGGLE);
buf.writeByte(toggle.getNetworkId());
return;
}
throw new IOException("Unknown AEStackTypeFilter delta type: " + delta.getClass().getName());
}
@Override
public AEStackTypeFilter applyDelta(@Nullable final AEStackTypeFilter base, final ByteBuf buf) throws IOException {
if (base == null) {
throw new IOException("Cannot apply AEStackTypeFilter delta without a base value.");
}
final byte op = buf.readByte();
switch (op) {
case OP_TOGGLE -> {
final IAEStackType<?> type = this.requireType(buf.readByte());
base.toggle(type);
return base;
}
default -> throw new IOException("Unknown AEStackTypeFilter delta opcode: " + op);
}
}
@Override
public String getTypeKey() {
return AEStackTypeFilter.class.getName();
}
private IAEStackType<?> requireType(final byte networkId) throws IOException {
final IAEStackType<?> type = AEStackTypeRegistry.getTypeFromNetworkId(networkId);
if (type == null) {
throw new IOException("Unknown AE stack type network id: " + networkId);
}
return type;
}
}
@@ -0,0 +1,24 @@
package appeng.container.sync.deltas;
import org.jetbrains.annotations.Nullable;
import appeng.api.storage.data.IAEStack;
public final class AEStackInventoryDelta {
private final int slotIndex;
private final @Nullable IAEStack<?> stack;
public AEStackInventoryDelta(final int slotIndex, @Nullable final IAEStack<?> stack) {
this.slotIndex = slotIndex;
this.stack = stack == null ? null : stack.copy();
}
public int getSlotIndex() {
return this.slotIndex;
}
public @Nullable IAEStack<?> getStack() {
return this.stack == null ? null : this.stack.copy();
}
}
@@ -0,0 +1,24 @@
package appeng.container.sync.deltas;
import appeng.api.storage.data.AEStackTypeRegistry;
import appeng.api.storage.data.IAEStackType;
public interface TypeFilterDelta {
static TypeFilterDelta toggle(final IAEStackType<?> type) {
return new Toggle(AEStackTypeRegistry.getNetworkId(type));
}
final class Toggle implements TypeFilterDelta {
private final byte networkId;
public Toggle(final byte networkId) {
this.networkId = networkId;
}
public byte getNetworkId() {
return this.networkId;
}
}
}
@@ -0,0 +1,249 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.api.storage.data.IAEStack;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import appeng.container.sync.deltas.AEStackInventoryDelta;
import appeng.tile.inventory.IAEStackInventory;
import appeng.util.Platform;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class AEStackInventorySyncHandler extends AbstractSyncHandler {
@FunctionalInterface
public interface InventoryChangeListener {
void onChange(@NotNull IAEStackInventory inventory);
}
private final @NotNull IAEStackInventory inventory;
private final List<AEStackInventoryDelta> pendingDeltas = new ArrayList<>();
private IAEStack<?>[] lastSentSnapshot;
private @Nullable InventoryChangeListener clientChangeListener;
private @Nullable InventoryChangeListener serverChangeListener;
public AEStackInventorySyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction, final @NotNull IAEStackInventory inventory) {
super(manager, key, fullKey, direction);
this.inventory = Objects.requireNonNull(inventory, "inventory");
this.lastSentSnapshot = this.copyInventory();
this.dirty = false;
}
public @NotNull IAEStackInventory get() {
return this.inventory;
}
public void markDirty() {
this.clearQueuedDeltas();
this.markDirtyInternal();
}
public void applyAndQueueDelta(final AEStackInventoryDelta delta) {
this.applyDelta(delta);
this.pendingDeltas.add(delta);
this.markDirtyInternal();
}
public @NotNull AEStackInventorySyncHandler onClientChange(final InventoryChangeListener listener) {
this.clientChangeListener = listener;
return this;
}
public @NotNull AEStackInventorySyncHandler onServerChange(final InventoryChangeListener listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf)
throws IOException {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync && this.valuesEqual(this.lastSentSnapshot)) {
this.clearQueuedDeltas();
this.dirty = false;
return null;
}
SyncMode mode = SyncMode.FULL;
ByteBuf payload = buf;
if (!initialSync && !forceFullResync) {
final ByteBuf fullPayload = Unpooled.buffer();
this.writeFull(fullPayload);
final ByteBuf deltaPayload = this.writeDeltaPayload();
if (deltaPayload != null && deltaPayload.readableBytes() < fullPayload.readableBytes()) {
mode = SyncMode.DELTA;
payload = deltaPayload;
} else {
payload = fullPayload;
}
} else {
this.writeFull(payload);
}
if (payload != buf) {
buf.writeBytes(payload, payload.readerIndex(), payload.readableBytes());
}
this.lastSentSnapshot = this.copyInventory();
this.clearQueuedDeltas();
this.dirty = false;
this.clearFullResyncRequest();
return mode;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode == SyncMode.FULL) {
this.readFull(buf);
} else if (mode == SyncMode.DELTA) {
try {
this.applyDeltaPayload(buf);
} catch (final IOException e) {
if (this.getDirection() == SyncDirection.CLIENT_TO_SERVER || remoteEndpoint == SyncEndpoint.SERVER) {
this.requestRemoteFullResync();
} else {
this.requestFullResync();
}
throw e;
}
} else {
throw new IOException(
"AEStackInventorySyncHandler for " + this.fullKey + " does not support " + mode + ".");
}
this.lastSentSnapshot = this.copyInventory();
this.clearQueuedDeltas();
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(this.inventory);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(this.inventory);
}
}
@Override
protected String getTypeKey() {
return IAEStackInventory.class.getName();
}
private void writeFull(final ByteBuf buf) throws IOException {
for (int slot = 0; slot < this.inventory.getSizeInventory(); slot++) {
IAEStack.writeToPacketGeneric(buf, this.inventory.getAEStackInSlot(slot));
}
}
private void readFull(final ByteBuf buf) throws IOException {
for (int slot = 0; slot < this.inventory.getSizeInventory(); slot++) {
this.inventory.putAEStackInSlot(slot, IAEStack.fromPacketGeneric(buf));
}
}
private @Nullable ByteBuf writeDeltaPayload() throws IOException {
final List<AEStackInventoryDelta> deltas = this.pendingDeltas.isEmpty() ? this.diff() : this.pendingDeltas;
if (deltas == null || deltas.isEmpty()) {
return null;
}
final ByteBuf deltaPayload = Unpooled.buffer();
deltaPayload.writeInt(deltas.size());
for (final AEStackInventoryDelta delta : deltas) {
deltaPayload.writeInt(delta.getSlotIndex());
IAEStack.writeToPacketGeneric(deltaPayload, delta.getStack());
}
return deltaPayload;
}
private @Nullable List<AEStackInventoryDelta> diff() {
List<AEStackInventoryDelta> deltas = null;
for (int slot = 0; slot < this.inventory.getSizeInventory(); slot++) {
final IAEStack<?> previousStack = this.lastSentSnapshot[slot];
final IAEStack<?> currentStack = this.inventory.getAEStackInSlot(slot);
if (!Platform.isStacksIdentical(previousStack, currentStack)) {
if (deltas == null) {
deltas = new ArrayList<>();
}
deltas.add(new AEStackInventoryDelta(slot, currentStack));
}
}
return deltas;
}
private void clearQueuedDeltas() {
this.pendingDeltas.clear();
}
private void applyDeltaPayload(final ByteBuf buf) throws IOException {
final int deltaCount = buf.readInt();
if (deltaCount <= 0) {
throw new IOException("Invalid delta count " + deltaCount + " for " + this.fullKey + ".");
}
for (int i = 0; i < deltaCount; i++) {
final int slotIndex = buf.readInt();
if (slotIndex < 0 || slotIndex >= this.inventory.getSizeInventory()) {
throw new IOException("Invalid AE stack inventory slot index: " + slotIndex);
}
this.inventory.putAEStackInSlot(slotIndex, IAEStack.fromPacketGeneric(buf));
}
}
private void applyDelta(final AEStackInventoryDelta delta) {
final int slotIndex = delta.getSlotIndex();
if (slotIndex < 0 || slotIndex >= this.inventory.getSizeInventory()) {
throw new IllegalArgumentException("Invalid AE stack inventory slot index: " + slotIndex);
}
this.inventory.putAEStackInSlot(slotIndex, delta.getStack());
}
private boolean valuesEqual(final IAEStack<?>[] snapshot) {
for (int slot = 0; slot < this.inventory.getSizeInventory(); slot++) {
if (!Platform.isStacksIdentical(this.inventory.getAEStackInSlot(slot), snapshot[slot])) {
return false;
}
}
return true;
}
private IAEStack<?>[] copyInventory() {
final IAEStack<?>[] copy = new IAEStack[this.inventory.getSizeInventory()];
for (int slot = 0; slot < copy.length; slot++) {
final IAEStack<?> stack = this.inventory.getAEStackInSlot(slot);
copy[slot] = stack == null ? null : stack.copy();
}
return copy;
}
}
@@ -0,0 +1,121 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import io.netty.buffer.ByteBuf;
public class BooleanSyncHandler extends AbstractSyncHandler {
@FunctionalInterface
public interface BooleanChangeListener {
void onChange(boolean oldValue, boolean newValue);
}
private boolean value;
private boolean lastSentValue;
private boolean hasLastSentValue;
private @Nullable BooleanChangeListener clientChangeListener;
private @Nullable BooleanChangeListener serverChangeListener;
public BooleanSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction) {
super(manager, key, fullKey, direction);
}
public boolean get() {
return this.value;
}
/**
* Seeds the local value without marking it dirty or scheduling it to be sent.
*/
public void setLocalValue(final boolean value) {
this.value = value;
this.lastSentValue = value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
}
public void set(final boolean value) {
this.value = value;
this.markDirty();
}
public void markDirty() {
this.markDirtyInternal();
}
public BooleanSyncHandler onClientChange(final BooleanChangeListener listener) {
this.clientChangeListener = listener;
return this;
}
public BooleanSyncHandler onServerChange(final BooleanChangeListener listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf) {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync && this.hasLastSentValue && this.lastSentValue == this.value) {
this.dirty = false;
return null;
}
buf.writeBoolean(this.value);
this.lastSentValue = this.value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
return SyncMode.FULL;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode != SyncMode.FULL) {
throw new IOException("BooleanSyncHandler only supports full updates.");
}
final boolean oldValue = this.value;
final boolean newValue = buf.readBoolean();
this.value = newValue;
this.lastSentValue = newValue;
this.hasLastSentValue = true;
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(oldValue, newValue);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(oldValue, newValue);
}
}
@Override
protected String getTypeKey() {
return "boolean";
}
}
@@ -0,0 +1,110 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import java.util.Objects;
import org.jetbrains.annotations.Nullable;
import appeng.api.config.Settings;
import appeng.api.util.IConfigManager;
import appeng.container.sync.SyncCodec;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import appeng.util.Platform;
import io.netty.buffer.ByteBuf;
public final class ConfigEnumSyncHandler<E extends Enum<E>> extends ObjectSyncHandler<E> {
private final Settings setting;
private final IConfigManager configManager;
private final Class<E> enumClass;
public ConfigEnumSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction, final Settings setting, final Class<E> enumClass,
final IConfigManager configManager, @Nullable final E initialValue) {
super(manager, key, fullKey, direction, new ConfigEnumCodec<>(enumClass), initialValue);
this.setting = Objects.requireNonNull(setting, "setting");
this.configManager = Objects.requireNonNull(configManager, "configManager");
this.enumClass = Objects.requireNonNull(enumClass, "enumClass");
}
public void syncFromConfig() {
this.set(this.readCurrentValue());
}
public void rotate(final boolean backwards) {
final E next = this.rotateValue(this.getCurrentOrConfigValue(), backwards);
this.set(next);
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
super.readUpdate(remoteEndpoint, mode, buf);
if (remoteEndpoint == SyncEndpoint.CLIENT) {
final E value = this.get();
if (value != null) {
this.configManager.putSetting(this.setting, value);
}
}
}
private E readCurrentValue() {
return this.enumClass.cast(this.configManager.getSetting(this.setting));
}
private @Nullable E getCurrentOrConfigValue() {
final E current = this.get();
return current != null ? current : this.readCurrentValue();
}
private E rotateValue(final @Nullable E currentValue, final boolean backwards) {
if (currentValue == null) {
throw new IllegalStateException("Cannot rotate a null config enum value.");
}
return Platform.rotateEnum(currentValue, backwards, this.setting.getPossibleValues());
}
private static final class ConfigEnumCodec<E extends Enum<E>> implements SyncCodec<E> {
private final E[] enumValues;
private ConfigEnumCodec(final Class<E> enumClass) {
this.enumValues = Objects.requireNonNull(enumClass, "enumClass").getEnumConstants();
}
@Override
public void write(final ByteBuf buf, final E value) {
buf.writeInt(value.ordinal());
}
@Override
public E read(final ByteBuf buf) throws IOException {
final int ordinal = buf.readInt();
if (ordinal < 0 || ordinal >= this.enumValues.length) {
throw new IOException("Invalid enum ordinal " + ordinal + " for config enum sync.");
}
return this.enumValues[ordinal];
}
@Override
public E copy(final E value) {
return value;
}
@Override
public boolean valuesEqual(final E a, final E b) {
return a == b;
}
@Override
public String getTypeKey() {
return this.enumValues.getClass().getComponentType().getName();
}
}
}
@@ -0,0 +1,94 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.DeltaSyncCodec;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncManager;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
/**
* Object sync handler that can batch and transmit one or more delta updates before falling back to a normal full sync.
*/
public class DeltaObjectSyncHandler<T, D> extends ObjectSyncHandler<T> {
private final DeltaSyncCodec<T, D> deltaCodec;
private final List<D> pendingDeltas = new ArrayList<>();
public DeltaObjectSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction, final DeltaSyncCodec<T, D> codec, final T initialValue) {
super(manager, key, fullKey, direction, codec, initialValue);
this.deltaCodec = codec;
}
/**
* Applies a known local delta immediately and queues it for transmission on the next flush. If delta sync later
* fails, normal resync recovery still falls back to a full update/request as needed.
*/
public void applyAndQueueDelta(final D delta) {
if (this.getCurrentValue() == null) {
throw new IllegalStateException("Cannot apply a delta to a null sync value: " + this.fullKey);
}
try {
this.setCurrentValue(this.deltaCodec.applyDeltaLocally(this.copyValue(this.getCurrentValue()), delta));
} catch (final IOException e) {
throw new IllegalStateException("Failed to apply local delta for " + this.fullKey + ".", e);
}
this.pendingDeltas.add(delta);
this.markDirty();
}
@Override
public @NotNull DeltaObjectSyncHandler<T, D> onClientChange(final ObjectChangeListener<T> listener) {
super.onClientChange(listener);
return this;
}
@Override
public @NotNull DeltaObjectSyncHandler<T, D> onServerChange(final ObjectChangeListener<T> listener) {
super.onServerChange(listener);
return this;
}
@Override
protected void clearQueuedDeltas() {
this.pendingDeltas.clear();
}
@Override
protected @Nullable ByteBuf getQueuedDeltaPayload() throws IOException {
if (this.pendingDeltas.isEmpty()) {
return null;
}
final ByteBuf deltaPayload = Unpooled.buffer();
deltaPayload.writeInt(this.pendingDeltas.size());
for (final D delta : this.pendingDeltas) {
this.deltaCodec.writeDelta(deltaPayload, delta);
}
return deltaPayload;
}
@Override
protected T applyDeltaPayload(final ByteBuf buf) throws IOException {
T result = this.copyValue(this.getCurrentValue());
final int deltaCount = buf.readInt();
if (deltaCount <= 0) {
throw new IOException("Invalid delta count " + deltaCount + " for " + this.fullKey + ".");
}
for (int i = 0; i < deltaCount; i++) {
result = this.deltaCodec.applyDelta(result, buf);
}
return result;
}
}
@@ -0,0 +1,123 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import io.netty.buffer.ByteBuf;
public class DoubleSyncHandler extends AbstractSyncHandler {
@FunctionalInterface
public interface DoubleChangeListener {
void onChange(double oldValue, double newValue);
}
private double value;
private double lastSentValue;
private boolean hasLastSentValue;
private @Nullable DoubleChangeListener clientChangeListener;
private @Nullable DoubleChangeListener serverChangeListener;
public DoubleSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction) {
super(manager, key, fullKey, direction);
}
public double get() {
return this.value;
}
/**
* Seeds the local value without marking it dirty or scheduling it to be sent.
*/
public void setLocalValue(final double value) {
this.value = value;
this.lastSentValue = value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
}
public void set(final double value) {
this.value = value;
this.markDirty();
}
public void markDirty() {
this.markDirtyInternal();
}
public DoubleSyncHandler onClientChange(final DoubleChangeListener listener) {
this.clientChangeListener = listener;
return this;
}
public DoubleSyncHandler onServerChange(final DoubleChangeListener listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf) {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync
&& this.hasLastSentValue
&& Double.doubleToLongBits(this.lastSentValue) == Double.doubleToLongBits(this.value)) {
this.dirty = false;
return null;
}
buf.writeDouble(this.value);
this.lastSentValue = this.value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
return SyncMode.FULL;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode != SyncMode.FULL) {
throw new IOException("DoubleSyncHandler only supports full updates.");
}
final double oldValue = this.value;
final double newValue = buf.readDouble();
this.value = newValue;
this.lastSentValue = newValue;
this.hasLastSentValue = true;
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(oldValue, newValue);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(oldValue, newValue);
}
}
@Override
protected String getTypeKey() {
return "double";
}
}
@@ -0,0 +1,121 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import io.netty.buffer.ByteBuf;
public class IntSyncHandler extends AbstractSyncHandler {
@FunctionalInterface
public interface IntChangeListener {
void onChange(int oldValue, int newValue);
}
private int value;
private int lastSentValue;
private boolean hasLastSentValue;
private @Nullable IntChangeListener clientChangeListener;
private @Nullable IntChangeListener serverChangeListener;
public IntSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction) {
super(manager, key, fullKey, direction);
}
public int get() {
return this.value;
}
/**
* Seeds the local value without marking it dirty or scheduling it to be sent.
*/
public void setLocalValue(final int value) {
this.value = value;
this.lastSentValue = value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
}
public void set(final int value) {
this.value = value;
this.markDirty();
}
public void markDirty() {
this.markDirtyInternal();
}
public IntSyncHandler onClientChange(final IntChangeListener listener) {
this.clientChangeListener = listener;
return this;
}
public IntSyncHandler onServerChange(final IntChangeListener listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf) {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync && this.hasLastSentValue && this.lastSentValue == this.value) {
this.dirty = false;
return null;
}
buf.writeInt(this.value);
this.lastSentValue = this.value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
return SyncMode.FULL;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode != SyncMode.FULL) {
throw new IOException("IntSyncHandler only supports full updates.");
}
final int oldValue = this.value;
final int newValue = buf.readInt();
this.value = newValue;
this.lastSentValue = newValue;
this.hasLastSentValue = true;
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(oldValue, newValue);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(oldValue, newValue);
}
}
@Override
protected String getTypeKey() {
return "int";
}
}
@@ -0,0 +1,124 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import io.netty.buffer.ByteBuf;
public class LongSyncHandler extends AbstractSyncHandler {
@FunctionalInterface
public interface LongChangeListener {
void onChange(long oldValue, long newValue);
}
private long value;
private long lastSentValue;
private boolean hasLastSentValue;
private @Nullable LongChangeListener clientChangeListener;
private @Nullable LongChangeListener serverChangeListener;
public LongSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction) {
super(manager, key, fullKey, direction);
}
public long get() {
return this.value;
}
/**
* Seeds the local value without marking it dirty or scheduling it to be sent.
*/
public void setLocalValue(final long value) {
this.value = value;
this.lastSentValue = value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
}
public void set(final long value) {
this.value = value;
this.markDirty();
}
public void markDirty() {
this.markDirtyInternal();
}
@NotNull
public LongSyncHandler onClientChange(final LongChangeListener listener) {
this.clientChangeListener = listener;
return this;
}
@NotNull
public LongSyncHandler onServerChange(final LongChangeListener listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf) {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync && this.hasLastSentValue && this.lastSentValue == this.value) {
this.dirty = false;
return null;
}
buf.writeLong(this.value);
this.lastSentValue = this.value;
this.hasLastSentValue = true;
this.dirty = false;
this.clearFullResyncRequest();
return SyncMode.FULL;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
if (mode != SyncMode.FULL) {
throw new IOException("LongSyncHandler only supports full updates.");
}
final long oldValue = this.value;
final long newValue = buf.readLong();
this.value = newValue;
this.lastSentValue = newValue;
this.hasLastSentValue = true;
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(oldValue, newValue);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(oldValue, newValue);
}
}
@Override
protected String getTypeKey() {
return "long";
}
}
@@ -0,0 +1,269 @@
package appeng.container.sync.handlers;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import appeng.container.sync.AbstractSyncHandler;
import appeng.container.sync.DeltaSyncCodec;
import appeng.container.sync.SyncCodec;
import appeng.container.sync.SyncDirection;
import appeng.container.sync.SyncEndpoint;
import appeng.container.sync.SyncManager;
import appeng.container.sync.SyncMode;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class ObjectSyncHandler<T> extends AbstractSyncHandler {
@FunctionalInterface
public interface ObjectChangeListener<T> {
void onChange(T oldValue, T newValue);
}
private final SyncCodec<T> codec;
private T value;
private T lastSentSnapshot;
private boolean hasLastSentSnapshot;
private @Nullable ObjectChangeListener<T> clientChangeListener;
private @Nullable ObjectChangeListener<T> serverChangeListener;
public ObjectSyncHandler(final SyncManager manager, final String key, final String fullKey,
final SyncDirection direction, final SyncCodec<T> codec, final T initialValue) {
super(manager, key, fullKey, direction);
this.codec = Objects.requireNonNull(codec, "codec");
this.value = initialValue;
this.lastSentSnapshot = this.copyValue(initialValue);
this.hasLastSentSnapshot = true;
this.dirty = false;
}
public T get() {
return this.value;
}
public void set(final T value) {
this.value = value;
this.clearQueuedDeltas();
this.markDirty();
}
public void markDirty() {
this.markDirtyInternal();
}
/**
* Applies an in-place change to the current value and marks the handler dirty.
*/
public void mutate(final Consumer<T> mutator) {
if (this.value == null) {
throw new IllegalStateException("Cannot mutate a null sync value: " + this.fullKey);
}
mutator.accept(this.value);
this.clearQueuedDeltas();
this.markDirty();
}
public @NotNull ObjectSyncHandler<T> onClientChange(final ObjectChangeListener<T> listener) {
this.clientChangeListener = listener;
return this;
}
public @NotNull ObjectSyncHandler<T> onServerChange(final ObjectChangeListener<T> listener) {
this.serverChangeListener = listener;
return this;
}
@Override
protected SyncMode writeUpdate(final SyncEndpoint localEndpoint, final boolean initialSync, final ByteBuf buf)
throws IOException {
final boolean forceFullResync = this.shouldForceFullResync();
if (!this.canSendFrom(localEndpoint)) {
return null;
}
if (initialSync) {
if (!this.sendsInitialStateFrom(localEndpoint)) {
return null;
}
} else if (!this.dirty && !forceFullResync) {
return null;
}
if (!initialSync && !forceFullResync
&& this.hasLastSentSnapshot
&& this.valuesEqual(this.value, this.lastSentSnapshot)) {
this.clearQueuedDeltas();
this.dirty = false;
return null;
}
SyncMode mode = SyncMode.FULL;
ByteBuf payload = buf;
if (!initialSync && !forceFullResync
&& this.codec instanceof DeltaSyncCodec<?, ?>
&& this.hasLastSentSnapshot) {
final ByteBuf fullPayload = Unpooled.buffer();
this.writeFull(fullPayload, this.value);
final ByteBuf deltaPayload = this.writeDeltaPayload();
if (deltaPayload != null && deltaPayload.readableBytes() < fullPayload.readableBytes()) {
mode = SyncMode.DELTA;
payload = deltaPayload;
} else {
payload = fullPayload;
}
} else {
this.writeFull(payload, this.value);
}
if (payload != buf) {
buf.writeBytes(payload, payload.readerIndex(), payload.readableBytes());
}
this.lastSentSnapshot = this.copyValue(this.value);
this.hasLastSentSnapshot = true;
this.clearQueuedDeltas();
this.dirty = false;
this.clearFullResyncRequest();
return mode;
}
@Override
protected void readUpdate(final SyncEndpoint remoteEndpoint, final SyncMode mode, final ByteBuf buf)
throws IOException {
final T oldValue = this.value;
final T newValue;
this.clearQueuedDeltas();
if (mode == SyncMode.FULL) {
newValue = this.readFull(buf);
} else if (this.codec instanceof DeltaSyncCodec<?, ?>) {
try {
newValue = this.applyDeltaPayload(buf);
} catch (final IOException e) {
// CLIENT_TO_SERVER cannot push a full authoritative state from the server,
// so ask the client to resend its current full value instead.
if (this.getDirection() == SyncDirection.CLIENT_TO_SERVER || remoteEndpoint == SyncEndpoint.SERVER) {
this.requestRemoteFullResync();
} else {
// SERVER_TO_CLIENT and BIDIRECTIONAL can recover by sending the server state.
this.requestFullResync();
}
throw e;
}
} else {
throw new IOException("ObjectSyncHandler for " + this.fullKey + " does not support delta updates.");
}
this.value = newValue;
this.lastSentSnapshot = this.copyValue(newValue);
this.hasLastSentSnapshot = true;
this.dirty = false;
if (remoteEndpoint == SyncEndpoint.SERVER) {
if (this.clientChangeListener != null) {
this.clientChangeListener.onChange(oldValue, newValue);
}
} else if (this.serverChangeListener != null) {
this.serverChangeListener.onChange(oldValue, newValue);
}
}
@Override
protected String getTypeKey() {
return this.codec.getTypeKey();
}
protected final @Nullable T getCurrentValue() {
return this.value;
}
protected final void setCurrentValue(@Nullable final T value) {
this.value = value;
}
private void writeFull(final ByteBuf buf, @Nullable final T value) throws IOException {
buf.writeBoolean(value != null);
if (value != null) {
this.codec.write(buf, value);
}
}
@Nullable
private T readFull(final ByteBuf buf) throws IOException {
if (!buf.readBoolean()) {
return null;
}
return this.codec.read(buf);
}
protected final @Nullable T copyValue(@Nullable final T value) {
return value == null ? null : this.codec.copy(value);
}
private boolean valuesEqual(@Nullable final T a, @Nullable final T b) {
if (a == b) {
return true;
}
if (a == null || b == null) {
return false;
}
return this.codec.valuesEqual(a, b);
}
@Nullable
protected ByteBuf writeDeltaPayload() throws IOException {
final ByteBuf queuedDeltaPayload = this.getQueuedDeltaPayload();
if (queuedDeltaPayload != null) {
return queuedDeltaPayload;
}
final DeltaSyncCodec<T, Object> deltaCodec = (DeltaSyncCodec<T, Object>) this.codec;
final T previous = this.lastSentSnapshot;
final T current = this.value;
if (previous == null || current == null) {
return null;
}
final List<Object> deltas = deltaCodec.diff(previous, current);
if (deltas == null || deltas.isEmpty()) {
return null;
}
final ByteBuf deltaPayload = Unpooled.buffer();
deltaPayload.writeInt(deltas.size());
for (final Object delta : deltas) {
deltaCodec.writeDelta(deltaPayload, delta);
}
return deltaPayload;
}
protected T applyDeltaPayload(final ByteBuf buf) throws IOException {
final DeltaSyncCodec<T, Object> deltaCodec = (DeltaSyncCodec<T, Object>) this.codec;
T result = this.copyValue(this.value);
final int deltaCount = buf.readInt();
if (deltaCount <= 0) {
throw new IOException("Invalid delta count " + deltaCount + " for " + this.fullKey + ".");
}
for (int i = 0; i < deltaCount; i++) {
result = deltaCodec.applyDelta(result, buf);
}
return result;
}
protected void clearQueuedDeltas() {}
protected @Nullable ByteBuf getQueuedDeltaPayload() throws IOException {
return null;
}
}
@@ -23,6 +23,7 @@ import appeng.core.sync.packets.PacketCompassRequest;
import appeng.core.sync.packets.PacketCompassResponse;
import appeng.core.sync.packets.PacketCompressedNBT;
import appeng.core.sync.packets.PacketConfigButton;
import appeng.core.sync.packets.PacketContainerSync;
import appeng.core.sync.packets.PacketCraftRequest;
import appeng.core.sync.packets.PacketCraftingCPUTableUpdate;
import appeng.core.sync.packets.PacketCraftingCompleteNotification;
@@ -94,6 +95,7 @@ public class AppEngPacketHandlerBase {
PACKET_TRANSITION_EFFECT(PacketTransitionEffect.class),
PACKET_GUI_DATA_SYNC(PacketGuiDataSync.class),
PACKET_CONTAINER_SYNC(PacketContainerSync.class),
PACKET_CLICK(PacketClick.class),
@@ -0,0 +1,47 @@
package appeng.core.sync.packets;
import net.minecraft.entity.player.EntityPlayer;
import appeng.container.AEBaseContainer;
import appeng.container.sync.SyncEndpoint;
import appeng.core.sync.AppEngPacket;
import appeng.core.sync.network.INetworkInfo;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class PacketContainerSync extends AppEngPacket {
private final int windowId;
private final byte[] data;
public PacketContainerSync(final ByteBuf buf) {
this.windowId = buf.readInt();
this.data = new byte[buf.readableBytes()];
buf.readBytes(this.data);
}
public PacketContainerSync(final int windowId, final ByteBuf data) {
this.windowId = windowId;
this.data = null;
final ByteBuf buf = Unpooled.buffer();
buf.writeInt(this.getPacketID());
buf.writeInt(windowId);
buf.writeBytes(data, data.readerIndex(), data.readableBytes());
this.configureWrite(buf);
}
@Override
public void clientPacketData(final INetworkInfo network, final AppEngPacket packet, final EntityPlayer player) {
if (player.openContainer instanceof AEBaseContainer container && container.windowId == this.windowId) {
container.receiveSyncData(SyncEndpoint.SERVER, Unpooled.wrappedBuffer(this.data));
}
}
@Override
public void serverPacketData(final INetworkInfo network, final AppEngPacket packet, final EntityPlayer player) {
if (player.openContainer instanceof AEBaseContainer container && container.windowId == this.windowId) {
container.receiveSyncData(SyncEndpoint.CLIENT, Unpooled.wrappedBuffer(this.data));
}
}
}
@@ -31,7 +31,6 @@ import appeng.container.implementations.ContainerCellWorkbench;
import appeng.container.implementations.ContainerCraftConfirm;
import appeng.container.implementations.ContainerCraftingCPU;
import appeng.container.implementations.ContainerInterface;
import appeng.container.implementations.ContainerLevelEmitter;
import appeng.container.implementations.ContainerMEMonitorable;
import appeng.container.implementations.ContainerNetworkStatus;
import appeng.container.implementations.ContainerNetworkTool;
@@ -137,10 +136,6 @@ public class PacketValueConfig extends AppEngPacket {
pc.setPriority(Integer.parseInt(this.Value), player);
} else if (this.Name.equals("OreFilter") && c instanceof ContainerOreFilter fc) {
fc.setFilter(this.Value);
} else if (this.Name.equals("LevelEmitter.Value") && c instanceof ContainerLevelEmitter lvc) {
lvc.setLevel(Long.parseLong(this.Value), player);
} else if (this.Name.equals("LevelEmitter.TypeFilter") && c instanceof ContainerLevelEmitter lvc) {
lvc.toggleTypeFilter(this.Value, player);
} else if (this.Name.startsWith("PatternTerminal.") && c instanceof final ContainerPatternTerm cpt) {
switch (this.Name) {
case "PatternTerminal.CraftMode" -> cpt.getPatternTerminal().setCraftingRecipe(this.Value.equals("1"));
@@ -103,6 +103,14 @@ public class AEStackTypeFilter implements IGuiPacketWritable {
return this.filters.getBoolean(type);
}
public void copyFrom(@NotNull final AEStackTypeFilter other) {
this.filters.clear();
this.filters.putAll(createDefaultMap());
for (Reference2BooleanMap.Entry<IAEStackType<?>> entry : other.filters.reference2BooleanEntrySet()) {
this.filters.put(entry.getKey(), entry.getBooleanValue());
}
}
@NotNull
public Reference2BooleanMap<IAEStackType<?>> getImmutableFilters() {
return Reference2BooleanMaps.unmodifiable(new Reference2BooleanOpenHashMap<>(this.filters));