diff --git a/common/src/main/java/earth/terrarium/heracles/api/groups/Group.java b/common/src/main/java/earth/terrarium/heracles/api/groups/Group.java new file mode 100644 index 00000000..38cc4606 --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/api/groups/Group.java @@ -0,0 +1,46 @@ +package earth.terrarium.heracles.api.groups; + +import com.mojang.serialization.Codec; +import com.mojang.serialization.codecs.RecordCodecBuilder; +import earth.terrarium.heracles.api.quests.QuestIcon; +import earth.terrarium.heracles.api.quests.QuestIcons; + +import java.util.Optional; + +public record Group( + Optional> icon, + String title, + String description +) { + + public static final Codec CODEC = RecordCodecBuilder.create(instance -> instance.group( + QuestIcons.CODEC.optionalFieldOf("icon").forGetter(Group::icon), + Codec.STRING.fieldOf("title").forGetter(Group::title), + Codec.STRING.fieldOf("description").forGetter(Group::description) + ).apply(instance, Group::new)); + + public Group(String id) { + this(Optional.empty(), id, ""); + } + + public Group withIcon(QuestIcon icon) { + return new Group(Optional.of(icon), this.title, this.description); + } + + public Group withTitle(String title) { + return new Group(this.icon, title, this.description); + } + + public Group withDescription(String description) { + return new Group(this.icon, this.title, description); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public Group edit(Optional> icon, Optional title, Optional description) { + return new Group( + icon.or(() -> this.icon), + title.orElse(this.title), + description.orElse(this.description) + ); + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java b/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java index 210a52ee..587741af 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java +++ b/common/src/main/java/earth/terrarium/heracles/client/HeraclesClient.java @@ -48,7 +48,7 @@ public static void clientTick() { } public static void openQuestScreen() { - if (!ClientQuests.groups().contains(lastGroup)) { + if (!ClientQuests.groups().containsKey(lastGroup)) { lastGroup = ""; } if (DisplayConfig.showTutorial) { diff --git a/common/src/main/java/earth/terrarium/heracles/client/handlers/ClientQuests.java b/common/src/main/java/earth/terrarium/heracles/client/handlers/ClientQuests.java index 6ad50170..8e7c9f18 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/handlers/ClientQuests.java +++ b/common/src/main/java/earth/terrarium/heracles/client/handlers/ClientQuests.java @@ -1,5 +1,6 @@ package earth.terrarium.heracles.client.handlers; +import earth.terrarium.heracles.api.groups.Group; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.common.handlers.progress.QuestProgress; import earth.terrarium.heracles.common.menus.quests.QuestsContent; @@ -15,7 +16,8 @@ public class ClientQuests { private static final Map ENTRIES = new HashMap<>(); private static final Map STATUS = new HashMap<>(); private static final Map> BY_GROUPS = new HashMap<>(); - private static final List GROUPS = new ArrayList<>(); + private static final Map GROUPS = new LinkedHashMap<>(); + private static final List GROUP_ORDERS = new ArrayList<>(); private static final Map PROGRESS = new HashMap<>(); @@ -23,11 +25,11 @@ public static Optional get(String key) { return Optional.ofNullable(ENTRIES.get(key)); } - public static List groups() { + public static Map groups() { return GROUPS; } - public static void sync(Map quests, List groups) { + public static void sync(Map quests, Map groups) { ENTRIES.clear(); BY_GROUPS.clear(); GROUPS.clear(); @@ -36,7 +38,8 @@ public static void sync(Map quests, List groups) { addEntry(entry.getKey(), entry.getValue(), quests); } - GROUPS.addAll(groups); + updateGroupsWithOrder(groups); + for (QuestEntry value : ENTRIES.values()) { for (String s : value.value.display().groups().keySet()) { BY_GROUPS.computeIfAbsent(s, k -> new ArrayList<>()).add(value); @@ -44,6 +47,26 @@ public static void sync(Map quests, List groups) { } } + public static void syncGroupOrders(List groupOrders) { + GROUP_ORDERS.clear(); + GROUP_ORDERS.addAll(groupOrders); + updateGroupsWithOrder(new LinkedHashMap<>(GROUPS)); + } + + public static void updateGroupsWithOrder(Map groups) { + GROUPS.clear(); + for (String id : ClientQuests.GROUP_ORDERS) { + Group group = groups.get(id); + if (group == null) continue; + GROUPS.put(id, group); + } + + for (var entry : groups.entrySet()) { + if (GROUPS.containsKey(entry.getKey())) continue; + GROUPS.put(entry.getKey(), entry.getValue()); + } + } + public static void syncDescriptions(Map descriptions) { for (Map.Entry entry : descriptions.entrySet()) { get(entry.getKey()) @@ -126,6 +149,10 @@ public static Collection entries() { return ENTRIES.values(); } + public static List groupOrders() { + return GROUP_ORDERS; + } + public static QuestProgress getProgress(String id) { return PROGRESS.get(id); } diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/AbstractQuestScreen.java b/common/src/main/java/earth/terrarium/heracles/client/screens/AbstractQuestScreen.java index a979baf0..5d9272f8 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/AbstractQuestScreen.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/AbstractQuestScreen.java @@ -1,7 +1,7 @@ package earth.terrarium.heracles.client.screens; import com.mojang.blaze3d.systems.RenderSystem; -import com.teamresourceful.resourcefullib.client.screens.BaseCursorScreen; +import com.teamresourceful.resourcefullib.client.screens.PriorityScreen; import earth.terrarium.heracles.Heracles; import earth.terrarium.heracles.api.client.theme.QuestsScreenTheme; import earth.terrarium.heracles.client.utils.ClientUtils; @@ -23,7 +23,7 @@ import java.util.ArrayList; import java.util.List; -public abstract class AbstractQuestScreen extends BaseCursorScreen { +public abstract class AbstractQuestScreen extends PriorityScreen { public static final ResourceLocation HEADING = new ResourceLocation(Heracles.MOD_ID, "textures/gui/heading.png"); diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsContextMenu.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsContextMenu.java new file mode 100644 index 00000000..91e40912 --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsContextMenu.java @@ -0,0 +1,14 @@ +package earth.terrarium.heracles.client.screens.quests; + +import com.teamresourceful.resourcefullib.client.components.context.ContextMenu; + +public class GroupsContextMenu extends ContextMenu { + + @Override + public boolean mouseClicked(double mouseX, double mouseY, int button) { + if (this.visible && this.active) { + return super.mouseClicked(mouseX, mouseY, button); + } + return false; + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsList.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsList.java index ee7f99ee..0e5e0aeb 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsList.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/GroupsList.java @@ -1,6 +1,9 @@ package earth.terrarium.heracles.client.screens.quests; +import com.mojang.blaze3d.platform.InputConstants; import com.mojang.blaze3d.systems.RenderSystem; +import com.teamresourceful.resourcefullib.client.components.context.ContextMenu; +import com.teamresourceful.resourcefullib.client.components.context.ContextualMenuScreen; import com.teamresourceful.resourcefullib.client.components.selection.ListEntry; import com.teamresourceful.resourcefullib.client.components.selection.SelectionList; import com.teamresourceful.resourcefullib.client.scissor.ScissorBoxStack; @@ -8,28 +11,40 @@ import com.teamresourceful.resourcefullib.client.utils.CursorUtils; import com.teamresourceful.resourcefullib.client.utils.ScreenUtils; import earth.terrarium.heracles.api.client.theme.QuestsScreenTheme; +import earth.terrarium.heracles.api.groups.Group; +import earth.terrarium.heracles.api.quests.QuestIcon; +import earth.terrarium.heracles.api.quests.defaults.ItemQuestIcon; import earth.terrarium.heracles.client.handlers.ClientQuests; import earth.terrarium.heracles.client.screens.AbstractQuestScreen; +import earth.terrarium.heracles.client.utils.ClientUtils; +import earth.terrarium.heracles.client.utils.MouseClick; import earth.terrarium.heracles.common.constants.ConstantComponents; import earth.terrarium.heracles.common.network.NetworkHandler; import earth.terrarium.heracles.common.network.packets.groups.DeleteGroupPacket; +import earth.terrarium.heracles.common.network.packets.groups.EditGroupPacket; +import earth.terrarium.heracles.common.network.packets.quests.ServerboundUpdateGroupOrderPacket; +import earth.terrarium.heracles.common.utils.ItemValue; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.resources.sounds.SimpleSoundInstance; +import net.minecraft.network.chat.Component; import net.minecraft.sounds.SoundEvents; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.util.ArrayList; -import java.util.List; +import java.util.*; import java.util.function.Consumer; public class GroupsList extends SelectionList { private final int width; + private final Consumer<@Nullable Entry> onSelection; + private final int x; public GroupsList(int x, int y, int width, int height, Consumer<@Nullable Entry> onSelection) { - super(x, y, width, height, 20, onSelection, true); + super(x, y, width, height, 20, entry -> {}, true); + this.x = x; + this.onSelection = onSelection; this.width = width; } @@ -44,12 +59,12 @@ private void internalSetSelected(@Nullable Entry entry) { super.setSelected(entry); } - public void update(List groups, String selected) { + public void update(Map groups, String selected) { List entries = new ArrayList<>(groups.size()); Entry selectedEntry = null; - for (String group : groups) { - Entry entry = new Entry(this, group); - if (group.equals(selected)) { + for (var group : groups.entrySet()) { + Entry entry = new Entry(this, group.getKey(), group.getValue()); + if (group.getKey().equals(selected)) { selectedEntry = entry; } entries.add(entry); @@ -60,18 +75,24 @@ public void update(List groups, String selected) { } } - public void addGroup(String group) { - addEntry(new Entry(this, group)); + public void updateOrder() { + update(ClientQuests.groups(), getSelected() == null ? null : getSelected().name()); + } + + public void addGroup(String id, Group group) { + addEntry(new Entry(this, id, group)); } public static class Entry extends ListEntry { private final GroupsList list; private final String name; + private Group group; - public Entry(GroupsList list, String name) { + public Entry(GroupsList list, String name, Group group) { this.list = list; this.name = name; + this.group = group; } @Override @@ -82,7 +103,13 @@ protected void render(@NotNull GuiGraphics graphics, @NotNull ScissorBoxStack sc graphics.blitNineSliced(AbstractQuestScreen.HEADING, left, top, width, height, 5, 64, 20, 192, 55); } RenderSystem.disableBlend(); - graphics.drawCenteredString(Minecraft.getInstance().font, name, left + width / 2, top + height / 2 - 4, QuestsScreenTheme.getGroupName()); + int x = left + 5; + if (group.icon().isPresent()) { + int size = height - 2; + group.icon().get().render(graphics, scissorStack, x, top + 1 + ((size - 16) / 2), size, size); + } + x += height; + graphics.drawString(Minecraft.getInstance().font, Component.translatable(group.title()), x, top + height / 2 - 4, QuestsScreenTheme.getGroupName()); CursorUtils.setCursor(hovered, CursorScreen.Cursor.POINTER); if (Minecraft.getInstance().screen instanceof QuestsEditScreen) { if (mouseX - left >= width - 11 && mouseX - left <= width - 2 && mouseY - top >= 2 && mouseY - top <= 12 && hovered) { @@ -106,9 +133,10 @@ protected void render(@NotNull GuiGraphics graphics, @NotNull ScissorBoxStack sc @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - if (this.list.getSelected() != this) Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); + if (this.list.getSelected() != this) + Minecraft.getInstance().getSoundManager().play(SimpleSoundInstance.forUI(SoundEvents.UI_BUTTON_CLICK, 1.0F)); boolean cant = !ClientQuests.byGroup(name).isEmpty() || this.list.children().size() == 1; - if (Minecraft.getInstance().screen instanceof QuestsEditScreen screen && button == 0 && !cant) { + if (Minecraft.getInstance().screen instanceof QuestsEditScreen screen && button == InputConstants.MOUSE_BUTTON_LEFT && !cant) { boolean closingButton = mouseX >= this.list.width - 11 && mouseX <= this.list.width - 2 && mouseY >= 2 && mouseY <= 12; if (closingButton) { screen.confirmModal().setVisible(true); @@ -129,6 +157,62 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { return true; } } + if (button == InputConstants.MOUSE_BUTTON_RIGHT) { + MouseClick mouse = ClientUtils.getMousePos(); + Optional menu = ContextualMenuScreen.getMenu(); + if (menu.isPresent()) { + menu.get() + .start(this.list.x + this.list.width + 6, mouse.y()) + .addOption(Component.literal("\uD83D\uDCAC Edit Name"), () -> { + if (Minecraft.getInstance().screen instanceof QuestsEditScreen screen) { + screen.textModal().setVisible(true); + screen.textModal().setText(this.group.title()); + screen.textModal().setCallback((unused, text) -> { + this.group = this.group.withTitle(text); + NetworkHandler.CHANNEL.sendToServer(EditGroupPacket.ofTitle(name, text)); + screen.textModal().setVisible(false); + }); + } + }) + .addOption(Component.literal("\uD83D\uDDBC Edit Icon"), () -> { + if (Minecraft.getInstance().screen instanceof QuestsEditScreen screen) { + screen.itemModal().setVisible(true); + screen.itemModal().setCallback(item -> { + QuestIcon icon = new ItemQuestIcon(new ItemValue(item)); + this.group = this.group.withIcon(icon); + NetworkHandler.CHANNEL.sendToServer(EditGroupPacket.ofIcon(name, icon)); + screen.itemModal().setVisible(false); + }); + } + }) + .addDivider() + .addOption(Component.literal("⬆ Move Up"), () -> { + int currentIndex = ClientQuests.groupOrders().indexOf(name); + if (currentIndex > 0) { + String previous = ClientQuests.groupOrders().get(currentIndex - 1); + ClientQuests.groupOrders().set(currentIndex - 1, name); + ClientQuests.groupOrders().set(currentIndex, previous); + ClientQuests.updateGroupsWithOrder(new LinkedHashMap<>(ClientQuests.groups())); + this.list.updateOrder(); + NetworkHandler.CHANNEL.sendToServer(new ServerboundUpdateGroupOrderPacket(ClientQuests.groupOrders())); + } + }) + .addOption(Component.literal("⬇ Move Down"), () -> { + int currentIndex = ClientQuests.groupOrders().indexOf(name); + if (currentIndex < ClientQuests.groupOrders().size() - 1) { + String next = ClientQuests.groupOrders().get(currentIndex + 1); + ClientQuests.groupOrders().set(currentIndex + 1, name); + ClientQuests.groupOrders().set(currentIndex, next); + ClientQuests.updateGroupsWithOrder(new LinkedHashMap<>(ClientQuests.groups())); + this.list.updateOrder(); + NetworkHandler.CHANNEL.sendToServer(new ServerboundUpdateGroupOrderPacket(ClientQuests.groupOrders())); + } + }) + .open(); + return true; + } + } + this.list.onSelection.accept(this); return super.mouseClicked(mouseX, mouseY, button); } diff --git a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestsEditScreen.java b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestsEditScreen.java index 45b2a048..323ff2f6 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestsEditScreen.java +++ b/common/src/main/java/earth/terrarium/heracles/client/screens/quests/QuestsEditScreen.java @@ -1,6 +1,9 @@ package earth.terrarium.heracles.client.screens.quests; import com.mojang.blaze3d.platform.InputConstants; +import com.teamresourceful.resourcefullib.client.components.context.ContextualMenuScreen; +import com.teamresourceful.resourcefullib.client.components.context.ContextMenu; +import earth.terrarium.heracles.api.groups.Group; import earth.terrarium.heracles.api.quests.GroupDisplay; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.api.quests.QuestDisplay; @@ -33,7 +36,7 @@ import java.util.HashSet; import java.util.Locale; -public class QuestsEditScreen extends QuestsScreen { +public class QuestsEditScreen extends QuestsScreen implements ContextualMenuScreen { private SelectableImageButton moveTool; private SelectableImageButton dragTool; @@ -46,6 +49,9 @@ public class QuestsEditScreen extends QuestsScreen { private ItemModal itemModal; private AddDependencyModal dependencyModal; private TextInputModal questModal; + private TextInputModal textModal; + + private ContextMenu contextMenu; public QuestsEditScreen(QuestsContent content) { super(content); @@ -95,12 +101,12 @@ protected void init() { this.uploadModal = addTemporary(new UploadModal(this.width, this.height)); this.groupModal = addTemporary(new TextInputModal<>(this.width, this.height, ConstantComponents.Groups.CREATE, (ignored, text) -> { - NetworkHandler.CHANNEL.sendToServer(new CreateGroupPacket(text)); - ClientQuests.groups().add(text); + NetworkHandler.CHANNEL.sendToServer(new CreateGroupPacket(text.trim())); + ClientQuests.groups().put(text.trim(), new Group(text.trim())); if (Minecraft.getInstance().screen instanceof QuestsScreen screen) { - screen.getGroupsList().addGroup(text); + screen.getGroupsList().addGroup(text.trim(), new Group(text.trim())); } - }, text -> !ClientQuests.groups().contains(text.trim()))); + }, text -> !ClientQuests.groups().containsKey(text.trim()))); this.iconBackgroundModal = addTemporary(new IconBackgroundModal(this.width, this.height)); this.itemModal = addTemporary(new ItemModal(this.width, this.height)); this.dependencyModal = addTemporary(new AddDependencyModal(this.width, this.height)); @@ -120,6 +126,15 @@ protected void init() { ); this.questsWidget.addQuest(ClientQuestNetworking.add(text, quest)); }, text -> text.toLowerCase(Locale.ROOT).replaceAll("[^a-zA-Z_-]", "").length() >= 2 && ClientQuests.get(text.trim()).isEmpty())); + + this.textModal = addTemporary(new TextInputModal<>(this.width, this.height, + Component.literal("Enter Name"), + (unit, text) -> {}, + text -> text.length() >= 2 + )); + this.textModal.setData(Unit.INSTANCE); + + this.contextMenu = addRenderableWidget(-1, new GroupsContextMenu()); } @Override @@ -213,4 +228,13 @@ public AddDependencyModal dependencyModal() { public TextInputModal questModal() { return this.questModal; } + + public TextInputModal textModal() { + return this.textModal; + } + + @Override + public ContextMenu getContextMenu() { + return this.contextMenu; + } } \ No newline at end of file diff --git a/common/src/main/java/earth/terrarium/heracles/client/utils/ClientUtils.java b/common/src/main/java/earth/terrarium/heracles/client/utils/ClientUtils.java index f0445d30..2293c524 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/utils/ClientUtils.java +++ b/common/src/main/java/earth/terrarium/heracles/client/utils/ClientUtils.java @@ -3,8 +3,6 @@ import com.mojang.blaze3d.platform.Window; import com.mojang.blaze3d.systems.RenderSystem; import com.mojang.blaze3d.vertex.*; -import it.unimi.dsi.fastutil.chars.Char2CharMap; -import it.unimi.dsi.fastutil.chars.Char2CharOpenHashMap; import net.minecraft.client.Minecraft; import net.minecraft.client.MouseHandler; import net.minecraft.client.gui.GuiGraphics; @@ -35,30 +33,6 @@ public static MouseClick getMousePos() { return new MouseClick((int) mouseX, (int) mouseY, -1); } - private static final Char2CharMap SMALL_NUMBERS = new Char2CharOpenHashMap(); - - static { - SMALL_NUMBERS.put('0', '₀'); - SMALL_NUMBERS.put('1', '₁'); - SMALL_NUMBERS.put('2', '₂'); - SMALL_NUMBERS.put('3', '₃'); - SMALL_NUMBERS.put('4', '₄'); - SMALL_NUMBERS.put('5', '₅'); - SMALL_NUMBERS.put('6', '₆'); - SMALL_NUMBERS.put('7', '₇'); - SMALL_NUMBERS.put('8', '₈'); - SMALL_NUMBERS.put('9', '₉'); - } - - public static String getSmallNumber(int num) { - String normal = String.valueOf(num); - StringBuilder builder = new StringBuilder(normal.length()); - for (char c : String.valueOf(num).toCharArray()) { - builder.append(SMALL_NUMBERS.getOrDefault(c, c)); - } - return builder.toString(); - } - public static void blitTiling(GuiGraphics graphics, ResourceLocation atlasLocation, int targetX, int targetY, int targetWidth, int targetHeight, int sourceX, int sourceY, int sourceWidth, int sourceHeight) { try (BlitBatcher batcher = new BlitBatcher(graphics, atlasLocation)) { int xOffset = 0; diff --git a/common/src/main/java/earth/terrarium/heracles/client/widgets/modals/TextInputModal.java b/common/src/main/java/earth/terrarium/heracles/client/widgets/modals/TextInputModal.java index 0887093f..84bdb8fa 100644 --- a/common/src/main/java/earth/terrarium/heracles/client/widgets/modals/TextInputModal.java +++ b/common/src/main/java/earth/terrarium/heracles/client/widgets/modals/TextInputModal.java @@ -24,6 +24,7 @@ public class TextInputModal extends BaseModal { private static final int HEIGHT = 57; private final Component title; + private final EnterableEditBox editBox; private T data; private BiConsumer callback; @@ -32,7 +33,7 @@ public TextInputModal(int screenWidth, int screenHeight, Component title, BiCons super(screenWidth, screenHeight, WIDTH, HEIGHT); this.title = title; this.callback = callback; - var editBox = addChild(new EnterableEditBox(Minecraft.getInstance().font, this.x + 8, this.y + 19, 152, 14, Component.nullToEmpty("Group Name"))); + editBox = addChild(new EnterableEditBox(Minecraft.getInstance().font, this.x + 8, this.y + 19, 152, 14, Component.nullToEmpty("Group Name"))); editBox.setEnter(value -> { if (!value.isBlank()) { this.callback.accept(this.data, value); @@ -93,6 +94,10 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { return true; } + public void setText(String text) { + editBox.setValue(text); + } + public void setData(T data) { this.data = data; } diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/QuestHandler.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/QuestHandler.java index 5bb22591..de82e496 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/QuestHandler.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/quests/QuestHandler.java @@ -2,15 +2,21 @@ import com.google.common.collect.HashBiMap; import com.google.common.collect.Sets; +import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import com.mojang.serialization.Codec; import com.mojang.serialization.JsonOps; import com.teamresourceful.resourcefullib.common.lib.Constants; import com.teamresourceful.resourcefullib.common.utils.FileUtils; import com.teamresourceful.resourcefullib.common.utils.Scheduling; import earth.terrarium.heracles.Heracles; +import static earth.terrarium.heracles.Heracles.LOGGER; + +import earth.terrarium.heracles.api.groups.Group; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.common.utils.ModUtils; +import net.minecraft.Util; import net.minecraft.core.RegistryAccess; import net.minecraft.resources.RegistryOps; @@ -28,7 +34,8 @@ public class QuestHandler { private static final Map QUESTS = HashBiMap.create(); private static final Set QUEST_KEYS = Sets.newConcurrentHashSet(); - private static final List GROUPS = new ArrayList<>(); + private static final Map GROUPS = new LinkedHashMap<>(); + private static final List GROUPS_ORDERS = new ArrayList<>(); private static Path lastPath; private static final Map> SAVING_FUTURES = new HashMap<>(); @@ -58,7 +65,7 @@ public static void load(RegistryAccess access, Path path) { for (Quest value : QUESTS.values()) { value.dependencies().removeIf(Predicate.not(QUESTS::containsKey)); } - loadGroups(heraclesPath.resolve("groups.txt").toFile()); + loadGroups(heraclesPath); } private static void load(RegistryAccess access, Reader reader, String id, Map quests) { @@ -72,22 +79,49 @@ private static void load(RegistryAccess access, Reader reader, String id, Map groups = Codec.unboundedMap(Codec.STRING, Group.CODEC) + .parse(JsonOps.INSTANCE, element.get("groups")) + .getOrThrow(false, LOGGER::error); + GROUPS.putAll(groups); + element.getAsJsonArray("order").forEach(e -> GROUPS_ORDERS.add(e.getAsString())); + } catch (Exception e) { + LOGGER.error("Failed to load quest groups", e); + } + } else if (txt.exists()) { + try { + for (String group : org.apache.commons.io.FileUtils.readLines(txt, StandardCharsets.UTF_8)) { + GROUPS.put(group, new Group(group)); + } + Files.deleteIfExists(txt.toPath()); + save = true; } catch (Exception e) { Heracles.LOGGER.error("Failed to load quest groups", e); } } + for (Quest value : QUESTS.values()) { for (String s : value.display().groups().keySet()) { - if (!GROUPS.contains(s)) { - GROUPS.add(s); + if (!GROUPS.containsKey(s)) { + GROUPS.put(s, new Group(s)); + if (!GROUPS_ORDERS.contains(s)) { + GROUPS_ORDERS.add(s); + } } } } + if (save) { + saveGroups(); + } } public static void markDirty(String id) { @@ -138,8 +172,14 @@ public static void saveGroups() { return; } try { - File file = new File(lastPath.toFile(), "groups.txt"); - org.apache.commons.io.FileUtils.writeLines(file, GROUPS); + File file = new File(lastPath.toFile(), "groups.json"); + JsonObject element = new JsonObject(); + JsonElement json = Codec.unboundedMap(Codec.STRING, Group.CODEC) + .encodeStart(JsonOps.INSTANCE, GROUPS) + .getOrThrow(false, LOGGER::error); + element.add("groups", json); + element.add("order", Util.make(new JsonArray(), array -> GROUPS_ORDERS.forEach(array::add))); + org.apache.commons.io.FileUtils.write(file, Constants.PRETTY_GSON.toJson(element), StandardCharsets.UTF_8); } catch (Exception e) { Heracles.LOGGER.error("Failed to save quest groups", e); } @@ -190,11 +230,16 @@ public static Map quests() { return QUESTS; } - public static List groups() { + public static Map groups() { if (GROUPS.isEmpty()) { - GROUPS.add("Main"); + GROUPS.put("main", new Group("Main")); + GROUPS_ORDERS.add("main"); saveGroups(); } return GROUPS; } + + public static List groupsOrder() { + return GROUPS_ORDERS; + } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/handlers/syncing/QuestSyncer.java b/common/src/main/java/earth/terrarium/heracles/common/handlers/syncing/QuestSyncer.java index 48667c43..2264b960 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/handlers/syncing/QuestSyncer.java +++ b/common/src/main/java/earth/terrarium/heracles/common/handlers/syncing/QuestSyncer.java @@ -8,6 +8,7 @@ import earth.terrarium.heracles.common.handlers.quests.QuestHandler; import earth.terrarium.heracles.common.network.NetworkHandler; import earth.terrarium.heracles.common.network.packets.quests.SyncDescriptionsPacket; +import earth.terrarium.heracles.common.network.packets.quests.SyncGroupOrderPacket; import earth.terrarium.heracles.common.network.packets.quests.SyncQuestsPacket; import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerPlayer; @@ -18,12 +19,14 @@ public final class QuestSyncer { public static void syncToAll(MinecraftServer server, List players) { NetworkHandler.CHANNEL.sendToPlayers(createPacket(), players); + NetworkHandler.CHANNEL.sendToPlayers(new SyncGroupOrderPacket(QuestHandler.groupsOrder()), players); syncDescriptions(players); QuestProgressHandler.read(server).updatePossibleQuests(); } public static void sync(ServerPlayer player) { NetworkHandler.CHANNEL.sendToPlayer(createPacket(), player); + NetworkHandler.CHANNEL.sendToPlayer(new SyncGroupOrderPacket(QuestHandler.groupsOrder()), player); PinnedQuestHandler.sync(player); syncDescriptions(List.of(player)); } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/NetworkHandler.java b/common/src/main/java/earth/terrarium/heracles/common/network/NetworkHandler.java index 3857604b..1419a753 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/NetworkHandler.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/NetworkHandler.java @@ -7,6 +7,7 @@ import earth.terrarium.heracles.common.network.packets.QuestUnlockedPacket; import earth.terrarium.heracles.common.network.packets.groups.CreateGroupPacket; import earth.terrarium.heracles.common.network.packets.groups.DeleteGroupPacket; +import earth.terrarium.heracles.common.network.packets.groups.EditGroupPacket; import earth.terrarium.heracles.common.network.packets.groups.OpenGroupPacket; import earth.terrarium.heracles.common.network.packets.pinned.SetPinnedQuestPacket; import earth.terrarium.heracles.common.network.packets.pinned.SyncPinnedQuestsPacket; @@ -37,6 +38,7 @@ public static void init() { CHANNEL.register(ClientboundRemoveQuestPacket.TYPE); CHANNEL.register(ClientboundUpdateQuestPacket.TYPE); CHANNEL.register(QuestUnlockedPacket.TYPE); + CHANNEL.register(SyncGroupOrderPacket.TYPE); CHANNEL.register(OpenGroupPacket.TYPE); CHANNEL.register(OpenQuestPacket.TYPE); @@ -51,5 +53,7 @@ public static void init() { CHANNEL.register(CheckTaskPacket.TYPE); CHANNEL.register(ManualItemTaskPacket.TYPE); CHANNEL.register(ManualXpTaskPacket.TYPE); + CHANNEL.register(EditGroupPacket.TYPE); + CHANNEL.register(ServerboundUpdateGroupOrderPacket.TYPE); } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/CreateGroupPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/CreateGroupPacket.java index eb54503e..cc850f38 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/CreateGroupPacket.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/CreateGroupPacket.java @@ -4,6 +4,7 @@ import com.teamresourceful.resourcefullib.common.network.base.PacketType; import com.teamresourceful.resourcefullib.common.network.base.ServerboundPacketType; import earth.terrarium.heracles.Heracles; +import earth.terrarium.heracles.api.groups.Group; import earth.terrarium.heracles.common.handlers.quests.QuestHandler; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.resources.ResourceLocation; @@ -44,8 +45,8 @@ public ResourceLocation id() { @Override public Consumer handle(CreateGroupPacket message) { return (player) -> { - if (player.hasPermissions(2) && !QuestHandler.groups().contains(message.group)) { - QuestHandler.groups().add(message.group); + if (player.hasPermissions(2) && !QuestHandler.groups().containsKey(message.group)) { + QuestHandler.groups().put(message.group, new Group(message.group)); QuestHandler.saveGroups(); } }; diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/DeleteGroupPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/DeleteGroupPacket.java index d1bae1f8..63d8ede2 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/DeleteGroupPacket.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/DeleteGroupPacket.java @@ -44,7 +44,7 @@ public DeleteGroupPacket decode(FriendlyByteBuf buffer) { @Override public Consumer handle(DeleteGroupPacket message) { return (player) -> { - if (player.hasPermissions(2) && QuestHandler.groups().contains(message.group)) { + if (player.hasPermissions(2) && QuestHandler.groups().containsKey(message.group)) { QuestHandler.groups().remove(message.group); QuestHandler.saveGroups(); } diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/EditGroupPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/EditGroupPacket.java new file mode 100644 index 00000000..1ef4c1f5 --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/EditGroupPacket.java @@ -0,0 +1,89 @@ +package earth.terrarium.heracles.common.network.packets.groups; + +import com.teamresourceful.resourcefullib.common.network.Packet; +import com.teamresourceful.resourcefullib.common.network.base.PacketType; +import com.teamresourceful.resourcefullib.common.network.base.ServerboundPacketType; +import com.teamresourceful.resourcefullib.common.networking.PacketHelper; +import earth.terrarium.heracles.Heracles; +import earth.terrarium.heracles.api.groups.Group; +import earth.terrarium.heracles.api.quests.QuestIcon; +import earth.terrarium.heracles.api.quests.QuestIcons; +import earth.terrarium.heracles.common.handlers.quests.QuestHandler; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; + +import java.util.Optional; +import java.util.function.Consumer; + +public record EditGroupPacket( + String group, + Optional> icon, + Optional title, + Optional description +) implements Packet { + + public static final ServerboundPacketType TYPE = new Type(); + + public static EditGroupPacket ofTitle(String group, String title) { + return new EditGroupPacket(group, Optional.empty(), Optional.of(title), Optional.empty()); + } + + public static EditGroupPacket ofIcon(String group, QuestIcon icon) { + return new EditGroupPacket(group, Optional.of(icon), Optional.empty(), Optional.empty()); + } + + @Override + public PacketType type() { + return TYPE; + } + + public static class Type implements ServerboundPacketType { + + public static final ResourceLocation ID = new ResourceLocation(Heracles.MOD_ID, "edit_group"); + + @Override + public Class type() { + return EditGroupPacket.class; + } + + @Override + public ResourceLocation id() { + return ID; + } + + @Override + public void encode(EditGroupPacket message, FriendlyByteBuf buffer) { + buffer.writeUtf(message.group); + buffer.writeOptional(message.icon(), (buf, icon) -> PacketHelper.writeWithRegistryYabn(Heracles.getRegistryAccess(), buffer, QuestIcons.CODEC, icon, true)); + buffer.writeOptional(message.title(), FriendlyByteBuf::writeUtf); + buffer.writeOptional(message.description(), FriendlyByteBuf::writeUtf); + } + + @Override + public EditGroupPacket decode(FriendlyByteBuf buffer) { + String id = buffer.readUtf(); + Optional> icon = buffer.readOptional(buf -> PacketHelper.readWithRegistryYabn(Heracles.getRegistryAccess(), buf, QuestIcons.CODEC, true) + .getOrThrow(false, System.err::println)); + Optional title = buffer.readOptional(FriendlyByteBuf::readUtf); + Optional description = buffer.readOptional(FriendlyByteBuf::readUtf); + return new EditGroupPacket(id, icon, title, description); + } + + @Override + public Consumer handle(EditGroupPacket message) { + return (player) -> { + if (player.hasPermissions(2) && QuestHandler.groups().containsKey(message.group())) { + Group group = QuestHandler.groups().get(message.group()); + QuestHandler.groups().put(message.group(), new Group( + message.icon().or(group::icon), + message.title().orElse(group.title()), + message.description().orElse(group.description()) + )); + QuestHandler.saveGroups(); + //TODO send packet to all players to update their quest gui + } + }; + } + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/OpenGroupPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/OpenGroupPacket.java index 0d0d3ba6..6e035348 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/OpenGroupPacket.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/groups/OpenGroupPacket.java @@ -50,7 +50,7 @@ public Consumer handle(OpenGroupPacket message) { if (player instanceof ServerPlayer serverPlayer) { String group = message.group; if (group.isEmpty()) { - group = QuestHandler.groups().get(0); + group = QuestHandler.groups().keySet().iterator().next(); } if (message.edit) { ModUtils.editGroup(serverPlayer, group); diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/ServerboundUpdateGroupOrderPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/ServerboundUpdateGroupOrderPacket.java new file mode 100644 index 00000000..515e39de --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/ServerboundUpdateGroupOrderPacket.java @@ -0,0 +1,61 @@ +package earth.terrarium.heracles.common.network.packets.quests; + +import com.teamresourceful.resourcefullib.common.network.Packet; +import com.teamresourceful.resourcefullib.common.network.base.PacketType; +import com.teamresourceful.resourcefullib.common.network.base.ServerboundPacketType; +import earth.terrarium.heracles.Heracles; +import earth.terrarium.heracles.common.handlers.quests.QuestHandler; +import earth.terrarium.heracles.common.network.NetworkHandler; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.world.entity.player.Player; + +import java.util.List; +import java.util.function.Consumer; + +public record ServerboundUpdateGroupOrderPacket( + List order +) implements Packet { + + public static final ServerboundPacketType TYPE = new Type(); + + @Override + public PacketType type() { + return TYPE; + } + + private static class Type implements ServerboundPacketType { + + @Override + public Class type() { + return ServerboundUpdateGroupOrderPacket.class; + } + + @Override + public ResourceLocation id() { + return new ResourceLocation(Heracles.MOD_ID, "update_group_order"); + } + + @Override + public void encode(ServerboundUpdateGroupOrderPacket message, FriendlyByteBuf buffer) { + buffer.writeCollection(message.order, FriendlyByteBuf::writeUtf); + } + + @Override + public ServerboundUpdateGroupOrderPacket decode(FriendlyByteBuf buffer) { + return new ServerboundUpdateGroupOrderPacket(buffer.readList(FriendlyByteBuf::readUtf)); + } + + @Override + public Consumer handle(ServerboundUpdateGroupOrderPacket message) { + return (player) -> { + if (player.hasPermissions(2)) { + QuestHandler.groupsOrder().clear(); + QuestHandler.groupsOrder().addAll(message.order); + QuestHandler.saveGroups(); + NetworkHandler.CHANNEL.sendToAllPlayers(new SyncGroupOrderPacket(QuestHandler.groupsOrder()), player.getServer()); + } + }; + } + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncGroupOrderPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncGroupOrderPacket.java new file mode 100644 index 00000000..88175d73 --- /dev/null +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncGroupOrderPacket.java @@ -0,0 +1,50 @@ +package earth.terrarium.heracles.common.network.packets.quests; + +import com.teamresourceful.resourcefullib.common.network.Packet; +import com.teamresourceful.resourcefullib.common.network.base.ClientboundPacketType; +import com.teamresourceful.resourcefullib.common.network.base.PacketType; +import earth.terrarium.heracles.Heracles; +import earth.terrarium.heracles.client.handlers.ClientQuests; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; + +import java.util.List; + +public record SyncGroupOrderPacket(List groups) implements Packet { + + public static final ClientboundPacketType TYPE = new Type(); + + @Override + public PacketType type() { + return TYPE; + } + + private static class Type implements ClientboundPacketType { + + @Override + public Class type() { + return SyncGroupOrderPacket.class; + } + + @Override + public ResourceLocation id() { + return new ResourceLocation(Heracles.MOD_ID, "sync_group_orders"); + } + + @Override + public void encode(SyncGroupOrderPacket message, FriendlyByteBuf buffer) { + buffer.writeCollection(message.groups(), FriendlyByteBuf::writeUtf); + } + + @Override + public SyncGroupOrderPacket decode(FriendlyByteBuf buffer) { + return new SyncGroupOrderPacket(buffer.readList(FriendlyByteBuf::readUtf)); + } + + + @Override + public Runnable handle(SyncGroupOrderPacket message) { + return () -> ClientQuests.syncGroupOrders(message.groups()); + } + } +} diff --git a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncQuestsPacket.java b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncQuestsPacket.java index e77562fd..5d3af6d3 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncQuestsPacket.java +++ b/common/src/main/java/earth/terrarium/heracles/common/network/packets/quests/SyncQuestsPacket.java @@ -10,6 +10,7 @@ import com.teamresourceful.yabn.elements.YabnElement; import com.teamresourceful.yabn.reader.ByteReader; import earth.terrarium.heracles.Heracles; +import earth.terrarium.heracles.api.groups.Group; import earth.terrarium.heracles.api.quests.Quest; import earth.terrarium.heracles.client.handlers.ClientQuests; import io.netty.buffer.ByteBuf; @@ -17,10 +18,9 @@ import net.minecraft.resources.RegistryOps; import net.minecraft.resources.ResourceLocation; -import java.util.List; import java.util.Map; -public record SyncQuestsPacket(Map quests, List groups) implements Packet { +public record SyncQuestsPacket(Map quests, Map groups) implements Packet { public static final ClientboundPacketType TYPE = new Type(); @@ -31,6 +31,7 @@ public PacketType type() { private static class Type implements ClientboundPacketType { private static final Codec> QUEST_MAP_CODEC = Codec.unboundedMap(Codec.STRING, Quest.CODEC); + private static final Codec> GROUP_MAP_CODEC = Codec.unboundedMap(Codec.STRING, Group.CODEC); @Override public Class type() { @@ -45,19 +46,22 @@ public ResourceLocation id() { @Override public void encode(SyncQuestsPacket message, FriendlyByteBuf buffer) { PacketHelper.writeWithRegistryYabn(Heracles.getRegistryAccess(), buffer, QUEST_MAP_CODEC, message.quests(), true); - buffer.writeCollection(message.groups(), FriendlyByteBuf::writeUtf); + PacketHelper.writeWithRegistryYabn(Heracles.getRegistryAccess(), buffer, GROUP_MAP_CODEC, message.groups(), true); } @Override public SyncQuestsPacket decode(FriendlyByteBuf buffer) { - YabnElement element = YabnParser.parse(new ByteBufByteReader(buffer)); + YabnElement quests = YabnParser.parse(new ByteBufByteReader(buffer)); + YabnElement groups = YabnParser.parse(new ByteBufByteReader(buffer)); try { return new SyncQuestsPacket( - QUEST_MAP_CODEC.parse(RegistryOps.create(YabnOps.COMPRESSED, Heracles.getRegistryAccess()), element).get().orThrow(), - buffer.readList(FriendlyByteBuf::readUtf) + QUEST_MAP_CODEC.parse(RegistryOps.create(YabnOps.COMPRESSED, Heracles.getRegistryAccess()), quests).get().orThrow(), + GROUP_MAP_CODEC.parse(RegistryOps.create(YabnOps.COMPRESSED, Heracles.getRegistryAccess()), groups).get().orThrow() ); - } catch (Exception e) { - Heracles.LOGGER.error("Failed to decode sync quests packet: {}", element, e); + }catch (Exception e) { + Heracles.LOGGER.error("Failed to decode sync quests packet"); + Heracles.LOGGER.error("Quests: " + quests); + Heracles.LOGGER.error("Groups: " + groups); throw e; } } diff --git a/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java b/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java index c57d0907..6d743607 100644 --- a/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java +++ b/common/src/main/java/earth/terrarium/heracles/common/utils/ModUtils.java @@ -92,7 +92,7 @@ public static void openEditQuest(ServerPlayer player, String group, String id) { } public static void openGroup(ServerPlayer player, String group) { - if (!QuestHandler.groups().contains(group)) { + if (!QuestHandler.groups().containsKey(group)) { player.sendSystemMessage(Component.translatable("gui.heracles.error.group.not_found", group)); return; } @@ -103,7 +103,7 @@ public static void openGroup(ServerPlayer player, String group) { } public static void editGroup(ServerPlayer player, String group) { - if (!QuestHandler.groups().contains(group)) { + if (!QuestHandler.groups().containsKey(group)) { player.sendSystemMessage(Component.translatable("gui.heracles.error.group.not_found", group)); player.closeContainer(); return;