From 6e04c6ab6db99183d4c67ae75b342aecad7a387f Mon Sep 17 00:00:00 2001 From: Sollace Date: Sun, 11 Sep 2022 12:22:06 +0200 Subject: [PATCH] Store the state in the spellbook and synchronize it between players when multiple are viewing the same book --- .../client/gui/spellbook/DynamicContent.java | 13 +- .../gui/spellbook/SpellbookChapterList.java | 26 ++-- .../SpellbookCraftingPageContent.java | 22 +++- .../SpellbookProfilePageContent.java | 8 +- .../client/gui/spellbook/SpellbookScreen.java | 32 +++-- .../SpellbookTraitDexPageContent.java | 15 ++- .../unicopia/container/SpellbookPage.java | 10 -- .../container/SpellbookScreenHandler.java | 20 ++- .../unicopia/container/SpellbookState.java | 122 ++++++++++++++++++ .../unicopia/container/UScreenHandlers.java | 7 +- .../unicopia/entity/SpellbookEntity.java | 55 +++++++- .../unicopia/item/SpellbookItem.java | 14 +- .../unicopia/network/Channel.java | 5 + .../network/MsgSpellbookStateChanged.java | 52 ++++++++ .../network/datasync/Synchronizable.java | 20 +++ .../unicopia/util/network/S2CPacketType.java | 2 +- 16 files changed, 356 insertions(+), 67 deletions(-) create mode 100644 src/main/java/com/minelittlepony/unicopia/container/SpellbookState.java create mode 100644 src/main/java/com/minelittlepony/unicopia/network/MsgSpellbookStateChanged.java create mode 100644 src/main/java/com/minelittlepony/unicopia/network/datasync/Synchronizable.java diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/DynamicContent.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/DynamicContent.java index 27f06868..f0c39b08 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/DynamicContent.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/DynamicContent.java @@ -8,18 +8,18 @@ import com.minelittlepony.common.client.gui.dimension.Bounds; import com.minelittlepony.unicopia.client.gui.DrawableUtil; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Content; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Drawable; +import com.minelittlepony.unicopia.container.SpellbookState; import com.minelittlepony.unicopia.entity.player.Pony; import net.minecraft.client.MinecraftClient; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; import net.minecraft.util.*; -import net.minecraft.util.math.MathHelper; public class DynamicContent implements Content { private static final Text UNKNOWN = Text.of("???"); private static final Text UNKNOWN_LEVEL = Text.literal("Level: ???").formatted(Formatting.DARK_GREEN); - private int offset = 0; + private SpellbookState.PageState state = new SpellbookState.PageState(); private final List pages = new ArrayList<>(); private Bounds bounds = Bounds.empty(); @@ -30,7 +30,7 @@ public class DynamicContent implements Content { @Override public void draw(MatrixStack matrices, int mouseX, int mouseY, IViewRoot container) { - int pageIndex = offset * 2; + int pageIndex = state.getOffset() * 2; getPage(pageIndex).ifPresent(page -> page.draw(matrices, mouseX, mouseY, container)); @@ -45,7 +45,7 @@ public class DynamicContent implements Content { @Override public void copyStateFrom(Content old) { if (old instanceof DynamicContent o) { - offset = o.offset; + state = o.state; setBounds(o.bounds); } } @@ -67,10 +67,11 @@ public class DynamicContent implements Content { } @Override - public void init(SpellbookScreen screen) { + public void init(SpellbookScreen screen, Identifier pageId) { + state = screen.getState().getState(pageId); setBounds(screen.getFrameBounds()); screen.addPageButtons(187, 30, 350, incr -> { - offset = MathHelper.clamp(offset + incr, 0, (int)Math.ceil(pages.size() / 2F) - 1); + state.swap(incr, (int)Math.ceil(pages.size() / 2F)); }); } diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookChapterList.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookChapterList.java index 71abaf26..7ea25e84 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookChapterList.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookChapterList.java @@ -1,7 +1,7 @@ package com.minelittlepony.unicopia.client.gui.spellbook; import java.util.*; -import java.util.function.Consumer; +import java.util.function.BiConsumer; import java.util.stream.Stream; import com.minelittlepony.common.client.gui.IViewRoot; @@ -15,13 +15,14 @@ public class SpellbookChapterList { public static final Identifier PROFILE_ID = Unicopia.id("profile"); public static final Identifier TRAIT_DEX_ID = Unicopia.id("traits"); - private final Chapter craftingChapter; + private final SpellbookScreen screen; - private Optional currentChapter = Optional.empty(); + private final Chapter craftingChapter; private final Map chapters = new HashMap<>(); - public SpellbookChapterList(Chapter craftingChapter, Chapter... builtIn) { + public SpellbookChapterList(SpellbookScreen screen, Chapter craftingChapter, Chapter... builtIn) { + this.screen = screen; this.craftingChapter = craftingChapter; SpellbookChapterLoader.INSTANCE.getChapters().forEach(chapter -> { chapters.put(chapter.id(), chapter); @@ -45,11 +46,8 @@ public class SpellbookChapterList { chapters.put(chapter.id(), chapter); }); } - return currentChapter.map(chapters::get).orElse(craftingChapter); - } - public void setCurrentChapter(Chapter chapter) { - currentChapter = Optional.of(chapter.id()); + return screen.getState().getCurrentPageId().map(chapters::get).orElse(craftingChapter); } public record Chapter ( @@ -70,15 +68,19 @@ public class SpellbookChapterList { } public interface Content extends Drawable { - void init(SpellbookScreen screen); + void init(SpellbookScreen screen, Identifier pageId); default void copyStateFrom(Content old) {} - static Optional of(Consumer init, Drawable obj) { + default boolean showInventory() { + return false; + } + + static Optional of(BiConsumer init, Drawable obj) { return Optional.of(new Content() { @Override - public void init(SpellbookScreen screen) { - init.accept(screen); + public void init(SpellbookScreen screen, Identifier pageId) { + init.accept(screen, pageId); } @Override diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookCraftingPageContent.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookCraftingPageContent.java index c2b3de79..7136d728 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookCraftingPageContent.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookCraftingPageContent.java @@ -6,15 +6,19 @@ import com.minelittlepony.common.client.gui.element.Label; import com.minelittlepony.unicopia.ability.magic.spell.crafting.SpellbookRecipe; import com.minelittlepony.unicopia.client.gui.DrawableUtil; import com.minelittlepony.unicopia.container.SpellbookPage; +import com.minelittlepony.unicopia.container.SpellbookState; import com.minelittlepony.unicopia.item.URecipes; import com.mojang.blaze3d.systems.RenderSystem; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; public class SpellbookCraftingPageContent extends ScrollContainer implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener { private final SpellbookScreen screen; + private SpellbookState.PageState state = new SpellbookState.PageState(); + public SpellbookCraftingPageContent(SpellbookScreen screen) { this.screen = screen; backgroundColor = 0xFFf9efd3; @@ -22,8 +26,11 @@ public class SpellbookCraftingPageContent extends ScrollContainer implements Spe } @Override - public void init(SpellbookScreen screen) { - screen.addPageButtons(187, 300, 350, SpellbookPage::swap); + public void init(SpellbookScreen screen, Identifier pageId) { + state = screen.getState().getState(pageId); + screen.addPageButtons(187, 300, 350, incr -> { + state.swap(incr, SpellbookPage.VALUES.length); + }); initContents(); screen.addDrawable(this); ((IViewRoot)screen).getChildElements().add(this); @@ -34,9 +41,9 @@ public class SpellbookCraftingPageContent extends ScrollContainer implements Spe int headerColor = mouseY % 255; - DrawableUtil.drawScaledText(matrices, SpellbookPage.getCurrent().getLabel(), screen.getFrameBounds().left + screen.getFrameBounds().width / 2 + 20, SpellbookScreen.TITLE_Y, 1.3F, headerColor); + DrawableUtil.drawScaledText(matrices, SpellbookPage.VALUES[state.getOffset()].getLabel(), screen.getFrameBounds().left + screen.getFrameBounds().width / 2 + 20, SpellbookScreen.TITLE_Y, 1.3F, headerColor); - Text pageText = Text.translatable("%s/%s", SpellbookPage.getCurrent().ordinal() + 1, SpellbookPage.VALUES.length); + Text pageText = Text.translatable("%s/%s", state.getOffset() + 1, SpellbookPage.VALUES.length); textRenderer.draw(matrices, pageText, 337 - textRenderer.getWidth(pageText) / 2F, 190, headerColor); } @@ -49,11 +56,16 @@ public class SpellbookCraftingPageContent extends ScrollContainer implements Spe init(this::initPageContent); } + @Override + public boolean showInventory() { + return SpellbookPage.VALUES[state.getOffset()] == SpellbookPage.INVENTORY; + } + private void initPageContent() { getContentPadding().setVertical(10); getContentPadding().bottom = 30; - switch (SpellbookPage.getCurrent()) { + switch (SpellbookPage.VALUES[state.getOffset()]) { case INVENTORY: // handled elsewhere break; diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookProfilePageContent.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookProfilePageContent.java index 5965a9d4..bb9bff1a 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookProfilePageContent.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookProfilePageContent.java @@ -14,6 +14,7 @@ import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.resource.language.I18n; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.minecraft.util.math.MathHelper; public class SpellbookProfilePageContent extends DrawableHelper implements SpellbookChapterList.Content { @@ -28,10 +29,15 @@ public class SpellbookProfilePageContent extends DrawableHelper implements Spell } @Override - public void init(SpellbookScreen screen) { + public void init(SpellbookScreen screen, Identifier pageId) { } + @Override + public boolean showInventory() { + return true; + } + @Override public void draw(MatrixStack matrices, int mouseX, int mouseY, IViewRoot container) { diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java index 69fd85b9..a7f21e98 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookScreen.java @@ -10,9 +10,10 @@ import com.minelittlepony.common.client.gui.element.Button; import com.minelittlepony.common.client.gui.sprite.TextureSprite; import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*; -import com.minelittlepony.unicopia.container.SpellbookPage; -import com.minelittlepony.unicopia.container.SpellbookScreenHandler; +import com.minelittlepony.unicopia.container.*; import com.minelittlepony.unicopia.container.SpellbookScreenHandler.*; +import com.minelittlepony.unicopia.network.Channel; +import com.minelittlepony.unicopia.network.MsgSpellbookStateChanged; import com.mojang.blaze3d.platform.GlStateManager; import com.mojang.blaze3d.systems.RenderSystem; @@ -25,6 +26,7 @@ import net.minecraft.client.texture.NativeImage; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.entity.player.PlayerInventory; import net.minecraft.screen.slot.Slot; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.sound.SoundEvents; import net.minecraft.text.Text; import net.minecraft.util.Identifier; @@ -42,11 +44,12 @@ public class SpellbookScreen extends HandledScreen imple private final RecipeBookWidget recipeBook = new RecipeBookWidget(); - private final Chapter craftingChapter = new Chapter(SpellbookChapterList.CRAFTING_ID, TabSide.LEFT, 0, 0, Optional.of(new SpellbookCraftingPageContent(this))); - private final Chapter profileChapter = new Chapter(SpellbookChapterList.PROFILE_ID, TabSide.LEFT, 1, 0, Optional.of(new SpellbookProfilePageContent(this))); - private final Chapter traitdexChapter = new Chapter(SpellbookChapterList.TRAIT_DEX_ID, TabSide.LEFT, 3, 0, Optional.of(new SpellbookTraitDexPageContent(this))); - - private final SpellbookChapterList chapters = new SpellbookChapterList(craftingChapter, profileChapter, traitdexChapter); + private final Chapter craftingChapter; + private final SpellbookChapterList chapters = new SpellbookChapterList(this, + craftingChapter = new Chapter(SpellbookChapterList.CRAFTING_ID, TabSide.LEFT, 0, 0, Optional.of(new SpellbookCraftingPageContent(this))), + new Chapter(SpellbookChapterList.PROFILE_ID, TabSide.LEFT, 1, 0, Optional.of(new SpellbookProfilePageContent(this))), + new Chapter(SpellbookChapterList.TRAIT_DEX_ID, TabSide.LEFT, 3, 0, Optional.of(new SpellbookTraitDexPageContent(this))) + ); private final SpellbookTabBar tabs = new SpellbookTabBar(this, chapters); private Bounds contentBounds = Bounds.empty(); @@ -59,11 +62,17 @@ public class SpellbookScreen extends HandledScreen imple handler.addSlotShowingCondition(slotType -> { if (slotType == SlotType.INVENTORY) { - return chapters.getCurrentChapter() == profileChapter - || (chapters.getCurrentChapter() == craftingChapter && SpellbookPage.getCurrent() == SpellbookPage.INVENTORY); + return chapters.getCurrentChapter().content().filter(Content::showInventory).isPresent(); } return chapters.getCurrentChapter() == craftingChapter; }); + handler.getSpellbookState().setSynchronizer(state -> { + Channel.CLIENT_SPELLBOOK_UPDATE.send(new MsgSpellbookStateChanged(handler.syncId, state)); + }); + } + + public SpellbookState getState() { + return handler.getSpellbookState(); } public void addPageButtons(int buttonY, int prevX, int nextX, IntConsumer pageAction) { @@ -100,7 +109,7 @@ public class SpellbookScreen extends HandledScreen imple public void init() { super.init(); tabs.init(); - chapters.getCurrentChapter().content().ifPresent(content -> content.init(this)); + chapters.getCurrentChapter().content().ifPresent(content -> content.init(this, chapters.getCurrentChapter().id())); } @Override @@ -196,9 +205,8 @@ public class SpellbookScreen extends HandledScreen imple @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { return tabs.getAllTabs().anyMatch(tab -> { - if (tab.bounds().contains(mouseX, mouseY) && chapters.getCurrentChapter() != tab.chapter()) { - chapters.setCurrentChapter(tab.chapter()); + getState().setCurrentPageId(tab.chapter().id()); GameGui.playSound(SoundEvents.ITEM_BOOK_PAGE_TURN); clearAndInit(); return true; diff --git a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java index 01bda5b3..1e8d7e9d 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java +++ b/src/main/java/com/minelittlepony/unicopia/client/gui/spellbook/SpellbookTraitDexPageContent.java @@ -10,6 +10,7 @@ import com.minelittlepony.common.client.gui.element.Label; import com.minelittlepony.common.client.gui.sprite.TextureSprite; import com.minelittlepony.unicopia.ability.magic.spell.trait.*; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookScreen.ImageButton; +import com.minelittlepony.unicopia.container.SpellbookState; import com.minelittlepony.unicopia.entity.player.Pony; import com.mojang.blaze3d.systems.RenderSystem; @@ -18,13 +19,13 @@ import net.minecraft.client.gui.DrawableHelper; import net.minecraft.client.util.math.MatrixStack; import net.minecraft.item.*; import net.minecraft.text.Text; +import net.minecraft.util.Identifier; import net.minecraft.util.collection.DefaultedList; -import net.minecraft.util.math.MathHelper; public class SpellbookTraitDexPageContent extends DrawableHelper implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener { private final Trait[] traits = Trait.values(); - private int offset; + private SpellbookState.PageState state = new SpellbookState.PageState(); private final DexPage leftPage = new DexPage(); private final DexPage rightPage = new DexPage(); @@ -41,12 +42,14 @@ public class SpellbookTraitDexPageContent extends DrawableHelper implements Spel } @Override - public void init(SpellbookScreen screen) { - int page = offset * 2; + public void init(SpellbookScreen screen, Identifier pageId) { + state = screen.getState().getState(pageId); + + int page = state.getOffset() * 2; leftPage.init(screen, page); rightPage.init(screen, page + 1); screen.addPageButtons(187, 30, 350, incr -> { - offset = MathHelper.clamp(offset + incr, 0, (int)Math.ceil(traits.length / 2F) - 1); + state.swap(incr, (int)Math.ceil(traits.length / 2F)); leftPage.scrollbar.scrollBy(leftPage.scrollbar.getVerticalScrollAmount()); rightPage.scrollbar.scrollBy(rightPage.scrollbar.getVerticalScrollAmount()); }); @@ -54,7 +57,7 @@ public class SpellbookTraitDexPageContent extends DrawableHelper implements Spel @Override public void onRecipesChanged() { - init(screen); + init(screen, SpellbookChapterList.TRAIT_DEX_ID); } private final class DexPage extends ScrollContainer { diff --git a/src/main/java/com/minelittlepony/unicopia/container/SpellbookPage.java b/src/main/java/com/minelittlepony/unicopia/container/SpellbookPage.java index 513e5e4a..2341c815 100644 --- a/src/main/java/com/minelittlepony/unicopia/container/SpellbookPage.java +++ b/src/main/java/com/minelittlepony/unicopia/container/SpellbookPage.java @@ -1,26 +1,16 @@ package com.minelittlepony.unicopia.container; import net.minecraft.text.Text; -import net.minecraft.util.math.MathHelper; public enum SpellbookPage { INVENTORY, RECIPES; public static final SpellbookPage[] VALUES = values(); - private static int current; private final Text label = Text.translatable("gui.unicopia.spellbook.page." + name().toLowerCase()); public Text getLabel() { return label; } - - public static SpellbookPage getCurrent() { - return VALUES[current]; - } - - public static void swap(int increment) { - current = MathHelper.clamp(current + increment, 0, VALUES.length - 1); - } } diff --git a/src/main/java/com/minelittlepony/unicopia/container/SpellbookScreenHandler.java b/src/main/java/com/minelittlepony/unicopia/container/SpellbookScreenHandler.java index 13fe334d..0848f8dc 100644 --- a/src/main/java/com/minelittlepony/unicopia/container/SpellbookScreenHandler.java +++ b/src/main/java/com/minelittlepony/unicopia/container/SpellbookScreenHandler.java @@ -3,6 +3,8 @@ package com.minelittlepony.unicopia.container; import java.util.*; import java.util.function.Predicate; +import org.jetbrains.annotations.Nullable; + import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.ability.magic.spell.crafting.SpellbookRecipe; @@ -23,6 +25,7 @@ import net.minecraft.inventory.CraftingInventory; import net.minecraft.inventory.CraftingResultInventory; import net.minecraft.inventory.Inventory; import net.minecraft.item.ItemStack; +import net.minecraft.network.PacketByteBuf; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.screen.PlayerScreenHandler; import net.minecraft.screen.ScreenHandler; @@ -59,12 +62,19 @@ public class SpellbookScreenHandler extends ScreenHandler { private Predicate canShowSlots; - protected SpellbookScreenHandler(int syncId, PlayerInventory inv) { - this(syncId, inv, ScreenHandlerContext.EMPTY); + private final SpellbookState state; + + @Nullable + public UUID entityId; + + protected SpellbookScreenHandler(int syncId, PlayerInventory inv, PacketByteBuf buf) { + this(syncId, inv, ScreenHandlerContext.EMPTY, new SpellbookState().fromPacket(buf), null); } - public SpellbookScreenHandler(int syncId, PlayerInventory inv, ScreenHandlerContext context) { + public SpellbookScreenHandler(int syncId, PlayerInventory inv, ScreenHandlerContext context, SpellbookState state, UUID entityId) { super(UScreenHandlers.SPELL_BOOK, syncId); + this.entityId = entityId; + this.state = state; inventory = inv; this.context = context; @@ -136,6 +146,10 @@ public class SpellbookScreenHandler extends ScreenHandler { onContentChanged(input); } + public SpellbookState getSpellbookState() { + return state; + } + public void addSlotShowingCondition(Predicate canShowSlots) { this.canShowSlots = canShowSlots; } diff --git a/src/main/java/com/minelittlepony/unicopia/container/SpellbookState.java b/src/main/java/com/minelittlepony/unicopia/container/SpellbookState.java new file mode 100644 index 00000000..84f4d9f1 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/container/SpellbookState.java @@ -0,0 +1,122 @@ +package com.minelittlepony.unicopia.container; + +import java.util.*; +import java.util.function.Consumer; + +import com.minelittlepony.unicopia.network.datasync.Synchronizable; +import com.minelittlepony.unicopia.util.NbtSerialisable; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.nbt.NbtElement; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.MathHelper; + +public class SpellbookState extends Synchronizable implements NbtSerialisable { + public static final SpellbookState INSTANCE = new SpellbookState(); + + private Optional currentPageId = Optional.empty(); + + private final Map states = new HashMap<>(); + + public Optional getCurrentPageId() { + return currentPageId; + } + + public void setCurrentPageId(Identifier pageId) { + currentPageId = Optional.ofNullable(pageId); + synchronize(); + } + + public PageState getState(Identifier pageId) { + return states.computeIfAbsent(pageId, i -> new PageState(page -> synchronize())); + } + + @Override + public void copyFrom(SpellbookState state) { + currentPageId = state.currentPageId; + state.states.forEach((id, page) -> getState(id).copyFrom(page)); + } + + public void toPacket(PacketByteBuf buf) { + buf.writeOptional(currentPageId, PacketByteBuf::writeIdentifier); + buf.writeMap(states, PacketByteBuf::writeIdentifier, (b, v) -> v.toPacket(b)); + } + + public SpellbookState fromPacket(PacketByteBuf buf) { + currentPageId = buf.readOptional(PacketByteBuf::readIdentifier); + buf.readMap(PacketByteBuf::readIdentifier, b -> new PageState(page -> synchronize()).fromPacket(b)).forEach((id, state) -> { + getState(id).copyFrom(state); + }); + return this; + } + + @Override + public void toNBT(NbtCompound compound) { + currentPageId.ifPresent(id -> compound.putString("current_page", id.toString())); + NbtCompound states = new NbtCompound(); + compound.put("states", states); + this.states.forEach((id, page) -> { + states.put(id.toString(), page.toNBT()); + }); + } + + @Override + public void fromNBT(NbtCompound compound) { + currentPageId = compound.contains("current_page", NbtElement.STRING_TYPE) ? Optional.ofNullable(Identifier.tryParse(compound.getString("current_page"))) : Optional.empty(); + NbtCompound states = compound.getCompound("states"); + states.getKeys().stream().forEach(key -> { + Identifier id = Identifier.tryParse(key); + if (id != null) { + getState(id).fromNBT(states.getCompound(key)); + } + }); + } + + public static class PageState extends Synchronizable implements NbtSerialisable { + private int offset; + + public PageState() {} + + PageState(Consumer synchronizer) { + setSynchronizer(synchronizer); + } + + @Override + public void copyFrom(PageState other) { + offset = other.offset; + } + + public void setOffset(int offset) { + this.offset = offset; + synchronize(); + } + + public int getOffset() { + return offset; + } + + public void swap(int incr, int max) { + setOffset(MathHelper.clamp(getOffset() + incr, 0, max - 1)); + } + + public void toPacket(PacketByteBuf buf) { + buf.writeInt(offset); + } + + public PageState fromPacket(PacketByteBuf buf) { + offset = buf.readInt(); + return this; + } + + @Override + public void toNBT(NbtCompound compound) { + compound.putInt("offset", offset); + } + + @Override + public void fromNBT(NbtCompound compound) { + offset = compound.getInt("offset"); + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/container/UScreenHandlers.java b/src/main/java/com/minelittlepony/unicopia/container/UScreenHandlers.java index 42b6faff..07f99b2b 100644 --- a/src/main/java/com/minelittlepony/unicopia/container/UScreenHandlers.java +++ b/src/main/java/com/minelittlepony/unicopia/container/UScreenHandlers.java @@ -2,15 +2,16 @@ package com.minelittlepony.unicopia.container; import com.minelittlepony.unicopia.Unicopia; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType; import net.minecraft.screen.ScreenHandler; import net.minecraft.screen.ScreenHandlerType; import net.minecraft.util.registry.Registry; public interface UScreenHandlers { - ScreenHandlerType SPELL_BOOK = register("spell_book", SpellbookScreenHandler::new); + ScreenHandlerType SPELL_BOOK = register("spell_book", new ExtendedScreenHandlerType<>(SpellbookScreenHandler::new)); - static ScreenHandlerType register(String name, ScreenHandlerType.Factory factory) { - return Registry.register(Registry.SCREEN_HANDLER, Unicopia.id(name), new ScreenHandlerType<>(factory)); + static ScreenHandlerType register(String name, ScreenHandlerType type) { + return Registry.register(Registry.SCREEN_HANDLER, Unicopia.id(name), type); } static void bootstrap() { } diff --git a/src/main/java/com/minelittlepony/unicopia/entity/SpellbookEntity.java b/src/main/java/com/minelittlepony/unicopia/entity/SpellbookEntity.java index 0dc86c9b..11f13e5a 100644 --- a/src/main/java/com/minelittlepony/unicopia/entity/SpellbookEntity.java +++ b/src/main/java/com/minelittlepony/unicopia/entity/SpellbookEntity.java @@ -4,8 +4,12 @@ import org.jetbrains.annotations.Nullable; import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.container.SpellbookScreenHandler; +import com.minelittlepony.unicopia.container.SpellbookState; import com.minelittlepony.unicopia.item.UItems; +import com.minelittlepony.unicopia.network.Channel; +import com.minelittlepony.unicopia.network.MsgSpellbookStateChanged; +import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerFactory; import net.fabricmc.fabric.api.util.TriState; import net.minecraft.entity.Entity; import net.minecraft.entity.EntityType; @@ -15,14 +19,17 @@ import net.minecraft.entity.data.TrackedData; import net.minecraft.entity.data.TrackedDataHandlerRegistry; import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.entity.player.PlayerInventory; import net.minecraft.item.ItemStack; import net.minecraft.nbt.NbtCompound; +import net.minecraft.network.PacketByteBuf; import net.minecraft.particle.ParticleTypes; -import net.minecraft.screen.ScreenHandlerContext; -import net.minecraft.screen.SimpleNamedScreenHandlerFactory; +import net.minecraft.screen.*; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.sound.BlockSoundGroup; import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundEvents; +import net.minecraft.text.Text; import net.minecraft.util.ActionResult; import net.minecraft.util.Hand; import net.minecraft.util.math.Vec3d; @@ -39,10 +46,23 @@ public class SpellbookEntity extends MobEntity { private int activeTicks = TICKS_TO_SLEEP; + private final SpellbookState state = new SpellbookState(); + public SpellbookEntity(EntityType type, World world) { super(type, world); setPersistent(); setAltered(world.random.nextInt(3) == 0); + if (!world.isClient) { + state.setSynchronizer(state -> { + getWorld().getPlayers().forEach(player -> { + if (player instanceof ServerPlayerEntity recipient + && player.currentScreenHandler instanceof SpellbookScreenHandler book + && getUuid().equals(book.entityId)) { + Channel.SERVER_SPELLBOOK_UPDATE.send(recipient, new MsgSpellbookStateChanged<>(player.currentScreenHandler.syncId, state)); + } + }); + }); + } } @Override @@ -54,9 +74,15 @@ public class SpellbookEntity extends MobEntity { dataTracker.startTracking(ALTERED, false); } + public SpellbookState getSpellbookState() { + return state; + } + @Override public ItemStack getPickBlockStack() { - return new ItemStack(UItems.SPELLBOOK); + ItemStack stack = UItems.SPELLBOOK.getDefaultStack(); + stack.getOrCreateNbt().put("spellbookState", state.toNBT()); + return stack; } @Override @@ -180,7 +206,7 @@ public class SpellbookEntity extends MobEntity { world.playSound(getX(), getY(), getZ(), sound.getBreakSound(), SoundCategory.BLOCKS, sound.getVolume(), sound.getPitch(), true); if (world.getGameRules().getBoolean(GameRules.DO_TILE_DROPS)) { - dropItem(UItems.SPELLBOOK, 1); + dropStack(getPickBlockStack(), 1); } } return false; @@ -198,7 +224,22 @@ public class SpellbookEntity extends MobEntity { if (isOpen() && EquinePredicates.PLAYER_UNICORN.test(player)) { setBored(false); - player.openHandledScreen(new SimpleNamedScreenHandlerFactory((syncId, inv, ply) -> new SpellbookScreenHandler(syncId, inv, ScreenHandlerContext.create(world, getBlockPos())), getDisplayName())); + player.openHandledScreen(new ExtendedScreenHandlerFactory() { + @Override + public Text getDisplayName() { + return SpellbookEntity.this.getDisplayName(); + } + + @Override + public ScreenHandler createMenu(int syncId, PlayerInventory inv, PlayerEntity player) { + return new SpellbookScreenHandler(syncId, inv, ScreenHandlerContext.create(world, getBlockPos()), state, getUuid()); + } + + @Override + public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) { + state.toPacket(buf); + } + }); player.playSound(SoundEvents.ITEM_BOOK_PAGE_TURN, 2, 1); return ActionResult.SUCCESS; } @@ -213,6 +254,8 @@ public class SpellbookEntity extends MobEntity { setBored(compound.getBoolean("bored")); setAltered(compound.getBoolean("altered")); setLocked(compound.contains("locked") ? TriState.of(compound.getBoolean("locked")) : TriState.DEFAULT); + + state.fromNBT(compound.getCompound("spellbookState")); } @Override @@ -226,5 +269,7 @@ public class SpellbookEntity extends MobEntity { if (locked != TriState.DEFAULT) { compound.putBoolean("locked", locked.get()); } + + compound.put("spellbookState", state.toNBT()); } } diff --git a/src/main/java/com/minelittlepony/unicopia/item/SpellbookItem.java b/src/main/java/com/minelittlepony/unicopia/item/SpellbookItem.java index 98d256cb..2bbbd644 100644 --- a/src/main/java/com/minelittlepony/unicopia/item/SpellbookItem.java +++ b/src/main/java/com/minelittlepony/unicopia/item/SpellbookItem.java @@ -12,6 +12,7 @@ import net.minecraft.entity.player.PlayerEntity; import net.minecraft.item.BookItem; import net.minecraft.item.ItemStack; import net.minecraft.item.ItemUsageContext; +import net.minecraft.nbt.NbtCompound; import net.minecraft.util.ActionResult; import net.minecraft.util.TypedActionResult; import net.minecraft.util.math.BlockPointer; @@ -31,7 +32,7 @@ public class SpellbookItem extends BookItem implements Dispensable { BlockPos pos = source.getPos().offset(facing); float yaw = facing.getOpposite().asRotation(); - placeBook(source.getWorld(), pos.getX(), pos.getY(), pos.getZ(), yaw); + placeBook(stack, source.getWorld(), pos.getX(), pos.getY(), pos.getZ(), yaw); stack.decrement(1); return new TypedActionResult<>(ActionResult.SUCCESS, stack); @@ -46,7 +47,7 @@ public class SpellbookItem extends BookItem implements Dispensable { if (!context.getWorld().isClient && EquinePredicates.PLAYER_UNICORN.test(player)) { BlockPos pos = context.getBlockPos().offset(context.getSide()); - placeBook(context.getWorld(), pos.getX(), pos.getY(), pos.getZ(), context.getPlayerYaw() + 180); + placeBook(context.getStack(), context.getWorld(), pos.getX(), pos.getY(), pos.getZ(), context.getPlayerYaw() + 180); if (!player.getAbilities().creativeMode) { player.getStackInHand(context.getHand()).decrement(1); @@ -57,12 +58,19 @@ public class SpellbookItem extends BookItem implements Dispensable { return ActionResult.PASS; } - private static void placeBook(World world, int x, int y, int z, float yaw) { + private static void placeBook(ItemStack stack, World world, int x, int y, int z, float yaw) { SpellbookEntity book = UEntities.SPELLBOOK.create(world); book.refreshPositionAndAngles(x + 0.5, y, z + 0.5, 0, 0); book.setHeadYaw(yaw); book.setYaw(yaw); + + @Nullable + NbtCompound tag = stack.getSubNbt("spellbookState"); + if (tag != null) { + book.getSpellbookState().fromNBT(tag); + } + world.spawnEntity(book); } } diff --git a/src/main/java/com/minelittlepony/unicopia/network/Channel.java b/src/main/java/com/minelittlepony/unicopia/network/Channel.java index c225f4a1..3e46e70b 100644 --- a/src/main/java/com/minelittlepony/unicopia/network/Channel.java +++ b/src/main/java/com/minelittlepony/unicopia/network/Channel.java @@ -8,6 +8,8 @@ import com.minelittlepony.unicopia.util.network.C2SPacketType; import com.minelittlepony.unicopia.util.network.SimpleNetworking; import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.server.network.ServerPlayerEntity; import net.minecraft.util.Identifier; public interface Channel { @@ -25,6 +27,9 @@ public interface Channel { Identifier SERVER_SELECT_TRIBE_ID = Unicopia.id("select_tribe"); S2CPacketType SERVER_SELECT_TRIBE = SimpleNetworking.serverToClient(SERVER_SELECT_TRIBE_ID, MsgTribeSelect::new); + S2CPacketType> SERVER_SPELLBOOK_UPDATE = SimpleNetworking.serverToClient(Unicopia.id("server_spellbook_update"), MsgSpellbookStateChanged::new); + C2SPacketType> CLIENT_SPELLBOOK_UPDATE = SimpleNetworking.clientToServer(Unicopia.id("client_spellbook_update"), MsgSpellbookStateChanged::new); + Identifier SERVER_RESOURCES_SEND_ID = Unicopia.id("resources_send"); S2CPacketType SERVER_RESOURCES_SEND = SimpleNetworking.serverToClient(SERVER_RESOURCES_SEND_ID, MsgServerResources::new); diff --git a/src/main/java/com/minelittlepony/unicopia/network/MsgSpellbookStateChanged.java b/src/main/java/com/minelittlepony/unicopia/network/MsgSpellbookStateChanged.java new file mode 100644 index 00000000..0f39e928 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/network/MsgSpellbookStateChanged.java @@ -0,0 +1,52 @@ +package com.minelittlepony.unicopia.network; + +import com.minelittlepony.unicopia.container.SpellbookScreenHandler; +import com.minelittlepony.unicopia.container.SpellbookState; +import com.minelittlepony.unicopia.util.network.Packet; + +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.network.ServerPlayerEntity; + +/** + * Received by the server when a player changes their opened spellbook's state + * Received by the client when another player changes the shared spellbook's state + */ +public class MsgSpellbookStateChanged implements Packet { + + private final int syncId; + private final SpellbookState state; + + public MsgSpellbookStateChanged(int syncId, SpellbookState state) { + this.syncId = syncId; + this.state = state; + } + + public MsgSpellbookStateChanged(PacketByteBuf buffer) { + syncId = buffer.readInt(); + state = new SpellbookState().fromPacket(buffer); + } + + @Override + public void toBuffer(PacketByteBuf buffer) { + buffer.writeInt(syncId); + state.toPacket(buffer); + } + + @Override + public void handle(T sender) { + + if (sender.currentScreenHandler.syncId != syncId) { + return; + } + + System.out.println("Spellbook update page=" + state.getCurrentPageId().get() + " offset=" + state.getState(state.getCurrentPageId().get()).getOffset() + " to " + sender); + + if (sender.currentScreenHandler instanceof SpellbookScreenHandler spellbook) { + spellbook.getSpellbookState().copyFrom(state); + if (sender instanceof ServerPlayerEntity) { + spellbook.getSpellbookState().synchronize(); + } + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/network/datasync/Synchronizable.java b/src/main/java/com/minelittlepony/unicopia/network/datasync/Synchronizable.java new file mode 100644 index 00000000..07834714 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/network/datasync/Synchronizable.java @@ -0,0 +1,20 @@ +package com.minelittlepony.unicopia.network.datasync; + +import java.util.Optional; +import java.util.function.Consumer; + +public abstract class Synchronizable> { + + private Optional> synchronizer = Optional.empty(); + + @SuppressWarnings("unchecked") + public void synchronize() { + synchronizer.ifPresent(s -> s.accept((T)this)); + } + + public void setSynchronizer(Consumer synchronizer) { + this.synchronizer = Optional.of(synchronizer); + } + + public abstract void copyFrom(T state); +} diff --git a/src/main/java/com/minelittlepony/unicopia/util/network/S2CPacketType.java b/src/main/java/com/minelittlepony/unicopia/util/network/S2CPacketType.java index f17ce593..fe8abfd8 100644 --- a/src/main/java/com/minelittlepony/unicopia/util/network/S2CPacketType.java +++ b/src/main/java/com/minelittlepony/unicopia/util/network/S2CPacketType.java @@ -8,7 +8,7 @@ import net.minecraft.util.Identifier; /** * A client packet type. Sent by the server to a specific player. */ -public interface S2CPacketType> { +public interface S2CPacketType> { Identifier getId(); default void send(ServerPlayerEntity recipient, T packet) {