Store the state in the spellbook and synchronize it between players when multiple are viewing the same book

This commit is contained in:
Sollace 2022-09-11 12:22:06 +02:00
parent 0f90593a9a
commit 6e04c6ab6d
16 changed files with 356 additions and 67 deletions

View file

@ -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.DrawableUtil;
import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Content; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Content;
import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Drawable; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.Drawable;
import com.minelittlepony.unicopia.container.SpellbookState;
import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.entity.player.Pony;
import net.minecraft.client.MinecraftClient; import net.minecraft.client.MinecraftClient;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.*; import net.minecraft.util.*;
import net.minecraft.util.math.MathHelper;
public class DynamicContent implements Content { public class DynamicContent implements Content {
private static final Text UNKNOWN = Text.of("???"); private static final Text UNKNOWN = Text.of("???");
private static final Text UNKNOWN_LEVEL = Text.literal("Level: ???").formatted(Formatting.DARK_GREEN); 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<Page> pages = new ArrayList<>(); private final List<Page> pages = new ArrayList<>();
private Bounds bounds = Bounds.empty(); private Bounds bounds = Bounds.empty();
@ -30,7 +30,7 @@ public class DynamicContent implements Content {
@Override @Override
public void draw(MatrixStack matrices, int mouseX, int mouseY, IViewRoot container) { 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)); getPage(pageIndex).ifPresent(page -> page.draw(matrices, mouseX, mouseY, container));
@ -45,7 +45,7 @@ public class DynamicContent implements Content {
@Override @Override
public void copyStateFrom(Content old) { public void copyStateFrom(Content old) {
if (old instanceof DynamicContent o) { if (old instanceof DynamicContent o) {
offset = o.offset; state = o.state;
setBounds(o.bounds); setBounds(o.bounds);
} }
} }
@ -67,10 +67,11 @@ public class DynamicContent implements Content {
} }
@Override @Override
public void init(SpellbookScreen screen) { public void init(SpellbookScreen screen, Identifier pageId) {
state = screen.getState().getState(pageId);
setBounds(screen.getFrameBounds()); setBounds(screen.getFrameBounds());
screen.addPageButtons(187, 30, 350, incr -> { 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));
}); });
} }

View file

@ -1,7 +1,7 @@
package com.minelittlepony.unicopia.client.gui.spellbook; package com.minelittlepony.unicopia.client.gui.spellbook;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.BiConsumer;
import java.util.stream.Stream; import java.util.stream.Stream;
import com.minelittlepony.common.client.gui.IViewRoot; 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 PROFILE_ID = Unicopia.id("profile");
public static final Identifier TRAIT_DEX_ID = Unicopia.id("traits"); public static final Identifier TRAIT_DEX_ID = Unicopia.id("traits");
private final Chapter craftingChapter; private final SpellbookScreen screen;
private Optional<Identifier> currentChapter = Optional.empty(); private final Chapter craftingChapter;
private final Map<Identifier, Chapter> chapters = new HashMap<>(); private final Map<Identifier, Chapter> chapters = new HashMap<>();
public SpellbookChapterList(Chapter craftingChapter, Chapter... builtIn) { public SpellbookChapterList(SpellbookScreen screen, Chapter craftingChapter, Chapter... builtIn) {
this.screen = screen;
this.craftingChapter = craftingChapter; this.craftingChapter = craftingChapter;
SpellbookChapterLoader.INSTANCE.getChapters().forEach(chapter -> { SpellbookChapterLoader.INSTANCE.getChapters().forEach(chapter -> {
chapters.put(chapter.id(), chapter); chapters.put(chapter.id(), chapter);
@ -45,11 +46,8 @@ public class SpellbookChapterList {
chapters.put(chapter.id(), chapter); chapters.put(chapter.id(), chapter);
}); });
} }
return currentChapter.map(chapters::get).orElse(craftingChapter);
}
public void setCurrentChapter(Chapter chapter) { return screen.getState().getCurrentPageId().map(chapters::get).orElse(craftingChapter);
currentChapter = Optional.of(chapter.id());
} }
public record Chapter ( public record Chapter (
@ -70,15 +68,19 @@ public class SpellbookChapterList {
} }
public interface Content extends Drawable { public interface Content extends Drawable {
void init(SpellbookScreen screen); void init(SpellbookScreen screen, Identifier pageId);
default void copyStateFrom(Content old) {} default void copyStateFrom(Content old) {}
static Optional<Content> of(Consumer<SpellbookScreen> init, Drawable obj) { default boolean showInventory() {
return false;
}
static Optional<Content> of(BiConsumer<SpellbookScreen, Identifier> init, Drawable obj) {
return Optional.of(new Content() { return Optional.of(new Content() {
@Override @Override
public void init(SpellbookScreen screen) { public void init(SpellbookScreen screen, Identifier pageId) {
init.accept(screen); init.accept(screen, pageId);
} }
@Override @Override

View file

@ -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.ability.magic.spell.crafting.SpellbookRecipe;
import com.minelittlepony.unicopia.client.gui.DrawableUtil; import com.minelittlepony.unicopia.client.gui.DrawableUtil;
import com.minelittlepony.unicopia.container.SpellbookPage; import com.minelittlepony.unicopia.container.SpellbookPage;
import com.minelittlepony.unicopia.container.SpellbookState;
import com.minelittlepony.unicopia.item.URecipes; import com.minelittlepony.unicopia.item.URecipes;
import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.systems.RenderSystem;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
public class SpellbookCraftingPageContent extends ScrollContainer implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener { public class SpellbookCraftingPageContent extends ScrollContainer implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener {
private final SpellbookScreen screen; private final SpellbookScreen screen;
private SpellbookState.PageState state = new SpellbookState.PageState();
public SpellbookCraftingPageContent(SpellbookScreen screen) { public SpellbookCraftingPageContent(SpellbookScreen screen) {
this.screen = screen; this.screen = screen;
backgroundColor = 0xFFf9efd3; backgroundColor = 0xFFf9efd3;
@ -22,8 +26,11 @@ public class SpellbookCraftingPageContent extends ScrollContainer implements Spe
} }
@Override @Override
public void init(SpellbookScreen screen) { public void init(SpellbookScreen screen, Identifier pageId) {
screen.addPageButtons(187, 300, 350, SpellbookPage::swap); state = screen.getState().getState(pageId);
screen.addPageButtons(187, 300, 350, incr -> {
state.swap(incr, SpellbookPage.VALUES.length);
});
initContents(); initContents();
screen.addDrawable(this); screen.addDrawable(this);
((IViewRoot)screen).getChildElements().add(this); ((IViewRoot)screen).getChildElements().add(this);
@ -34,9 +41,9 @@ public class SpellbookCraftingPageContent extends ScrollContainer implements Spe
int headerColor = mouseY % 255; 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); 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); init(this::initPageContent);
} }
@Override
public boolean showInventory() {
return SpellbookPage.VALUES[state.getOffset()] == SpellbookPage.INVENTORY;
}
private void initPageContent() { private void initPageContent() {
getContentPadding().setVertical(10); getContentPadding().setVertical(10);
getContentPadding().bottom = 30; getContentPadding().bottom = 30;
switch (SpellbookPage.getCurrent()) { switch (SpellbookPage.VALUES[state.getOffset()]) {
case INVENTORY: case INVENTORY:
// handled elsewhere // handled elsewhere
break; break;

View file

@ -14,6 +14,7 @@ import net.minecraft.client.gui.DrawableHelper;
import net.minecraft.client.resource.language.I18n; import net.minecraft.client.resource.language.I18n;
import net.minecraft.client.util.math.MatrixStack; import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
public class SpellbookProfilePageContent extends DrawableHelper implements SpellbookChapterList.Content { public class SpellbookProfilePageContent extends DrawableHelper implements SpellbookChapterList.Content {
@ -28,10 +29,15 @@ public class SpellbookProfilePageContent extends DrawableHelper implements Spell
} }
@Override @Override
public void init(SpellbookScreen screen) { public void init(SpellbookScreen screen, Identifier pageId) {
} }
@Override
public boolean showInventory() {
return true;
}
@Override @Override
public void draw(MatrixStack matrices, int mouseX, int mouseY, IViewRoot container) { public void draw(MatrixStack matrices, int mouseX, int mouseY, IViewRoot container) {

View file

@ -10,9 +10,10 @@ import com.minelittlepony.common.client.gui.element.Button;
import com.minelittlepony.common.client.gui.sprite.TextureSprite; import com.minelittlepony.common.client.gui.sprite.TextureSprite;
import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.Unicopia;
import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookChapterList.*;
import com.minelittlepony.unicopia.container.SpellbookPage; import com.minelittlepony.unicopia.container.*;
import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
import com.minelittlepony.unicopia.container.SpellbookScreenHandler.*; 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.platform.GlStateManager;
import com.mojang.blaze3d.systems.RenderSystem; 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.client.util.math.MatrixStack;
import net.minecraft.entity.player.PlayerInventory; import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.screen.slot.Slot; import net.minecraft.screen.slot.Slot;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.sound.SoundEvents; import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
@ -42,11 +44,12 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
private final RecipeBookWidget recipeBook = new RecipeBookWidget(); 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 craftingChapter;
private final Chapter profileChapter = new Chapter(SpellbookChapterList.PROFILE_ID, TabSide.LEFT, 1, 0, Optional.of(new SpellbookProfilePageContent(this))); private final SpellbookChapterList chapters = new SpellbookChapterList(this,
private final Chapter traitdexChapter = new Chapter(SpellbookChapterList.TRAIT_DEX_ID, TabSide.LEFT, 3, 0, Optional.of(new SpellbookTraitDexPageContent(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))),
private final SpellbookChapterList chapters = new SpellbookChapterList(craftingChapter, profileChapter, traitdexChapter); new Chapter(SpellbookChapterList.TRAIT_DEX_ID, TabSide.LEFT, 3, 0, Optional.of(new SpellbookTraitDexPageContent(this)))
);
private final SpellbookTabBar tabs = new SpellbookTabBar(this, chapters); private final SpellbookTabBar tabs = new SpellbookTabBar(this, chapters);
private Bounds contentBounds = Bounds.empty(); private Bounds contentBounds = Bounds.empty();
@ -59,11 +62,17 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
handler.addSlotShowingCondition(slotType -> { handler.addSlotShowingCondition(slotType -> {
if (slotType == SlotType.INVENTORY) { if (slotType == SlotType.INVENTORY) {
return chapters.getCurrentChapter() == profileChapter return chapters.getCurrentChapter().content().filter(Content::showInventory).isPresent();
|| (chapters.getCurrentChapter() == craftingChapter && SpellbookPage.getCurrent() == SpellbookPage.INVENTORY);
} }
return chapters.getCurrentChapter() == craftingChapter; return chapters.getCurrentChapter() == craftingChapter;
}); });
handler.getSpellbookState().setSynchronizer(state -> {
Channel.CLIENT_SPELLBOOK_UPDATE.send(new MsgSpellbookStateChanged<ServerPlayerEntity>(handler.syncId, state));
});
}
public SpellbookState getState() {
return handler.getSpellbookState();
} }
public void addPageButtons(int buttonY, int prevX, int nextX, IntConsumer pageAction) { public void addPageButtons(int buttonY, int prevX, int nextX, IntConsumer pageAction) {
@ -100,7 +109,7 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
public void init() { public void init() {
super.init(); super.init();
tabs.init(); tabs.init();
chapters.getCurrentChapter().content().ifPresent(content -> content.init(this)); chapters.getCurrentChapter().content().ifPresent(content -> content.init(this, chapters.getCurrentChapter().id()));
} }
@Override @Override
@ -196,9 +205,8 @@ public class SpellbookScreen extends HandledScreen<SpellbookScreenHandler> imple
@Override @Override
public boolean mouseClicked(double mouseX, double mouseY, int button) { public boolean mouseClicked(double mouseX, double mouseY, int button) {
return tabs.getAllTabs().anyMatch(tab -> { return tabs.getAllTabs().anyMatch(tab -> {
if (tab.bounds().contains(mouseX, mouseY) && chapters.getCurrentChapter() != tab.chapter()) { 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); GameGui.playSound(SoundEvents.ITEM_BOOK_PAGE_TURN);
clearAndInit(); clearAndInit();
return true; return true;

View file

@ -10,6 +10,7 @@ import com.minelittlepony.common.client.gui.element.Label;
import com.minelittlepony.common.client.gui.sprite.TextureSprite; import com.minelittlepony.common.client.gui.sprite.TextureSprite;
import com.minelittlepony.unicopia.ability.magic.spell.trait.*; import com.minelittlepony.unicopia.ability.magic.spell.trait.*;
import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookScreen.ImageButton; import com.minelittlepony.unicopia.client.gui.spellbook.SpellbookScreen.ImageButton;
import com.minelittlepony.unicopia.container.SpellbookState;
import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.entity.player.Pony;
import com.mojang.blaze3d.systems.RenderSystem; 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.client.util.math.MatrixStack;
import net.minecraft.item.*; import net.minecraft.item.*;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.Identifier;
import net.minecraft.util.collection.DefaultedList; import net.minecraft.util.collection.DefaultedList;
import net.minecraft.util.math.MathHelper;
public class SpellbookTraitDexPageContent extends DrawableHelper implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener { public class SpellbookTraitDexPageContent extends DrawableHelper implements SpellbookChapterList.Content, SpellbookScreen.RecipesChangedListener {
private final Trait[] traits = Trait.values(); 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 leftPage = new DexPage();
private final DexPage rightPage = new DexPage(); private final DexPage rightPage = new DexPage();
@ -41,12 +42,14 @@ public class SpellbookTraitDexPageContent extends DrawableHelper implements Spel
} }
@Override @Override
public void init(SpellbookScreen screen) { public void init(SpellbookScreen screen, Identifier pageId) {
int page = offset * 2; state = screen.getState().getState(pageId);
int page = state.getOffset() * 2;
leftPage.init(screen, page); leftPage.init(screen, page);
rightPage.init(screen, page + 1); rightPage.init(screen, page + 1);
screen.addPageButtons(187, 30, 350, incr -> { 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()); leftPage.scrollbar.scrollBy(leftPage.scrollbar.getVerticalScrollAmount());
rightPage.scrollbar.scrollBy(rightPage.scrollbar.getVerticalScrollAmount()); rightPage.scrollbar.scrollBy(rightPage.scrollbar.getVerticalScrollAmount());
}); });
@ -54,7 +57,7 @@ public class SpellbookTraitDexPageContent extends DrawableHelper implements Spel
@Override @Override
public void onRecipesChanged() { public void onRecipesChanged() {
init(screen); init(screen, SpellbookChapterList.TRAIT_DEX_ID);
} }
private final class DexPage extends ScrollContainer { private final class DexPage extends ScrollContainer {

View file

@ -1,26 +1,16 @@
package com.minelittlepony.unicopia.container; package com.minelittlepony.unicopia.container;
import net.minecraft.text.Text; import net.minecraft.text.Text;
import net.minecraft.util.math.MathHelper;
public enum SpellbookPage { public enum SpellbookPage {
INVENTORY, INVENTORY,
RECIPES; RECIPES;
public static final SpellbookPage[] VALUES = values(); public static final SpellbookPage[] VALUES = values();
private static int current;
private final Text label = Text.translatable("gui.unicopia.spellbook.page." + name().toLowerCase()); private final Text label = Text.translatable("gui.unicopia.spellbook.page." + name().toLowerCase());
public Text getLabel() { public Text getLabel() {
return label; return label;
} }
public static SpellbookPage getCurrent() {
return VALUES[current];
}
public static void swap(int increment) {
current = MathHelper.clamp(current + increment, 0, VALUES.length - 1);
}
} }

View file

@ -3,6 +3,8 @@ package com.minelittlepony.unicopia.container;
import java.util.*; import java.util.*;
import java.util.function.Predicate; import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.EquinePredicates;
import com.minelittlepony.unicopia.USounds; import com.minelittlepony.unicopia.USounds;
import com.minelittlepony.unicopia.ability.magic.spell.crafting.SpellbookRecipe; 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.CraftingResultInventory;
import net.minecraft.inventory.Inventory; import net.minecraft.inventory.Inventory;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket; import net.minecraft.network.packet.s2c.play.ScreenHandlerSlotUpdateS2CPacket;
import net.minecraft.screen.PlayerScreenHandler; import net.minecraft.screen.PlayerScreenHandler;
import net.minecraft.screen.ScreenHandler; import net.minecraft.screen.ScreenHandler;
@ -59,12 +62,19 @@ public class SpellbookScreenHandler extends ScreenHandler {
private Predicate<SlotType> canShowSlots; private Predicate<SlotType> canShowSlots;
protected SpellbookScreenHandler(int syncId, PlayerInventory inv) { private final SpellbookState state;
this(syncId, inv, ScreenHandlerContext.EMPTY);
@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); super(UScreenHandlers.SPELL_BOOK, syncId);
this.entityId = entityId;
this.state = state;
inventory = inv; inventory = inv;
this.context = context; this.context = context;
@ -136,6 +146,10 @@ public class SpellbookScreenHandler extends ScreenHandler {
onContentChanged(input); onContentChanged(input);
} }
public SpellbookState getSpellbookState() {
return state;
}
public void addSlotShowingCondition(Predicate<SlotType> canShowSlots) { public void addSlotShowingCondition(Predicate<SlotType> canShowSlots) {
this.canShowSlots = canShowSlots; this.canShowSlots = canShowSlots;
} }

View file

@ -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<SpellbookState> implements NbtSerialisable {
public static final SpellbookState INSTANCE = new SpellbookState();
private Optional<Identifier> currentPageId = Optional.empty();
private final Map<Identifier, PageState> states = new HashMap<>();
public Optional<Identifier> 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<PageState> implements NbtSerialisable {
private int offset;
public PageState() {}
PageState(Consumer<PageState> 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");
}
}
}

View file

@ -2,15 +2,16 @@ package com.minelittlepony.unicopia.container;
import com.minelittlepony.unicopia.Unicopia; import com.minelittlepony.unicopia.Unicopia;
import net.fabricmc.fabric.api.screenhandler.v1.ExtendedScreenHandlerType;
import net.minecraft.screen.ScreenHandler; import net.minecraft.screen.ScreenHandler;
import net.minecraft.screen.ScreenHandlerType; import net.minecraft.screen.ScreenHandlerType;
import net.minecraft.util.registry.Registry; import net.minecraft.util.registry.Registry;
public interface UScreenHandlers { public interface UScreenHandlers {
ScreenHandlerType<SpellbookScreenHandler> SPELL_BOOK = register("spell_book", SpellbookScreenHandler::new); ScreenHandlerType<SpellbookScreenHandler> SPELL_BOOK = register("spell_book", new ExtendedScreenHandlerType<>(SpellbookScreenHandler::new));
static <T extends ScreenHandler> ScreenHandlerType<T> register(String name, ScreenHandlerType.Factory<T> factory) { static <T extends ScreenHandler> ScreenHandlerType<T> register(String name, ScreenHandlerType<T> type) {
return Registry.register(Registry.SCREEN_HANDLER, Unicopia.id(name), new ScreenHandlerType<>(factory)); return Registry.register(Registry.SCREEN_HANDLER, Unicopia.id(name), type);
} }
static void bootstrap() { } static void bootstrap() { }

View file

@ -4,8 +4,12 @@ import org.jetbrains.annotations.Nullable;
import com.minelittlepony.unicopia.EquinePredicates; import com.minelittlepony.unicopia.EquinePredicates;
import com.minelittlepony.unicopia.container.SpellbookScreenHandler; import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
import com.minelittlepony.unicopia.container.SpellbookState;
import com.minelittlepony.unicopia.item.UItems; 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.fabricmc.fabric.api.util.TriState;
import net.minecraft.entity.Entity; import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType; 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.data.TrackedDataHandlerRegistry;
import net.minecraft.entity.mob.MobEntity; import net.minecraft.entity.mob.MobEntity;
import net.minecraft.entity.player.PlayerEntity; import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.entity.player.PlayerInventory;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NbtCompound; import net.minecraft.nbt.NbtCompound;
import net.minecraft.network.PacketByteBuf;
import net.minecraft.particle.ParticleTypes; import net.minecraft.particle.ParticleTypes;
import net.minecraft.screen.ScreenHandlerContext; import net.minecraft.screen.*;
import net.minecraft.screen.SimpleNamedScreenHandlerFactory; import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.sound.BlockSoundGroup; import net.minecraft.sound.BlockSoundGroup;
import net.minecraft.sound.SoundCategory; import net.minecraft.sound.SoundCategory;
import net.minecraft.sound.SoundEvents; import net.minecraft.sound.SoundEvents;
import net.minecraft.text.Text;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand; import net.minecraft.util.Hand;
import net.minecraft.util.math.Vec3d; import net.minecraft.util.math.Vec3d;
@ -39,10 +46,23 @@ public class SpellbookEntity extends MobEntity {
private int activeTicks = TICKS_TO_SLEEP; private int activeTicks = TICKS_TO_SLEEP;
private final SpellbookState state = new SpellbookState();
public SpellbookEntity(EntityType<SpellbookEntity> type, World world) { public SpellbookEntity(EntityType<SpellbookEntity> type, World world) {
super(type, world); super(type, world);
setPersistent(); setPersistent();
setAltered(world.random.nextInt(3) == 0); 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 @Override
@ -54,9 +74,15 @@ public class SpellbookEntity extends MobEntity {
dataTracker.startTracking(ALTERED, false); dataTracker.startTracking(ALTERED, false);
} }
public SpellbookState getSpellbookState() {
return state;
}
@Override @Override
public ItemStack getPickBlockStack() { public ItemStack getPickBlockStack() {
return new ItemStack(UItems.SPELLBOOK); ItemStack stack = UItems.SPELLBOOK.getDefaultStack();
stack.getOrCreateNbt().put("spellbookState", state.toNBT());
return stack;
} }
@Override @Override
@ -180,7 +206,7 @@ public class SpellbookEntity extends MobEntity {
world.playSound(getX(), getY(), getZ(), sound.getBreakSound(), SoundCategory.BLOCKS, sound.getVolume(), sound.getPitch(), true); world.playSound(getX(), getY(), getZ(), sound.getBreakSound(), SoundCategory.BLOCKS, sound.getVolume(), sound.getPitch(), true);
if (world.getGameRules().getBoolean(GameRules.DO_TILE_DROPS)) { if (world.getGameRules().getBoolean(GameRules.DO_TILE_DROPS)) {
dropItem(UItems.SPELLBOOK, 1); dropStack(getPickBlockStack(), 1);
} }
} }
return false; return false;
@ -198,7 +224,22 @@ public class SpellbookEntity extends MobEntity {
if (isOpen() && EquinePredicates.PLAYER_UNICORN.test(player)) { if (isOpen() && EquinePredicates.PLAYER_UNICORN.test(player)) {
setBored(false); 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); player.playSound(SoundEvents.ITEM_BOOK_PAGE_TURN, 2, 1);
return ActionResult.SUCCESS; return ActionResult.SUCCESS;
} }
@ -213,6 +254,8 @@ public class SpellbookEntity extends MobEntity {
setBored(compound.getBoolean("bored")); setBored(compound.getBoolean("bored"));
setAltered(compound.getBoolean("altered")); setAltered(compound.getBoolean("altered"));
setLocked(compound.contains("locked") ? TriState.of(compound.getBoolean("locked")) : TriState.DEFAULT); setLocked(compound.contains("locked") ? TriState.of(compound.getBoolean("locked")) : TriState.DEFAULT);
state.fromNBT(compound.getCompound("spellbookState"));
} }
@Override @Override
@ -226,5 +269,7 @@ public class SpellbookEntity extends MobEntity {
if (locked != TriState.DEFAULT) { if (locked != TriState.DEFAULT) {
compound.putBoolean("locked", locked.get()); compound.putBoolean("locked", locked.get());
} }
compound.put("spellbookState", state.toNBT());
} }
} }

View file

@ -12,6 +12,7 @@ import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.item.BookItem; import net.minecraft.item.BookItem;
import net.minecraft.item.ItemStack; import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemUsageContext; import net.minecraft.item.ItemUsageContext;
import net.minecraft.nbt.NbtCompound;
import net.minecraft.util.ActionResult; import net.minecraft.util.ActionResult;
import net.minecraft.util.TypedActionResult; import net.minecraft.util.TypedActionResult;
import net.minecraft.util.math.BlockPointer; import net.minecraft.util.math.BlockPointer;
@ -31,7 +32,7 @@ public class SpellbookItem extends BookItem implements Dispensable {
BlockPos pos = source.getPos().offset(facing); BlockPos pos = source.getPos().offset(facing);
float yaw = facing.getOpposite().asRotation(); 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); stack.decrement(1);
return new TypedActionResult<>(ActionResult.SUCCESS, stack); 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)) { if (!context.getWorld().isClient && EquinePredicates.PLAYER_UNICORN.test(player)) {
BlockPos pos = context.getBlockPos().offset(context.getSide()); 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) { if (!player.getAbilities().creativeMode) {
player.getStackInHand(context.getHand()).decrement(1); player.getStackInHand(context.getHand()).decrement(1);
@ -57,12 +58,19 @@ public class SpellbookItem extends BookItem implements Dispensable {
return ActionResult.PASS; 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); SpellbookEntity book = UEntities.SPELLBOOK.create(world);
book.refreshPositionAndAngles(x + 0.5, y, z + 0.5, 0, 0); book.refreshPositionAndAngles(x + 0.5, y, z + 0.5, 0, 0);
book.setHeadYaw(yaw); book.setHeadYaw(yaw);
book.setYaw(yaw); book.setYaw(yaw);
@Nullable
NbtCompound tag = stack.getSubNbt("spellbookState");
if (tag != null) {
book.getSpellbookState().fromNBT(tag);
}
world.spawnEntity(book); world.spawnEntity(book);
} }
} }

View file

@ -8,6 +8,8 @@ import com.minelittlepony.unicopia.util.network.C2SPacketType;
import com.minelittlepony.unicopia.util.network.SimpleNetworking; import com.minelittlepony.unicopia.util.network.SimpleNetworking;
import net.fabricmc.fabric.api.networking.v1.ServerPlayConnectionEvents; 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; import net.minecraft.util.Identifier;
public interface Channel { public interface Channel {
@ -25,6 +27,9 @@ public interface Channel {
Identifier SERVER_SELECT_TRIBE_ID = Unicopia.id("select_tribe"); Identifier SERVER_SELECT_TRIBE_ID = Unicopia.id("select_tribe");
S2CPacketType<MsgTribeSelect> SERVER_SELECT_TRIBE = SimpleNetworking.serverToClient(SERVER_SELECT_TRIBE_ID, MsgTribeSelect::new); S2CPacketType<MsgTribeSelect> SERVER_SELECT_TRIBE = SimpleNetworking.serverToClient(SERVER_SELECT_TRIBE_ID, MsgTribeSelect::new);
S2CPacketType<MsgSpellbookStateChanged<PlayerEntity>> SERVER_SPELLBOOK_UPDATE = SimpleNetworking.serverToClient(Unicopia.id("server_spellbook_update"), MsgSpellbookStateChanged::new);
C2SPacketType<MsgSpellbookStateChanged<ServerPlayerEntity>> CLIENT_SPELLBOOK_UPDATE = SimpleNetworking.clientToServer(Unicopia.id("client_spellbook_update"), MsgSpellbookStateChanged::new);
Identifier SERVER_RESOURCES_SEND_ID = Unicopia.id("resources_send"); Identifier SERVER_RESOURCES_SEND_ID = Unicopia.id("resources_send");
S2CPacketType<MsgServerResources> SERVER_RESOURCES_SEND = SimpleNetworking.serverToClient(SERVER_RESOURCES_SEND_ID, MsgServerResources::new); S2CPacketType<MsgServerResources> SERVER_RESOURCES_SEND = SimpleNetworking.serverToClient(SERVER_RESOURCES_SEND_ID, MsgServerResources::new);

View file

@ -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<T extends PlayerEntity> implements Packet<T> {
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();
}
}
}
}

View file

@ -0,0 +1,20 @@
package com.minelittlepony.unicopia.network.datasync;
import java.util.Optional;
import java.util.function.Consumer;
public abstract class Synchronizable<T extends Synchronizable<T>> {
private Optional<Consumer<T>> synchronizer = Optional.empty();
@SuppressWarnings("unchecked")
public void synchronize() {
synchronizer.ifPresent(s -> s.accept((T)this));
}
public void setSynchronizer(Consumer<T> synchronizer) {
this.synchronizer = Optional.of(synchronizer);
}
public abstract void copyFrom(T state);
}

View file

@ -8,7 +8,7 @@ import net.minecraft.util.Identifier;
/** /**
* A client packet type. Sent by the server to a specific player. * A client packet type. Sent by the server to a specific player.
*/ */
public interface S2CPacketType<T extends Packet<PlayerEntity>> { public interface S2CPacketType<T extends Packet<? extends PlayerEntity>> {
Identifier getId(); Identifier getId();
default void send(ServerPlayerEntity recipient, T packet) { default void send(ServerPlayerEntity recipient, T packet) {