diff --git a/src/main/java/com/cleanroommc/modularui/CommonProxy.java b/src/main/java/com/cleanroommc/modularui/CommonProxy.java index 120270eba..937faf178 100644 --- a/src/main/java/com/cleanroommc/modularui/CommonProxy.java +++ b/src/main/java/com/cleanroommc/modularui/CommonProxy.java @@ -21,6 +21,7 @@ import net.minecraftforge.fml.common.event.FMLPreInitializationEvent; import net.minecraftforge.fml.common.event.FMLServerStartingEvent; import net.minecraftforge.fml.common.eventhandler.SubscribeEvent; +import net.minecraftforge.fml.common.gameevent.TickEvent; import net.minecraftforge.fml.common.registry.EntityEntry; import net.minecraftforge.fml.common.registry.EntityEntryBuilder; import net.minecraftforge.fml.relauncher.Side; @@ -81,4 +82,11 @@ public void onConfigChange(ConfigChangedEvent.OnConfigChangedEvent event) { ConfigManager.sync(ModularUI.ID, Config.Type.INSTANCE); } } + + @SubscribeEvent + public static void onTick(TickEvent.PlayerTickEvent event) { + if (event.player.openContainer instanceof ModularContainer container) { + container.onUpdate(); + } + } } diff --git a/src/main/java/com/cleanroommc/modularui/api/IPacketWriter.java b/src/main/java/com/cleanroommc/modularui/api/IPacketWriter.java index a6f3693b2..adc38efe5 100644 --- a/src/main/java/com/cleanroommc/modularui/api/IPacketWriter.java +++ b/src/main/java/com/cleanroommc/modularui/api/IPacketWriter.java @@ -1,5 +1,7 @@ package com.cleanroommc.modularui.api; +import io.netty.buffer.Unpooled; + import net.minecraft.network.PacketBuffer; import java.io.IOException; @@ -7,6 +9,7 @@ /** * A function that can write any data to an {@link PacketBuffer}. */ +@FunctionalInterface public interface IPacketWriter { /** @@ -16,4 +19,14 @@ public interface IPacketWriter { * @throws IOException if data can not be written for some reason */ void write(PacketBuffer buffer) throws IOException; + + default PacketBuffer toPacket() { + PacketBuffer buffer = new PacketBuffer(Unpooled.buffer()); + try { + write(buffer); + } catch (IOException e) { + throw new RuntimeException(e); + } + return buffer; + } } diff --git a/src/main/java/com/cleanroommc/modularui/api/ISyncedAction.java b/src/main/java/com/cleanroommc/modularui/api/ISyncedAction.java new file mode 100644 index 000000000..f16239e85 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/api/ISyncedAction.java @@ -0,0 +1,13 @@ +package com.cleanroommc.modularui.api; + +import net.minecraft.network.PacketBuffer; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +@FunctionalInterface +public interface ISyncedAction { + + @ApiStatus.OverrideOnly + void invoke(@NotNull PacketBuffer packet); +} diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java b/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java index fedcc8b1c..c218485f5 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/ISynced.java @@ -24,8 +24,9 @@ default W getThis() { * Called when this widget gets initialised or when this widget is added to the gui * * @param syncManager sync manager + * @param late */ - void initialiseSyncHandler(ModularSyncManager syncManager); + void initialiseSyncHandler(ModularSyncManager syncManager, boolean late); /** * Checks if the received sync handler is valid for this widget. diff --git a/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java b/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java index 7cc97c20c..a836178fe 100644 --- a/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java +++ b/src/main/java/com/cleanroommc/modularui/api/widget/IWidget.java @@ -27,8 +27,9 @@ public interface IWidget extends IGuiElement { * This element now becomes valid * * @param parent the parent this element belongs to + * @param late true if this is called some time after the widget tree of the parent has been initialised */ - void initialise(@NotNull IWidget parent); + void initialise(@NotNull IWidget parent, boolean late); /** * Invalidates this element. diff --git a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java index 18d586d4f..82092c535 100644 --- a/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java +++ b/src/main/java/com/cleanroommc/modularui/factory/GuiManager.java @@ -74,7 +74,7 @@ public static void open(@NotNull UIFactory factory, @NotN // create panel, collect sync handlers and create container UISettings settings = new UISettings(JeiSettings.DUMMY); settings.defaultCanInteractWith(factory, guiData); - PanelSyncManager syncManager = new PanelSyncManager(); + PanelSyncManager syncManager = new PanelSyncManager(false); ModularPanel panel = factory.createPanel(guiData, syncManager, settings); WidgetTree.collectSyncValues(syncManager, panel); ModularContainer container = settings.hasContainer() ? settings.createContainer() : factory.createContainer(); @@ -100,7 +100,7 @@ public static void openFromClient(int windowId, @NotNull UIF T guiData = factory.readGuiData(player, data); UISettings settings = new UISettings(); settings.defaultCanInteractWith(factory, guiData); - PanelSyncManager syncManager = new PanelSyncManager(); + PanelSyncManager syncManager = new PanelSyncManager(true); ModularPanel panel = factory.createPanel(guiData, syncManager, settings); WidgetTree.collectSyncValues(syncManager, panel); ModularScreen screen = factory.createScreen(guiData, panel); diff --git a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java index 63ff5ad45..92fa59538 100644 --- a/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/network/packets/PacketSyncHandler.java @@ -1,10 +1,13 @@ package com.cleanroommc.modularui.network.packets; +import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.network.IPacket; import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.screen.ModularScreen; +import com.cleanroommc.modularui.value.sync.ModularSyncManager; + import net.minecraft.client.network.NetHandlerPlayClient; import net.minecraft.inventory.Container; import net.minecraft.network.NetHandlerPlayServer; @@ -18,12 +21,12 @@ public class PacketSyncHandler implements IPacket { private String panel; private String key; + private boolean action; private PacketBuffer packet; - public PacketSyncHandler() { - } + public PacketSyncHandler() {} - public PacketSyncHandler(String panel, String key, PacketBuffer packet) { + public PacketSyncHandler(String panel, String key, boolean action, PacketBuffer packet) { this.panel = panel; this.key = key; this.packet = packet; @@ -33,6 +36,7 @@ public PacketSyncHandler(String panel, String key, PacketBuffer packet) { public void write(PacketBuffer buf) { NetworkUtils.writeStringSafe(buf, this.panel); NetworkUtils.writeStringSafe(buf, this.key, 64, true); + buf.writeBoolean(this.action); NetworkUtils.writeByteBuf(buf, this.packet); } @@ -40,6 +44,7 @@ public void write(PacketBuffer buf) { public void read(PacketBuffer buf) { this.panel = NetworkUtils.readStringSafe(buf); this.key = NetworkUtils.readStringSafe(buf); + this.action = buf.readBoolean(); this.packet = NetworkUtils.readPacketBuffer(buf); } @@ -47,11 +52,7 @@ public void read(PacketBuffer buf) { public @Nullable IPacket executeClient(NetHandlerPlayClient handler) { ModularScreen screen = ModularScreen.getCurrent(); if (screen != null) { - try { - screen.getSyncManager().receiveWidgetUpdate(this.panel, this.key, this.packet.readVarInt(), this.packet); - } catch (IOException e) { - throw new RuntimeException(e); - } + execute(screen.getSyncManager()); } return null; } @@ -60,12 +61,19 @@ public void read(PacketBuffer buf) { public @Nullable IPacket executeServer(NetHandlerPlayServer handler) { Container container = handler.player.openContainer; if (container instanceof ModularContainer modularContainer) { - try { - modularContainer.getSyncManager().receiveWidgetUpdate(this.panel, this.key, this.packet.readVarInt(), this.packet); - } catch (IOException e) { - throw new RuntimeException(e); - } + execute(modularContainer.getSyncManager()); } return null; } + + private void execute(ModularSyncManager syncManager) { + try { + int id = this.action ? 0 : this.packet.readVarInt(); + syncManager.receiveWidgetUpdate(this.panel, this.key, this.action, id, this.packet); + } catch (IndexOutOfBoundsException e) { + ModularUI.LOGGER.error("Failed to read packet for sync handler {} in panel {}", this.key, this.panel); + } catch (IOException e) { + ModularUI.LOGGER.throwing(e); + } + } } diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java index da9942f93..6cdf94c89 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularContainer.java @@ -112,6 +112,14 @@ public void detectAndSendChanges() { this.init = false; } + @ApiStatus.Internal + public void onUpdate() { + // detectAndSendChanges is potentially called multiple times per tick, while this method is called exactly once per tick + if (this.syncManager != null) { + this.syncManager.onUpdate(); + } + } + private void sortShiftClickSlots() { this.shiftClickSlots.sort(Comparator.comparingInt(slot -> Objects.requireNonNull(slot.getSlotGroup()).getShiftClickPriority())); } diff --git a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java index bd289c3f1..6683fbe6e 100644 --- a/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java +++ b/src/main/java/com/cleanroommc/modularui/screen/ModularPanel.java @@ -219,7 +219,7 @@ public boolean canHover() { public void onOpen(ModularScreen screen) { this.screen = screen; getArea().z(1); - initialise(this); + initialise(this, false); // call first tick after everything is initialised WidgetTree.onUpdate(this); if (!isMainPanel() && shouldAnimate()) { diff --git a/src/main/java/com/cleanroommc/modularui/test/TestTile.java b/src/main/java/com/cleanroommc/modularui/test/TestTile.java index 933895652..031dcd04a 100644 --- a/src/main/java/com/cleanroommc/modularui/test/TestTile.java +++ b/src/main/java/com/cleanroommc/modularui/test/TestTile.java @@ -9,6 +9,7 @@ import com.cleanroommc.modularui.drawable.Rectangle; import com.cleanroommc.modularui.drawable.text.AnimatedText; import com.cleanroommc.modularui.factory.PosGuiData; +import com.cleanroommc.modularui.network.NetworkUtils; import com.cleanroommc.modularui.screen.ModularPanel; import com.cleanroommc.modularui.screen.RichTooltip; import com.cleanroommc.modularui.screen.UISettings; @@ -20,10 +21,13 @@ import com.cleanroommc.modularui.value.BoolValue; import com.cleanroommc.modularui.value.IntValue; import com.cleanroommc.modularui.value.StringValue; +import com.cleanroommc.modularui.value.sync.DynamicSyncHandler; import com.cleanroommc.modularui.value.sync.GenericSyncValue; import com.cleanroommc.modularui.value.sync.IntSyncValue; +import com.cleanroommc.modularui.value.sync.ItemSlotSH; import com.cleanroommc.modularui.value.sync.PanelSyncManager; import com.cleanroommc.modularui.value.sync.SyncHandlers; +import com.cleanroommc.modularui.widget.EmptyWidget; import com.cleanroommc.modularui.widget.ParentWidget; import com.cleanroommc.modularui.widgets.*; import com.cleanroommc.modularui.widgets.layout.Column; @@ -32,6 +36,10 @@ import com.cleanroommc.modularui.widgets.slot.*; import com.cleanroommc.modularui.widgets.textfield.TextFieldWidget; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; + import net.minecraft.init.Blocks; import net.minecraft.init.Items; import net.minecraft.item.Item; @@ -48,12 +56,22 @@ import java.util.Collection; import java.util.Random; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; public class TestTile extends TileEntity implements IGuiHolder, ITickable { + private static final Object2IntMap handlerSizeMap = new Object2IntOpenHashMap<>() {{ + put(Items.DIAMOND, 9); + put(Items.EMERALD, 9); + put(Items.GOLD_INGOT, 7); + put(Items.IRON_INGOT, 6); + put(Items.CLAY_BALL, 2); + defaultReturnValue(3); + }}; + private final FluidTank fluidTank = new FluidTank(10000); private final FluidTank fluidTankPhantom = new FluidTank(Integer.MAX_VALUE); private long time = 0; @@ -78,26 +96,44 @@ public int getSlotLimit(int slot) { private final FluidTank mixerFluids1 = new FluidTank(16000); private final FluidTank mixerFluids2 = new FluidTank(16000); private final ItemStackHandler craftingInventory = new ItemStackHandler(10); + private final ItemStackHandler storageInventory0 = new ItemStackHandler(1); + private final Map stackHandlerMap = new Object2ObjectOpenHashMap<>(); private int num = 2; @Override - public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager guiSyncManager, UISettings settings) { + public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager syncManager, UISettings settings) { settings.customContainer(() -> new CraftingModularContainer(3, 3, this.craftingInventory)); - guiSyncManager.registerSlotGroup("item_inv", 3); - guiSyncManager.registerSlotGroup("mixer_items", 2); + syncManager.registerSlotGroup("item_inv", 3); + syncManager.registerSlotGroup("mixer_items", 2); - guiSyncManager.syncValue("mixer_fluids", 0, SyncHandlers.fluidSlot(this.mixerFluids1)); - guiSyncManager.syncValue("mixer_fluids", 1, SyncHandlers.fluidSlot(this.mixerFluids2)); + syncManager.syncValue("mixer_fluids", 0, SyncHandlers.fluidSlot(this.mixerFluids1)); + syncManager.syncValue("mixer_fluids", 1, SyncHandlers.fluidSlot(this.mixerFluids2)); IntSyncValue cycleStateValue = new IntSyncValue(() -> this.cycleState, val -> this.cycleState = val); - guiSyncManager.syncValue("cycle_state", cycleStateValue); - guiSyncManager.syncValue("display_item", GenericSyncValue.forItem(() -> this.displayItem, null)); - guiSyncManager.bindPlayerInventory(guiData.getPlayer()); + syncManager.syncValue("cycle_state", cycleStateValue); + syncManager.syncValue("display_item", GenericSyncValue.forItem(() -> this.displayItem, null)); + syncManager.bindPlayerInventory(guiData.getPlayer()); + + DynamicSyncHandler dynamicSyncHandler = new DynamicSyncHandler() + .widgetProvider((syncManager1, packet) -> { + ItemStack itemStack = NetworkUtils.readItemStack(packet); + if (itemStack.isEmpty()) return new EmptyWidget(); + Item item = itemStack.getItem(); + ItemStackHandler handler = stackHandlerMap.computeIfAbsent(item, k -> new ItemStackHandler(handlerSizeMap.getInt(k))); + String name = item.getRegistryName().toString(); + Flow flow = Flow.row(); + for (int i = 0; i < handler.getSlots(); i++) { + int finalI = i; + flow.child(new ItemSlot() + .syncHandler(syncManager1.getOrCreateSyncHandler(name, i, ItemSlotSH.class, () -> new ItemSlotSH(new ModularSlot(handler, finalI))))); + } + return flow; + }); Rectangle colorPickerBackground = new Rectangle().setColor(Color.RED.main); ModularPanel panel = new ModularPanel("test_tile"); - IPanelHandler panelSyncHandler = guiSyncManager.panel("other_panel", this::openSecondWindow, true); + IPanelHandler panelSyncHandler = syncManager.panel("other_panel", this::openSecondWindow, true); IPanelHandler colorPicker = IPanelHandler.simple(panel, (mainPanel, player) -> new ColorPickerDialog(colorPickerBackground::setColor, colorPickerBackground.getColor(), true) .setDraggable(true) .relative(panel) @@ -117,7 +153,10 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager guiSyncManager, .child(new PageButton(1, tabController) .tab(GuiTextures.TAB_TOP, 0)) .child(new PageButton(2, tabController) - .tab(GuiTextures.TAB_TOP, 0))) + .tab(GuiTextures.TAB_TOP, 0)) + .child(new PageButton(3, tabController) + .tab(GuiTextures.TAB_TOP, 0) + .overlay(new ItemDrawable(Blocks.CHEST).asIcon()))) .child(new Expandable() .debugName("expandable") .top(0) @@ -368,7 +407,22 @@ public ModularPanel buildUI(PosGuiData guiData, PanelSyncManager guiSyncManager, .size(14, 14)) .child(IKey.lang("bogosort.gui.enabled").asWidget() .height(14))))) - )) + .addPage(new ParentWidget<>() + .debugName("page 4 storage") + .sizeRel(1f) + .child(new Column() + .padding(7) + .child(new ItemSlot() + .slot(new ModularSlot(this.storageInventory0, 0) + .changeListener(((newItem, onlyAmountChanged, client, init) -> { + if (client && !onlyAmountChanged) { + dynamicSyncHandler.notifyUpdate(packet -> NetworkUtils.writeItemStack(packet, newItem)); + } + })))) + .child(new DynamicSyncedWidget<>() + .widthRel(1f) + .syncHandler(dynamicSyncHandler))) + ))) .child(SlotGroupWidget.playerInventory(false)) ); /*panel.child(new ButtonWidget<>() diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java new file mode 100644 index 000000000..e7a65ef74 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/value/sync/DynamicSyncHandler.java @@ -0,0 +1,96 @@ +package com.cleanroommc.modularui.value.sync; + +import com.cleanroommc.modularui.api.IPacketWriter; +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.widget.WidgetTree; + +import net.minecraft.network.PacketBuffer; + +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.util.function.Consumer; +import java.util.function.Supplier; + +/** + * This sync handler calls a function on client and server which creates a widget after being notified. The widget is then handed over to a + * linked {@link com.cleanroommc.modularui.widgets.DynamicSyncedWidget}. + */ +public class DynamicSyncHandler extends SyncHandler { + + private IWidgetProvider widgetProvider; + private Consumer onWidgetUpdate; + + @Override + public void readOnClient(int id, PacketBuffer buf) throws IOException { + if (id == 0) { + updateWidgets(buf); + } + } + + @Override + public void readOnServer(int id, PacketBuffer buf) throws IOException { + if (id == 0) { + updateWidgets(buf); + } + } + + private void updateWidgets(PacketBuffer buf) { + getSyncManager().allowTemporarySyncHandlerRegistration(true); + IWidget widget = widgetProvider.createWidget(getSyncManager(), buf); + getSyncManager().allowTemporarySyncHandlerRegistration(false); + // collects any unregistered sync handlers + // since the sync manager is currently locked and we no longer allow bypassing the lock it will crash if it finds any + WidgetTree.collectSyncValues(getSyncManager(), getSyncManager().getPanelName(), widget, true); + if (widget != null && this.widgetProvider != null) { + this.onWidgetUpdate.accept(widget); + } + } + + /** + * Notifies the sync handler to create a new widget. + * + * @param packetWriter data to pass to the function + */ + public void notifyUpdate(IPacketWriter packetWriter) { + updateWidgets(packetWriter.toPacket()); + sync(0, packetWriter); + } + + /** + * Sets a widget creator which is called on client and server. {@link SyncHandler}s can be created here using + * {@link PanelSyncManager#getOrCreateSyncHandler(String, int, Class, Supplier)}. Returning null in the function will not update the widget. + * On client side the result is handed over to a linked {@link com.cleanroommc.modularui.widgets.DynamicSyncedWidget}. + * + * @param widgetProvider the widget creator function + * @return this + * @see IWidgetProvider + */ + public DynamicSyncHandler widgetProvider(IWidgetProvider widgetProvider) { + this.widgetProvider = widgetProvider; + return this; + } + + /** + * An internal function which is used to link the {@link com.cleanroommc.modularui.widgets.DynamicSyncedWidget}. + */ + @ApiStatus.Internal + public DynamicSyncHandler onWidgetUpdate(Consumer onWidgetUpdate) { + this.onWidgetUpdate = onWidgetUpdate; + return this; + } + + public interface IWidgetProvider { + + /** + * This is the function which creates a widget on client and server. + * In this method sync handlers can only be registered with {@link PanelSyncManager#getOrCreateSyncHandler(String, int, Class, Supplier)}. + * + * @param syncManager the sync manager of the current panel + * @param buf data which was passed in the notify method + * @return a new widget or null if widget shouldn't be updated + */ + @Nullable IWidget createWidget(PanelSyncManager syncManager, PacketBuffer buf); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java b/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java index 2e7e32cdf..60640ae0f 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ItemSlotSH.java @@ -25,7 +25,6 @@ public class ItemSlotSH extends SyncHandler { private ItemStack lastStoredItem; private boolean registered = false; - @ApiStatus.Internal public ItemSlotSH(ModularSlot slot) { this.slot = slot; } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java index 6bd601ebb..07f90d78d 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/ModularSyncManager.java @@ -48,8 +48,8 @@ public void construct(String mainPanelName, PanelSyncManager mainPSM) { if (this.mainPSM.getSlotGroup(PLAYER_INVENTORY) == null) { this.mainPSM.bindPlayerInventory(getPlayer()); } - open(mainPanelName, mainPSM); mainPSM.syncValue(CURSOR_KEY, this.cursorSlotSyncHandler); + open(mainPanelName, mainPSM); } public PanelSyncManager getMainPSM() { @@ -68,6 +68,10 @@ public void onOpen() { this.panelSyncManagerMap.values().forEach(PanelSyncManager::onOpen); } + public void onUpdate() { + this.panelSyncManagerMap.values().forEach(PanelSyncManager::onUpdate); + } + public PanelSyncManager getPanelSyncManager(String panelName) { PanelSyncManager psm = this.panelSyncManagerMap.get(panelName); if (psm != null) return psm; @@ -106,10 +110,10 @@ public boolean isOpen(String panelName) { return this.panelSyncManagerMap.containsKey(panelName); } - public void receiveWidgetUpdate(String panelName, String mapKey, int id, PacketBuffer buf) throws IOException { + public void receiveWidgetUpdate(String panelName, String mapKey, boolean action, int id, PacketBuffer buf) throws IOException { PanelSyncManager psm = this.panelSyncManagerMap.get(panelName); if (psm != null) { - psm.receiveWidgetUpdate(mapKey, id, buf); + psm.receiveWidgetUpdate(mapKey, action, id, buf); } else if (!this.panelHistory.contains(panelName)) { ModularUI.LOGGER.throwing(new IllegalStateException("A packet was send to panel '" + panelName + "' which was not opened yet!.")); } diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java index 61b3aae7c..97b9ab265 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncHandler.java @@ -57,7 +57,7 @@ private void openPanel(boolean syncToServer) { if (this.syncManager != null && this.syncManager.getModularSyncManager() != getSyncManager().getModularSyncManager()) { throw new IllegalStateException("Can't reopen synced panel in another screen!"); } else if (this.syncManager == null) { - this.syncManager = new PanelSyncManager(); + this.syncManager = new PanelSyncManager(client); this.openedPanel = Objects.requireNonNull(createUI(this.syncManager)); this.panelName = this.openedPanel.getName(); this.openedPanel.setSyncHandler(this); diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java index 7a5de71a7..16bdeffd1 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/PanelSyncManager.java @@ -2,15 +2,20 @@ import com.cleanroommc.modularui.ModularUI; import com.cleanroommc.modularui.api.IPanelHandler; +import com.cleanroommc.modularui.api.ISyncedAction; +import com.cleanroommc.modularui.network.NetworkHandler; +import com.cleanroommc.modularui.network.packets.PacketSyncHandler; import com.cleanroommc.modularui.screen.ModularContainer; import com.cleanroommc.modularui.widgets.slot.ModularSlot; import com.cleanroommc.modularui.widgets.slot.SlotGroup; import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.entity.player.EntityPlayerMP; import net.minecraft.item.ItemStack; import net.minecraft.network.PacketBuffer; import net.minecraftforge.items.wrapper.PlayerMainInvWrapper; +import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap; import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenHashMap; import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; @@ -20,27 +25,36 @@ import java.io.IOException; import java.util.*; import java.util.function.Consumer; +import java.util.function.Supplier; public class PanelSyncManager { private final Map syncHandlers = new Object2ObjectLinkedOpenHashMap<>(); private final Map slotGroups = new Object2ObjectOpenHashMap<>(); private final Map reverseSyncHandlers = new Object2ObjectOpenHashMap<>(); + private final Map syncedActions = new Object2ObjectOpenHashMap<>(); private final Map subPanels = new Object2ObjectArrayMap<>(); private ModularSyncManager modularSyncManager; private String panelName; private boolean init = true; + private boolean locked = false; + private boolean allowSyncHandlerRegistration = false; + private final boolean client; private final List> openListener = new ArrayList<>(); private final List> closeListener = new ArrayList<>(); + private final List tickListener = new ArrayList<>(); - public PanelSyncManager() {} + public PanelSyncManager(boolean client) { + this.client = client; + } @ApiStatus.Internal public void initialize(String panelName, ModularSyncManager msm) { this.modularSyncManager = msm; this.panelName = panelName; this.syncHandlers.forEach((mapKey, syncHandler) -> syncHandler.init(mapKey, this)); + this.locked = true; this.init = true; this.subPanels.forEach((s, syncHandler) -> msm.getMainPSM().registerPanelSyncHandler(s, syncHandler)); } @@ -86,7 +100,7 @@ public boolean isInitialised() { return this.modularSyncManager != null; } - public void detectAndSendChanges(boolean init) { + void detectAndSendChanges(boolean init) { if (!isClient()) { for (SyncHandler syncHandler : this.syncHandlers.values()) { syncHandler.detectAndSendChanges(init || this.init); @@ -95,9 +109,18 @@ public void detectAndSendChanges(boolean init) { this.init = false; } - public void receiveWidgetUpdate(String mapKey, int id, PacketBuffer buf) throws IOException { + void onUpdate() { + this.tickListener.forEach(Runnable::run); + } + + @ApiStatus.Internal + public void receiveWidgetUpdate(String mapKey, boolean action, int id, PacketBuffer buf) throws IOException { + if (action) { + invokeSyncedAction(mapKey, buf); + return; + } if (!this.syncHandlers.containsKey(mapKey)) { - ModularUI.LOGGER.warn("SyncHandler \"{}\" does not exist for panel \"{}\"! ID was {}.", mapKey, panelName, id); + ModularUI.LOGGER.warn("SyncHandler '{}' does not exist for panel '{}'! ID was {}.", mapKey, panelName, id); return; } SyncHandler syncHandler = this.syncHandlers.get(mapKey); @@ -108,6 +131,18 @@ public void receiveWidgetUpdate(String mapKey, int id, PacketBuffer buf) throws } } + private boolean invokeSyncedAction(String mapKey, PacketBuffer buf) { + ISyncedAction syncedAction = this.syncedActions.get(mapKey); + if (syncedAction == null) { + ModularUI.LOGGER.warn("SyncAction '{}' does not exist for panel '{}'!.", mapKey, panelName); + return false; + } + allowTemporarySyncHandlerRegistration(true); + syncedAction.invoke(buf); + allowTemporarySyncHandlerRegistration(false); + return true; + } + public ItemStack getCursorItem() { return getModularSyncManager().getCursorItem(); } @@ -121,6 +156,15 @@ public boolean hasSyncHandler(SyncHandler syncHandler) { } private void putSyncValue(String name, int id, SyncHandler syncHandler) { + if (isLocked()) { + // registration of sync handlers forbidden + if (this.allowSyncHandlerRegistration) { + // lock can be bypassed currently, but it wasn't used + throw new IllegalStateException("SyncHandlers must be registered during panel building. Please use getOrCreateSyncHandler()!"); + } else { + throw new IllegalStateException("SyncHandlers must be registered during panel building. The only exceptions is via a DynamicSyncHandler and sync functions!"); + } + } String key = makeSyncKey(name, id); String currentKey = this.reverseSyncHandlers.get(syncHandler); if (currentKey != null) { @@ -169,6 +213,16 @@ public PanelSyncManager itemSlot(int id, ModularSlot slot) { return itemSlot("_", id, slot); } + public DynamicSyncHandler dynamicSyncHandler(String key, DynamicSyncHandler.IWidgetProvider widgetProvider) { + return dynamicSyncHandler(key, 0, widgetProvider); + } + + public DynamicSyncHandler dynamicSyncHandler(String key, int id, DynamicSyncHandler.IWidgetProvider widgetProvider) { + DynamicSyncHandler syncHandler = new DynamicSyncHandler().widgetProvider(widgetProvider); + syncValue(key, id, syncHandler); + return syncHandler; + } + /** * Creates a synced panel handler. This can be used to automatically handle syncing for synced panels. * Synced panels do not need to be synced themselves, but contain at least one widget which is synced. @@ -244,6 +298,75 @@ public PanelSyncManager addCloseListener(Consumer listener) { return this; } + public PanelSyncManager onClientTick(Runnable runnable) { + if (this.client) { + this.tickListener.add(runnable); + } + return this; + } + + public PanelSyncManager onServerTick(Runnable runnable) { + if (!this.client) { + this.tickListener.add(runnable); + } + return this; + } + + public PanelSyncManager onCommonTick(Runnable runnable) { + this.tickListener.add(runnable); + return this; + } + + public PanelSyncManager registerSyncedAction(String mapKey, ISyncedAction action) { + this.syncedActions.put(mapKey, action); + return this; + } + + public void callSyncedAction(String mapKey, PacketBuffer packet) { + if (invokeSyncedAction(mapKey, packet)) { + PacketSyncHandler packetSyncHandler = new PacketSyncHandler(this.panelName, mapKey, true, packet); + if (isClient()) { + NetworkHandler.sendToServer(packetSyncHandler); + } else { + NetworkHandler.sendToPlayer(packetSyncHandler, (EntityPlayerMP) getPlayer()); + } + } + } + + public void callSyncedAction(String mapKey, Consumer packetBuilder) { + PacketBuffer packet = new PacketBuffer(Unpooled.buffer()); + packetBuilder.accept(packet); + callSyncedAction(mapKey, packet); + } + + public T getOrCreateSyncHandler(String name, Class clazz, Supplier supplier) { + return getOrCreateSyncHandler(name, 0, clazz, supplier); + } + + public T getOrCreateSyncHandler(String name, int id, Class clazz, Supplier supplier) { + SyncHandler syncHandler = getSyncHandler(name); + if (syncHandler == null) { + if (isLocked() && !this.allowSyncHandlerRegistration) { + // registration is locked, and we don't have permission to temporarily bypass lock + throw new IllegalStateException("SyncHandlers must be registered during panel building. The only exceptions is via a DynamicSyncHandler and sync functions!"); + } + T t = supplier.get(); + boolean l = this.locked; + this.locked = false; // bypass possible lock + putSyncValue(name, id, t); + this.locked = l; + return t; + } + if (clazz.isAssignableFrom(syncHandler.getClass())) { + return (T) syncHandler; + } + throw new IllegalStateException("SyncHandler for key " + makeSyncKey(name, id) + " is of type " + syncHandler.getClass() + ", but type " + clazz + " was expected!"); + } + + public ItemSlotSH getOrCreateSlot(String name, int id, Supplier slotSupplier) { + return getOrCreateSyncHandler(name, id, ItemSlotSH.class, () -> new ItemSlotSH(slotSupplier.get())); + } + public SlotGroup getSlotGroup(String name) { return this.slotGroups.get(name); } @@ -276,7 +399,15 @@ public String getPanelName() { } public boolean isClient() { - return getModularSyncManager().isClient(); + return this.client; + } + + public boolean isLocked() { + return locked; + } + + void allowTemporarySyncHandlerRegistration(boolean allow) { + this.allowSyncHandlerRegistration = allow; } public static String makeSyncKey(String name, int id) { diff --git a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java index 3f0376677..fb4945437 100644 --- a/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java +++ b/src/main/java/com/cleanroommc/modularui/value/sync/SyncHandler.java @@ -184,7 +184,7 @@ public static void sendToClient(String panel, PacketBuffer buffer, SyncHandler s if (!syncHandler.isValid()) { throw new IllegalStateException(); } - NetworkHandler.sendToPlayer(new PacketSyncHandler(panel, syncHandler.getKey(), buffer), (EntityPlayerMP) syncHandler.syncManager.getPlayer()); + NetworkHandler.sendToPlayer(new PacketSyncHandler(panel, syncHandler.getKey(), false, buffer), (EntityPlayerMP) syncHandler.syncManager.getPlayer()); } public static void sendToServer(String panel, PacketBuffer buffer, SyncHandler syncHandler) { @@ -193,6 +193,6 @@ public static void sendToServer(String panel, PacketBuffer buffer, SyncHandler s if (!syncHandler.isValid()) { throw new IllegalStateException(); } - NetworkHandler.sendToServer(new PacketSyncHandler(panel, syncHandler.getKey(), buffer)); + NetworkHandler.sendToServer(new PacketSyncHandler(panel, syncHandler.getKey(), false, buffer)); } } diff --git a/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java b/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java index 55309d197..818d34db3 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/AbstractParentWidget.java @@ -81,7 +81,7 @@ protected boolean addChild(I child, int index) { } this.children.add(index, child); if (isValid()) { - child.initialise(this); + child.initialise(this, true); } onChildAdd(child); return true; diff --git a/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java b/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java index f1f5e73c7..02bf74348 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/EmptyWidget.java @@ -26,7 +26,7 @@ public ModularScreen getScreen() { } @Override - public void initialise(@NotNull IWidget parent) { + public void initialise(@NotNull IWidget parent, boolean late) { this.parent = parent; } diff --git a/src/main/java/com/cleanroommc/modularui/widget/SingleChildWidget.java b/src/main/java/com/cleanroommc/modularui/widget/SingleChildWidget.java index a90d00af5..d6c0581e2 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/SingleChildWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/SingleChildWidget.java @@ -32,7 +32,7 @@ public W child(IWidget child) { this.child = child; if (isValid()) { - child.initialise(this); + child.initialise(this, true); } updateList(); return getThis(); diff --git a/src/main/java/com/cleanroommc/modularui/widget/Widget.java b/src/main/java/com/cleanroommc/modularui/widget/Widget.java index f5c9d1040..27cb9918e 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/Widget.java +++ b/src/main/java/com/cleanroommc/modularui/widget/Widget.java @@ -80,10 +80,11 @@ public class Widget> implements IWidget, IPositioned, ITo * Called when a panel is opened. Use {@link #onInit()} and {@link #afterInit()} for custom logic. * * @param parent the parent this element belongs to + * @param late true if this is called some time after the widget tree of the parent has been initialised */ @ApiStatus.Internal @Override - public final void initialise(@NotNull IWidget parent) { + public final void initialise(@NotNull IWidget parent, boolean late) { if (!(this instanceof ModularPanel)) { this.parent = parent; this.panel = parent.getPanel(); @@ -101,7 +102,7 @@ public final void initialise(@NotNull IWidget parent) { } this.valid = true; if (!getScreen().isClientOnly()) { - initialiseSyncHandler(getScreen().getSyncManager()); + initialiseSyncHandler(getScreen().getSyncManager(), late); } if (isExcludeAreaInJei()) { getContext().getJeiSettings().addJeiExclusionArea(this); @@ -109,7 +110,7 @@ public final void initialise(@NotNull IWidget parent) { onInit(); if (hasChildren()) { for (IWidget child : getChildren()) { - child.initialise(this); + child.initialise(this, false); } } afterInit(); @@ -133,7 +134,7 @@ public void afterInit() {} * Custom logic should be handled in {@link #isValidSyncHandler(SyncHandler)}. */ @Override - public void initialiseSyncHandler(ModularSyncManager syncManager) { + public void initialiseSyncHandler(ModularSyncManager syncManager, boolean late) { if (this.syncKey != null) { this.syncHandler = syncManager.getSyncHandler(getPanel().getName(), this.syncKey); } diff --git a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java index 83dff9a03..1a336fd52 100644 --- a/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java +++ b/src/main/java/com/cleanroommc/modularui/widget/WidgetTree.java @@ -392,8 +392,13 @@ public static void collectSyncValues(PanelSyncManager syncManager, ModularPanel @ApiStatus.Internal public static void collectSyncValues(PanelSyncManager syncManager, ModularPanel panel, boolean includePanel) { + collectSyncValues(syncManager, panel.getName(), panel, includePanel); + } + + @ApiStatus.Internal + public static void collectSyncValues(PanelSyncManager syncManager, String panelName, IWidget panel, boolean includePanel) { AtomicInteger id = new AtomicInteger(0); - String syncKey = ModularSyncManager.AUTO_SYNC_PREFIX + panel.getName(); + String syncKey = ModularSyncManager.AUTO_SYNC_PREFIX + panelName; foreachChildBFS(panel, widget -> { if (widget instanceof ISynced synced) { if (synced.isSynced() && !syncManager.hasSyncHandler(synced.getSyncHandler())) { diff --git a/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java new file mode 100644 index 000000000..4cc4e0433 --- /dev/null +++ b/src/main/java/com/cleanroommc/modularui/widgets/DynamicSyncedWidget.java @@ -0,0 +1,60 @@ +package com.cleanroommc.modularui.widgets; + +import com.cleanroommc.modularui.api.widget.IWidget; +import com.cleanroommc.modularui.value.sync.DynamicSyncHandler; +import com.cleanroommc.modularui.value.sync.SyncHandler; +import com.cleanroommc.modularui.widget.Widget; +import com.cleanroommc.modularui.widget.WidgetTree; + +import org.jetbrains.annotations.NotNull; + +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +/** + * A widget which can update its child based on a function in {@link DynamicSyncHandler}. + * Such a sync handler must be supplied or else this widget has no effect. + * The dynamic child can be a widget tree of any size which can also contain {@link SyncHandler}s. These sync handlers MUST be registered + * via {@link com.cleanroommc.modularui.value.sync.PanelSyncManager#getOrCreateSyncHandler(String, Class, Supplier)} + * @param + */ +public class DynamicSyncedWidget> extends Widget { + + private DynamicSyncHandler syncHandler; + private IWidget child; + + @Override + public boolean isValidSyncHandler(SyncHandler syncHandler) { + if (syncHandler instanceof DynamicSyncHandler dynamicSyncHandler) { + this.syncHandler = dynamicSyncHandler; + dynamicSyncHandler.onWidgetUpdate(this::updateChild); + return true; + } + return false; + } + + @Override + public @NotNull List getChildren() { + if (this.child == null) { + return Collections.emptyList(); + } else { + return Collections.singletonList(this.child); + } + } + + private void updateChild(IWidget widget) { + this.child = widget; + if (isValid()) { + this.child.initialise(this, true); + scheduleResize(); + } + } + + public W syncHandler(DynamicSyncHandler syncHandler) { + this.syncHandler = syncHandler; + setSyncHandler(syncHandler); + syncHandler.onWidgetUpdate(this::updateChild); + return getThis(); + } +} diff --git a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java index 4df6f48c5..d1cf17ae9 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/Expandable.java @@ -157,7 +157,7 @@ public Expandable normalView(IWidget normalView) { this.normalView = normalView; this.children.set(0, normalView); if (isValid()) { - this.normalView.initialise(this); + this.normalView.initialise(this, true); } return this; } @@ -166,7 +166,7 @@ public Expandable expandedView(IWidget expandedView) { this.expandedView = expandedView; this.children.set(1, expandedView); if (isValid()) { - this.expandedView.initialise(this); + this.expandedView.initialise(this, true); } return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java b/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java index 786f241a2..576c8b8be 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/SortableListWidget.java @@ -214,7 +214,7 @@ public boolean removeSelfFromList() { public Item child(IWidget widget) { this.children = Collections.singletonList(widget); - if (isValid()) widget.initialise(this); + if (isValid()) widget.initialise(this, true); return this; } diff --git a/src/main/java/com/cleanroommc/modularui/widgets/layout/Grid.java b/src/main/java/com/cleanroommc/modularui/widgets/layout/Grid.java index 2f4261100..41a1d5245 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/layout/Grid.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/layout/Grid.java @@ -189,7 +189,7 @@ public boolean addChild(IWidget child, int index) { } super.getChildren().add(index, child); if (isValid()) { - child.initialise(this); + child.initialise(this, true); } onChildAdd(child); this.dirty = true; diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java index 0ba196bdf..b61b68d15 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/ItemSlot.java @@ -163,15 +163,19 @@ public boolean handleAsVanillaSlot() { } public ItemSlot slot(ModularSlot slot) { - this.syncHandler = new ItemSlotSH(slot); - setSyncHandler(this.syncHandler); - return this; + return syncHandler(new ItemSlotSH(slot)); } public ItemSlot slot(IItemHandlerModifiable itemHandler, int index) { return slot(new ModularSlot(itemHandler, index)); } + public ItemSlot syncHandler(ItemSlotSH syncHandler) { + this.syncHandler = syncHandler; + setSyncHandler(this.syncHandler); + return this; + } + @SideOnly(Side.CLIENT) private void drawSlot(ModularSlot slotIn) { GuiScreen guiScreen = getScreen().getScreenWrapper().getGuiScreen(); diff --git a/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java b/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java index 26a45eeb0..57ff79b45 100644 --- a/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java +++ b/src/main/java/com/cleanroommc/modularui/widgets/slot/SlotGroup.java @@ -12,7 +12,7 @@ * A slot group is a group of slots that can be sorted (via Inventory BogoSorter) * and be shift clicked into. The slot group must exist on server and client side. * Slot groups must be registered via - * {@link com.cleanroommc.modularui.value.sync.GuiSyncManager#registerSlotGroup(String, int, boolean)} + * {@link com.cleanroommc.modularui.value.sync.PanelSyncManager#registerSlotGroup(String, int, boolean)} * or overloads of the method (except it's a singleton). */ public class SlotGroup {