diff --git a/gradle.properties b/gradle.properties index 415cef8..a9e6a4a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,7 +13,7 @@ cloth_config_version=12.0.119 mod_menu_version=8.0.1 # Project Metadata -mod_version=0.3.4 +mod_version=0.3.5 mod_name=Blockgame Journal default_release_type=release github_owner=blackjack26 diff --git a/src/main/java/dev/bnjc/blockgamejournal/config/modules/GeneralConfig.java b/src/main/java/dev/bnjc/blockgamejournal/config/modules/GeneralConfig.java index 8d04e75..a701585 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/config/modules/GeneralConfig.java +++ b/src/main/java/dev/bnjc/blockgamejournal/config/modules/GeneralConfig.java @@ -17,6 +17,9 @@ public class GeneralConfig implements ConfigData { @ConfigEntry.Gui.Tooltip public boolean showRecipeLock; + @ConfigEntry.Gui.Tooltip + public boolean enableManualTracking; + @ConfigEntry.Gui.Tooltip @ConfigEntry.Gui.EnumHandler(option = ConfigEntry.Gui.EnumHandler.EnumDisplayOption.BUTTON) public JournalMode.Type defaultMode; @@ -29,6 +32,7 @@ public GeneralConfig() { highlightMissingRecipes = true; highlightOutdatedRecipes = true; showRecipeLock = true; + enableManualTracking = false; defaultMode = JournalMode.Type.ITEM_SEARCH; defaultNpcSort = ItemListWidget.VendorSort.A_TO_Z; } diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/RecipeTrackerGameFeature.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/RecipeTrackerGameFeature.java index b41ec6f..657f8c1 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/RecipeTrackerGameFeature.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/RecipeTrackerGameFeature.java @@ -7,14 +7,12 @@ import dev.bnjc.blockgamejournal.client.BlockgameJournalClient; import dev.bnjc.blockgamejournal.gamefeature.GameFeature; import dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.BackpackHandler; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.CraftingStationHandler; import dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.ProfileHandler; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.RecipePreviewHandler; +import dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.vendor.VendorHandler; import dev.bnjc.blockgamejournal.gui.screen.JournalScreen; import dev.bnjc.blockgamejournal.journal.Journal; import dev.bnjc.blockgamejournal.journal.npc.NPCEntry; import dev.bnjc.blockgamejournal.listener.chat.ReceiveChatListener; -import dev.bnjc.blockgamejournal.listener.interaction.EntityAttackedListener; import dev.bnjc.blockgamejournal.listener.interaction.ItemInteractListener; import dev.bnjc.blockgamejournal.listener.renderer.PostRenderListener; import dev.bnjc.blockgamejournal.storage.Storage; @@ -72,10 +70,7 @@ public class RecipeTrackerGameFeature extends GameFeature { private static final Pattern BALANCE_PATTERN = Pattern.compile("Balance: ([\\d,]+(?:\\.\\d+)?)\\$"); @Getter - private final RecipePreviewHandler recipePreviewHandler; - - @Getter - private final CraftingStationHandler craftingStationHandler; + private final VendorHandler vendorHandler; @Getter private final ProfileHandler profileHandler; @@ -83,10 +78,6 @@ public class RecipeTrackerGameFeature extends GameFeature { @Getter private final BackpackHandler backpackHandler; - @Getter - @Nullable - private Entity lastAttackedEntity = null; - @Getter @Nullable private Screen lastScreen = null; @@ -96,8 +87,7 @@ public class RecipeTrackerGameFeature extends GameFeature { private String lastRecipeName = null; public RecipeTrackerGameFeature() { - this.recipePreviewHandler = new RecipePreviewHandler(this); - this.craftingStationHandler = new CraftingStationHandler(this); + this.vendorHandler = new VendorHandler(); this.profileHandler = new ProfileHandler(this); this.backpackHandler = new BackpackHandler(this); } @@ -108,7 +98,6 @@ public void init(MinecraftClient minecraftClient, BlockgameJournalClient blockga ClientPlayConnectionEvents.JOIN.register(this::handleJoin); ClientPlayConnectionEvents.DISCONNECT.register(this::handleDisconnect); - EntityAttackedListener.EVENT.register(this::handleEntityAttacked); ItemInteractListener.EVENT.register(this::handleItemInteract); ClientCommandRegistrationCallback.EVENT.register(this::registerCommand); ScreenEvents.AFTER_INIT.register(this::handleScreenInit); @@ -121,8 +110,7 @@ public void init(MinecraftClient minecraftClient, BlockgameJournalClient blockga } }); - this.craftingStationHandler.init(); - this.recipePreviewHandler.init(); + this.vendorHandler.init(); this.profileHandler.init(); this.backpackHandler.init(); @@ -256,13 +244,6 @@ else if (screen.getFocused() instanceof TextFieldWidget textFieldWidget) { } } - private ActionResult handleEntityAttacked(PlayerEntity playerEntity, Entity entity) { - lastAttackedEntity = entity; - craftingStationHandler.reset(); - - return ActionResult.PASS; - } - private ActionResult handleChatMessage(MinecraftClient client, String message) { if (Journal.INSTANCE == null) { return ActionResult.PASS; diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/CraftingStationHandler.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/CraftingStationHandler.java deleted file mode 100644 index 4d29526..0000000 --- a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/CraftingStationHandler.java +++ /dev/null @@ -1,373 +0,0 @@ -package dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers; - -import dev.bnjc.blockgamejournal.BlockgameJournal; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.RecipeTrackerGameFeature; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.station.CraftingStationItem; -import dev.bnjc.blockgamejournal.journal.Journal; -import dev.bnjc.blockgamejournal.journal.JournalEntry; -import dev.bnjc.blockgamejournal.journal.npc.NPCEntry; -import dev.bnjc.blockgamejournal.journal.npc.NPCUtil; -import dev.bnjc.blockgamejournal.listener.interaction.SlotClickedListener; -import dev.bnjc.blockgamejournal.listener.screen.DrawSlotListener; -import dev.bnjc.blockgamejournal.listener.screen.ScreenOpenedListener; -import dev.bnjc.blockgamejournal.listener.screen.ScreenReceivedInventoryListener; -import dev.bnjc.blockgamejournal.util.GuiUtil; -import dev.bnjc.blockgamejournal.util.ItemUtil; -import dev.bnjc.blockgamejournal.util.NbtUtil; -import dev.bnjc.blockgamejournal.util.Profession; -import lombok.Getter; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.client.network.ClientPlayerEntity; -import net.minecraft.entity.Entity; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.nbt.NbtList; -import net.minecraft.network.packet.s2c.play.InventoryS2CPacket; -import net.minecraft.network.packet.s2c.play.OpenScreenS2CPacket; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.text.MutableText; -import net.minecraft.util.ActionResult; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import java.util.*; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class CraftingStationHandler { - private static final Logger LOGGER = BlockgameJournal.getLogger("Crafting Station"); - - private static final Pattern COIN_PATTERN = Pattern.compile("([✔✖]) Requires (\\d+(?:\\.\\d+)?) Coin"); - private static final Pattern KNOWLEDGE_PATTERN = Pattern.compile("([✔✖]) Recipe (Known|Learned)"); - private static final Pattern CLASS_PATTERN = Pattern.compile("([✔✖]) Requires (\\d+) in ([A-Za-z]+)"); - private static final Pattern INGREDIENT_HEADER_PATTERN = Pattern.compile("Ingredients:"); - private static final Pattern INGREDIENT_PATTERN = Pattern.compile("([✔✖]) (\\d+) (.+?)$"); - - private final RecipeTrackerGameFeature gameFeature; - private final List inventory = new ArrayList<>(); - - private static final byte STATUS_NONE = 0; - private static final byte STATUS_MISSING = 1; - private static final byte STATUS_OUTDATED = 1 << 1; - private static final byte STATUS_LOCKED = 1 << 2; - private final Map statusCache = new HashMap<>(); - - private int syncId = -1; - private String npcName = ""; - - @Getter - @Nullable - private CraftingStationItem lastClickedItem; - - private boolean createOrUpdateVendor = false; - - public CraftingStationHandler(RecipeTrackerGameFeature gameFeature) { - this.gameFeature = gameFeature; - } - - public void init() { - ScreenOpenedListener.EVENT.register(this::handleOpenScreen); - ScreenReceivedInventoryListener.EVENT.register(this::handleScreenInventory); - SlotClickedListener.EVENT.register(this::handleSlotClicked); - DrawSlotListener.EVENT.register(this::drawSlot); - } - - public void reset() { - this.syncId = -1; - this.inventory.clear(); - this.npcName = ""; - this.lastClickedItem = null; - this.statusCache.clear(); - this.createOrUpdateVendor = false; - } - - private ActionResult handleOpenScreen(OpenScreenS2CPacket packet) { - this.createOrUpdateVendor = false; - String screenName = packet.getName().getString(); - - // Look for screen name "Some Name (#page#/#max#)" - exclude "Party" from the name - Matcher matcher = Pattern.compile("^((?!Party)[\\w\\s]+)\\s\\(\\d+/\\d+\\)").matcher(screenName); - Entity lastAttackedEntity = this.gameFeature.getLastAttackedEntity(); - - String entityName = ""; - if (lastAttackedEntity != null) { - entityName = lastAttackedEntity.getEntityName(); - - // Only use custom name if the entity is not a player - if (!(lastAttackedEntity instanceof PlayerEntity) && lastAttackedEntity.hasCustomName()) { - entityName = lastAttackedEntity.getCustomName().getString(); - } - } - - if (matcher.find() || (lastAttackedEntity != null && screenName.equals(entityName))) { - this.syncId = packet.getSyncId(); - - if (lastAttackedEntity != null) { - this.npcName = entityName; - this.createOrUpdateVendor = true; - } else { - this.npcName = matcher.group(1); - } - } else { - this.syncId = -1; - } - - // TODO: Add paging - this.inventory.clear(); - this.statusCache.clear(); - - return ActionResult.PASS; - } - - private ActionResult handleScreenInventory(InventoryS2CPacket packet) { - if (packet.getSyncId() != this.syncId) { - return ActionResult.PASS; - } - - packet.getContents().forEach(item -> { - if (item == null || item.isEmpty()) { - this.inventory.add(null); - return; - } - - CraftingStationItem invItem = new CraftingStationItem(item, this.inventory.size()); - NbtList loreTag = NbtUtil.getLore(invItem.getItem()); - if (loreTag != null) { - this.parseLoreMetadata(invItem, loreTag); - } - - this.inventory.add(invItem); - }); - - Entity lastAttackedEntity = this.gameFeature.getLastAttackedEntity(); - if (this.createOrUpdateVendor && !this.inventory.isEmpty() && Journal.INSTANCE != null && lastAttackedEntity != null) { - NPCUtil.createOrUpdate(this.npcName, lastAttackedEntity); - this.createOrUpdateVendor = false; - } - - if (Journal.INSTANCE != null) { - for (JournalEntry journalEntry : Journal.INSTANCE.getEntriesForVendor(this.npcName)) { - boolean found = false; - - // See if there is a matching item in the inventory in the same slot - for (CraftingStationItem stationItem : this.inventory) { - if (stationItem == null) { - continue; - } - - if (ItemUtil.getKey(stationItem.getItem()).equals(journalEntry.getKey())) { - found = true; - break; - } - } - - journalEntry.setUnavailable(!found); - } - } - - return ActionResult.PASS; - } - - private ActionResult handleSlotClicked(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player) { - if (syncId != this.syncId) { - return ActionResult.PASS; - } - - if (slotId < 0 || slotId >= this.inventory.size()) { - LOGGER.warn("[Blockgame Journal] Slot out of bounds: {}", slotId); - return ActionResult.PASS; - } - - CraftingStationItem inventoryItem = this.inventory.get(slotId); - - if (inventoryItem == null) { - LOGGER.warn("[Blockgame Journal] Empty item clicked"); - this.lastClickedItem = null; - return ActionResult.PASS; - } - - this.lastClickedItem = inventoryItem; - return ActionResult.PASS; - } - - private void drawSlot(DrawContext context, Slot slot) { - if (this.syncId == -1 || this.inventory.isEmpty() || Journal.INSTANCE == null) { - return; - } - - ClientPlayerEntity player = MinecraftClient.getInstance().player; - if (player != null && player.currentScreenHandler.syncId != this.syncId) { - this.syncId = -1; - this.inventory.clear(); - this.npcName = ""; - return; - } - - boolean highlightMissing = BlockgameJournal.getConfig().getGeneralConfig().highlightMissingRecipes; - boolean highlightOutdated = BlockgameJournal.getConfig().getGeneralConfig().highlightOutdatedRecipes; - boolean showRecipeLock = BlockgameJournal.getConfig().getGeneralConfig().showRecipeLock; - if (!highlightMissing && !highlightOutdated && !showRecipeLock) { - return; - } - - // Check if slot is within bounds (0-53) - if (slot.id < 0 || slot.id >= this.inventory.size() || slot.id >= 54) { - return; - } - - Byte status = this.statusCache.get(slot.id); - if (status != null) { - if ((status & STATUS_MISSING) != 0) { - this.highlightSlot(context, slot, 0x30FF0000); - } else if ((status & STATUS_OUTDATED) != 0) { - this.highlightSlot(context, slot, 0x40CCCC00); - } - - if ((status & STATUS_LOCKED) != 0) { - this.drawLocked(context, slot); - } - return; - } - - // See if slot item matches inventory item - ItemStack slotItem = slot.getStack(); - CraftingStationItem inventoryItem = this.inventory.get(slot.id); - if (slotItem == null || inventoryItem == null || slotItem.isEmpty()) { - return; - } - - if (slotItem.getItem() != inventoryItem.getItem().getItem()) { - LOGGER.warn("[Blockgame Journal] Slot item does not match inventory item"); - return; - } - - boolean recipeNotKnown = Boolean.FALSE.equals(inventoryItem.getRecipeKnown()); - boolean profRequirementNotMet = false; - if (inventoryItem.getRequiredLevel() != -1) { - int currLevel = Journal.INSTANCE.getMetadata().getProfessionLevels().getOrDefault(inventoryItem.getRequiredClass(), -1); - if (currLevel != -1 && currLevel < inventoryItem.getRequiredLevel()) { - profRequirementNotMet = true; - } - } - - if ((recipeNotKnown || profRequirementNotMet) && showRecipeLock) { - this.drawLocked(context, slot); - this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_LOCKED : (byte) (v | STATUS_LOCKED)); - } - - List entries = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(inventoryItem.getItem()), new ArrayList<>()); - for (JournalEntry entry : entries) { - String expectedNpcName = entry.getNpcName(); - int expectedSlot = entry.getSlot(); - - // If the item is in the journal, don't highlight it unless it's outdated - if (this.npcName.equals(expectedNpcName) && slot.id == expectedSlot) { - inventoryItem.setOutdated(ItemUtil.isOutdated(entry, inventoryItem)); - if (highlightOutdated && inventoryItem.getOutdated()) { - this.highlightSlot(context, slot, 0x40CCCC00); - this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_OUTDATED : (byte) (v | STATUS_OUTDATED)); - } else { - // Negate the status if it's not outdated - this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_NONE : (byte) (v & ~STATUS_OUTDATED)); - } - return; - } - } - - // If the item is not in the journal, highlight it - if (highlightMissing) { - this.highlightSlot(context, slot, 0x30FF0000); - this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_MISSING : (byte) (v | STATUS_MISSING)); - } - } - - private void drawLocked(DrawContext context, Slot slot) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 150); - context.drawGuiTexture(GuiUtil.sprite("lock_icon"), slot.x + 10, slot.y - 2, 150, 8, 8); - context.getMatrices().pop(); - } - - private void highlightSlot(DrawContext context, Slot slot, int color) { - context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, color); - context.drawBorder(slot.x, slot.y, 16, 16, color | 0xBB000000); - } - - /** - * Parses the metadata from the clicked item's lore. This includes the coin cost, recipe known status, class - * requirement, and expected ingredients. - */ - private void parseLoreMetadata(@Nullable CraftingStationItem item, NbtList loreTag) { - if (item == null) { - LOGGER.warn("[Blockgame Journal] No last clicked item found"); - return; - } - - if (loreTag == null) { - LOGGER.warn("[Blockgame Journal] No lore tag found"); - return; - } - - boolean listingIngredients = false; - for (int i = 0; i < loreTag.size(); i++) { - String lore = loreTag.getString(i); - MutableText text = NbtUtil.parseLoreLine(lore); - if (text == null) { - LOGGER.warn("[Blockgame Journal] Failed to parse lore line: {}", lore); - continue; - } - - lore = text.getString(); - - if (listingIngredients) { - Matcher ingredientMatcher = INGREDIENT_PATTERN.matcher(lore); - if (ingredientMatcher.find()) { - String name = ingredientMatcher.group(3); - item.addExpectedIngredient(name, Integer.parseInt(ingredientMatcher.group(2))); - continue; - } - - if (lore.isBlank()) { - // If we find an empty text, we might have more ingredients on the next line - continue; - } - - // If we didn't match an ingredient, we're done listing them - listingIngredients = false; - } else { - // Check for coin cost - Matcher coinMatcher = COIN_PATTERN.matcher(lore); - if (coinMatcher.find()) { - item.setCost(Float.parseFloat(coinMatcher.group(2))); - continue; - } - - // Check for recipe known - Matcher knowledgeMatcher = KNOWLEDGE_PATTERN.matcher(lore); - if (knowledgeMatcher.find()) { - item.setRecipeKnown("✔".equals(knowledgeMatcher.group(1))); - continue; - } - - // Check for class requirement - Matcher classMatcher = CLASS_PATTERN.matcher(lore); - if (classMatcher.find()) { - item.setRequiredLevel(Integer.parseInt(classMatcher.group(2))); - item.setRequiredClass(classMatcher.group(3)); - continue; - } - - // Check for ingredients header - Matcher ingredientMatcher = INGREDIENT_HEADER_PATTERN.matcher(lore); - if (ingredientMatcher.find()) { - listingIngredients = true; - continue; - } - } - - LOGGER.debug("[Blockgame Journal] Unrecognized lore: {}", lore); - } - } -} diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/RecipePreviewHandler.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/RecipePreviewHandler.java deleted file mode 100644 index 0c0f172..0000000 --- a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/RecipePreviewHandler.java +++ /dev/null @@ -1,352 +0,0 @@ -package dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers; - -import dev.bnjc.blockgamejournal.BlockgameJournal; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.RecipeTrackerGameFeature; -import dev.bnjc.blockgamejournal.gamefeature.recipetracker.station.CraftingStationItem; -import dev.bnjc.blockgamejournal.journal.Journal; -import dev.bnjc.blockgamejournal.journal.JournalEntry; -import dev.bnjc.blockgamejournal.journal.JournalEntryBuilder; -import dev.bnjc.blockgamejournal.journal.metadata.JournalAdvancement; -import dev.bnjc.blockgamejournal.listener.interaction.SlotClickedListener; -import dev.bnjc.blockgamejournal.listener.screen.DrawSlotListener; -import dev.bnjc.blockgamejournal.listener.screen.ScreenOpenedListener; -import dev.bnjc.blockgamejournal.listener.screen.ScreenReceivedInventoryListener; -import dev.bnjc.blockgamejournal.util.ItemUtil; -import lombok.Getter; -import net.minecraft.client.MinecraftClient; -import net.minecraft.client.gui.DrawContext; -import net.minecraft.entity.player.PlayerEntity; -import net.minecraft.item.ItemStack; -import net.minecraft.item.Items; -import net.minecraft.item.KnowledgeBookItem; -import net.minecraft.item.PlayerHeadItem; -import net.minecraft.network.packet.s2c.play.InventoryS2CPacket; -import net.minecraft.network.packet.s2c.play.OpenScreenS2CPacket; -import net.minecraft.screen.slot.Slot; -import net.minecraft.screen.slot.SlotActionType; -import net.minecraft.util.ActionResult; -import org.jetbrains.annotations.Nullable; -import org.slf4j.Logger; - -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.List; -import java.util.Queue; - -public class RecipePreviewHandler { - private static final Logger LOGGER = BlockgameJournal.getLogger("Recipe Preview"); - - private static final int[] RECIPE_SLOTS = { 12, 13, 14, 21, 22, 23, 30, 31, 32 }; - - private static final int BACK_BUTTON_INDEX = 10; - private static final int ITEM_INDEX = 16; - private static final int PREV_PAGE_BUTTON_INDEX = 20; - private static final int NEXT_PAGE_BUTTON_INDEX = 24; - private static final int CRAFT_BUTTON_INDEX = 28; - private static final int CONFIRM_BUTTON_INDEX = 34; - - private static Queue lastPreviewTimes = new ArrayDeque<>(); - - private final RecipeTrackerGameFeature gameFeature; - private int syncId = -1; - - @Getter - private int recipePage = 0; - - private boolean isLoadingNextPage = false; - private boolean isLoadingPrevPage = false; - private boolean stored = false; - - private final List ingredients = new ArrayList<>(); - - public RecipePreviewHandler(RecipeTrackerGameFeature gameFeature) { - this.gameFeature = gameFeature; - } - - public void init() { - ScreenOpenedListener.EVENT.register(this::handleOpenScreen); - ScreenReceivedInventoryListener.EVENT.register(this::handleScreenInventory); - SlotClickedListener.EVENT.register(this::handleSlotClicked); - DrawSlotListener.EVENT.register(this::drawSlot); - } - - private ActionResult handleOpenScreen(OpenScreenS2CPacket packet) { - String screenName = packet.getName().getString(); - - if (screenName.equals("Recipe Preview")) { - this.syncId = packet.getSyncId(); - - // Reset the recipe page if the screen is not the same as the previous one - if (!isLoadingNextPage && !isLoadingPrevPage) { - recipePage = 0; - ingredients.clear(); - stored = false; - } - - isLoadingNextPage = false; - isLoadingPrevPage = false; - } else { - this.syncId = -1; - this.reset(); - } - - return ActionResult.PASS; - } - - private ActionResult handleScreenInventory(InventoryS2CPacket packet) { - if (packet.getSyncId() != this.syncId) { - return ActionResult.PASS; - } - - List inv = packet.getContents(); - boolean hasBackButton = inv.get(BACK_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; - boolean hasCraftButton = inv.get(CRAFT_BUTTON_INDEX).getItem() instanceof KnowledgeBookItem; - boolean isScreenValid = hasBackButton && hasCraftButton; - - // Ensure that this is a valid recipe preview screen - if (!isScreenValid) { - return ActionResult.PASS; - } - - this.storeRecipe(inv); - - return ActionResult.PASS; - } - - private ActionResult handleSlotClicked(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player) { - if (syncId != this.syncId) { - return ActionResult.PASS; - } - - // Handle next button click - if (slotId == NEXT_PAGE_BUTTON_INDEX) { - isLoadingNextPage = true; - recipePage++; - } - // Handle prev button click - else if (slotId == PREV_PAGE_BUTTON_INDEX) { - isLoadingPrevPage = true; - recipePage--; - } - - return ActionResult.PASS; - } - - private void drawSlot(DrawContext context, Slot slot) { - if (this.syncId == -1) { - return; - } - - // Highlight - if (slot.id == NEXT_PAGE_BUTTON_INDEX && !this.stored) { - ItemStack item = slot.getStack(); - if (item.getItem() instanceof PlayerHeadItem) { - context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0x30FF0000); - context.drawBorder(slot.x, slot.y, 16, 16, 0xBBFF0000); - context.getMatrices().push(); - context.getMatrices().translate(0.0f, 0.0f, 200.0f); - context.drawText( - MinecraftClient.getInstance().textRenderer, - "●", - slot.x + 18 - MinecraftClient.getInstance().textRenderer.getWidth("●"), - slot.y - 3, - 0xFF3333, - true - ); - context.getMatrices().pop(); - } - } - } - - public void reset() { - this.recipePage = 0; - this.ingredients.clear(); - this.clearLoading(); - } - - public void clearLoading() { - this.isLoadingNextPage = false; - this.isLoadingPrevPage = false; - } - - private void storeRecipe(List inventory) { - if (Journal.INSTANCE == null) { - LOGGER.warn("[Blockgame Journal] Journal is not loaded"); - return; - } - - if (this.gameFeature.getLastAttackedEntity() == null) { - LOGGER.warn("[Blockgame Journal] No station entity to attribute the recipe to"); - return; - } - - boolean hasPrevPageButton = inventory.get(PREV_PAGE_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; - boolean hasNextPageButton = inventory.get(NEXT_PAGE_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; - - if (!hasPrevPageButton) { - recipePage = 0; - } - - ItemStack recipeItem = inventory.get(ITEM_INDEX); - if (recipeItem.isEmpty()) { - LOGGER.warn("[Blockgame Journal] Recipe item is empty, cannot store recipe. Try again later."); - return; - } - - // Store the recipe in the player's journal - BlockgameJournal.LOGGER.debug("[Blockgame Journal] Storing recipe for {}", ItemUtil.getName(recipeItem)); - - // Only store the ingredients if they haven't been stored yet - if (this.ingredients.size() - 1 < recipePage * RECIPE_SLOTS.length) { - // Get only the items in the recipe slots - for (int slot : RECIPE_SLOTS) { - ItemStack item = inventory.get(slot); - if (item.getItem() == Items.AIR) { - continue; - } - - this.ingredients.add(item); - } - } else { - BlockgameJournal.LOGGER.debug("[Blockgame Journal] Ingredients already stored for page {}", recipePage); - } - - if (!hasNextPageButton) { - // Add or update all the known ingredients - for (ItemStack stack : this.ingredients) { - String ingredientKey = ItemUtil.getKey(stack); - - // Do not store "minecraft:" items - if (!ingredientKey.startsWith("minecraft:")) { - Journal.INSTANCE.getKnownItems().put(ingredientKey, stack); - } - - BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [ ] {} x{}", ItemUtil.getName(stack), stack.getCount()); - } - - CraftingStationItem lastClickedItem = this.gameFeature.getCraftingStationHandler().getLastClickedItem(); - JournalEntry entry = new JournalEntryBuilder( - this.ingredients, - this.gameFeature.getLastAttackedEntity(), - lastClickedItem == null ? -1 : lastClickedItem.getSlot() - ).build(recipeItem); - - if (this.validateEntry(lastClickedItem, entry)) { - if (lastClickedItem != null) { - float cost = lastClickedItem.getCost(); - entry.setCost(cost); - if (cost != -1f) { - LOGGER.debug("[Blockgame Journal] - [ ] {} Coins", cost); - } - - Boolean recipeKnown = lastClickedItem.getRecipeKnown(); - if (recipeKnown == null) { - entry.setRecipeKnown((byte) -1); - } - else { - entry.setRecipeKnown(recipeKnown ? (byte) 1 : (byte) 0); - Journal.INSTANCE.getMetadata().setKnownRecipe(entry.getKey(), recipeKnown); - LOGGER.debug("[Blockgame Journal] - [{}] Recipe Known", recipeKnown ? "X" : " "); - } - - String requiredClass = lastClickedItem.getRequiredClass(); - entry.setRequiredClass(requiredClass); - if (requiredClass != null && !requiredClass.isEmpty()) { - LOGGER.debug("[Blockgame Journal] - [ ] Required Class: {}", requiredClass); - } - - int requiredLevel = lastClickedItem.getRequiredLevel(); - entry.setRequiredLevel(requiredLevel); - if (requiredLevel != -1) { - LOGGER.debug("[Blockgame Journal] - [ ] Required Level: {}", requiredLevel); - } - } - - if (this.ingredients.isEmpty() && Float.compare(entry.getCost(), -1f) == 0) { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] No ingredients found in the recipe, and no cost was set"); - } else { - Journal.INSTANCE.addEntry(recipeItem, entry); - this.checkForAdvancement(); - this.stored = true; - } - } else { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] Recipe validation failed"); - } - } else { - BlockgameJournal.LOGGER.info("[Blockgame Journal] Waiting for next page to store recipe"); - } - } - - private boolean validateEntry(@Nullable CraftingStationItem item, JournalEntry entry) { - // Validate the ingredients of the recipe - BlockgameJournal.LOGGER.debug("[Blockgame Journal] Validating ingredients..."); - - if (Journal.INSTANCE == null) { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] Journal is not loaded"); - return false; - } - - // Validate we can from clicking the item - if (item == null) { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] No last clicked item found"); - return false; - } - - // Validate the recipe keys match - if (!ItemUtil.getKey(item.getItem()).equals(entry.getKey())) { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] Recipe key mismatch. Expected: {}, Actual: {}", ItemUtil.getKey(item.getItem()), entry.getKey()); - return false; - } - - // Validate the expected ingredients - if (entry.getIngredients().size() != item.getExpectedIngredients().size()) { - BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient count mismatch"); - return false; - } - - // TODO: Fix this matching -// Set ingredientKeys = entry.getIngredients().keySet(); -// for (String key : ingredientKeys) { -// ItemStack stack = Journal.INSTANCE.getKnownItem(key); -// if (stack == null) { -// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient not known: {}", key); -//// return false; -// break; -// } -// -// String itemName = ItemUtil.getName(stack); -// if (!item.getExpectedIngredients().containsKey(itemName)) { -// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient not expected: {}", itemName); -//// return false; -// break; -// } -// -// int expectedAmount = item.getExpectedIngredients().get(itemName); -// int actualAmount = entry.getIngredients().get(key); -// if (expectedAmount != actualAmount) { -// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient amount mismatch: {} (expected: {}, actual: {})", key, expectedAmount, actualAmount); -//// return false; -// break; -// } -// } - - return true; - } - - private void checkForAdvancement() { - if (Journal.INSTANCE == null || Journal.INSTANCE.getMetadata().hasAdvancement(JournalAdvancement.CARPAL_TUNNEL)) { - return; - } - - lastPreviewTimes.add(System.currentTimeMillis()); - - if (lastPreviewTimes.size() >= 25) { - long firstTime = lastPreviewTimes.poll(); - - // If it has been less than a minute since the first preview, grant the advancement - if (System.currentTimeMillis() - firstTime <= 60000) { - JournalAdvancement.CARPAL_TUNNEL.grant(); - } - } - } -} diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/RecipeBuilder.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/RecipeBuilder.java new file mode 100644 index 0000000..36237a9 --- /dev/null +++ b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/RecipeBuilder.java @@ -0,0 +1,194 @@ +package dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.vendor; + +import dev.bnjc.blockgamejournal.BlockgameJournal; +import dev.bnjc.blockgamejournal.gamefeature.recipetracker.station.CraftingStationItem; +import dev.bnjc.blockgamejournal.journal.Journal; +import dev.bnjc.blockgamejournal.journal.JournalEntry; +import dev.bnjc.blockgamejournal.journal.JournalEntryBuilder; +import dev.bnjc.blockgamejournal.journal.npc.NPCUtil; +import dev.bnjc.blockgamejournal.util.ItemUtil; +import lombok.Getter; +import lombok.Setter; +import net.minecraft.entity.Entity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.Items; +import org.jetbrains.annotations.NotNull; + +import java.util.ArrayList; +import java.util.List; + +public class RecipeBuilder { + private static final int[] RECIPE_SLOTS = { 12, 13, 14, 21, 22, 23, 30, 31, 32 }; + + private final List ingredients; + + @Getter + private boolean storedRecipe; + + @Setter + private int page; + + private RecipeBuilder() { + this.ingredients = new ArrayList<>(); + this.storedRecipe = false; + this.page = 0; + } + + public void goToNextPage() { + this.page++; + } + + public void goToPreviousPage() { + this.page--; + } + + public void addIngredientsFromPreview(List previewItems) { + // Only store the ingredients if they haven't been stored yet + if (this.ingredients.size() - 1 < this.page * RECIPE_SLOTS.length) { + // Get only the items in the recipe slots + for (int slot : RECIPE_SLOTS) { + ItemStack item = previewItems.get(slot); + if (item.getItem() == Items.AIR) { + continue; + } + + this.ingredients.add(item); + } + } else { + BlockgameJournal.LOGGER.debug("[Blockgame Journal] Ingredients already stored for page {}", page); + } + } + + public void updateKnownIngredients() { + // Add or update all the known ingredients + for (ItemStack stack : this.ingredients) { + String ingredientKey = ItemUtil.getKey(stack); + + // Do not store "minecraft:" items + if (!ingredientKey.startsWith("minecraft:")) { + Journal.INSTANCE.getKnownItems().put(ingredientKey, stack); + } + + BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [ ] {} x{}", ItemUtil.getName(stack), stack.getCount()); + } + } + + public boolean createEntry( + @NotNull ItemStack item, + @NotNull Entity vendor, + @NotNull CraftingStationItem stationItem, + String vendorName + ) { + if (Journal.INSTANCE == null) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] Journal is not loaded"); + return false; + } + + JournalEntry entry = new JournalEntryBuilder(this.ingredients, vendor, stationItem.getSlot()).build(item); + if (!this.validateEntry(stationItem, entry)) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] Recipe validation failed"); + return false; + } + + // Cost + float cost = stationItem.getCost(); + entry.setCost(cost); + if (cost != -1f) { + BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [ ] {} Coins", cost); + } + + // Verify that there are at least some ingredients or a cost + if (this.ingredients.isEmpty() && Float.compare(entry.getCost(), -1f) == 0) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] No ingredients found in the recipe, and no cost was set"); + return false; + } + + // Recipe Known + Boolean recipeKnown = stationItem.getRecipeKnown(); + if (recipeKnown == null) { + entry.setRecipeKnown((byte) -1); + } + else { + entry.setRecipeKnown(recipeKnown ? (byte) 1 : (byte) 0); + Journal.INSTANCE.getMetadata().setKnownRecipe(entry.getKey(), recipeKnown); + BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [{}] Recipe Known", recipeKnown ? "X" : " "); + } + + // Class Requirement + String requiredClass = stationItem.getRequiredClass(); + entry.setRequiredClass(requiredClass); + if (requiredClass != null && !requiredClass.isEmpty()) { + BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [ ] Required Class: {}", requiredClass); + } + + // Required Level + int requiredLevel = stationItem.getRequiredLevel(); + entry.setRequiredLevel(requiredLevel); + if (requiredLevel != -1) { + BlockgameJournal.LOGGER.debug("[Blockgame Journal] - [ ] Required Level: {}", requiredLevel); + } + + // Create or update the vendor + if (!vendorName.isEmpty()) { + NPCUtil.createOrUpdate(vendorName, vendor); + } + + Journal.INSTANCE.addEntry(item, entry); + this.storedRecipe = true; + return true; + } + + private boolean validateEntry(@NotNull CraftingStationItem item, JournalEntry entry) { + // Validate the ingredients of the recipe + BlockgameJournal.LOGGER.debug("[Blockgame Journal] Validating ingredients..."); + + if (Journal.INSTANCE == null) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] Journal is not loaded"); + return false; + } + + // Validate the recipe keys match + if (!ItemUtil.getKey(item.getItem()).equals(entry.getKey())) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] Recipe key mismatch. Expected: {}, Actual: {}", ItemUtil.getKey(item.getItem()), entry.getKey()); + return false; + } + + // Validate the expected ingredients + if (entry.getIngredients().size() != item.getExpectedIngredients().size()) { + BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient count mismatch"); + return false; + } + + // TODO: Fix this matching +// Set ingredientKeys = entry.getIngredients().keySet(); +// for (String key : ingredientKeys) { +// ItemStack stack = Journal.INSTANCE.getKnownItem(key); +// if (stack == null) { +// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient not known: {}", key); +//// return false; +// break; +// } +// +// String itemName = ItemUtil.getName(stack); +// if (!item.getExpectedIngredients().containsKey(itemName)) { +// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient not expected: {}", itemName); +//// return false; +// break; +// } +// +// int expectedAmount = item.getExpectedIngredients().get(itemName); +// int actualAmount = entry.getIngredients().get(key); +// if (expectedAmount != actualAmount) { +// BlockgameJournal.LOGGER.warn("[Blockgame Journal] Ingredient amount mismatch: {} (expected: {}, actual: {})", key, expectedAmount, actualAmount); +//// return false; +// break; +// } +// } + + return true; + } + + public static RecipeBuilder create() { + return new RecipeBuilder(); + } +} diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorHandler.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorHandler.java new file mode 100644 index 0000000..b845a62 --- /dev/null +++ b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorHandler.java @@ -0,0 +1,419 @@ +package dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.vendor; + +import dev.bnjc.blockgamejournal.BlockgameJournal; +import dev.bnjc.blockgamejournal.gamefeature.recipetracker.station.CraftingStationItem; +import dev.bnjc.blockgamejournal.journal.Journal; +import dev.bnjc.blockgamejournal.journal.JournalEntry; +import dev.bnjc.blockgamejournal.journal.metadata.JournalAdvancement; +import dev.bnjc.blockgamejournal.listener.interaction.EntityAttackedListener; +import dev.bnjc.blockgamejournal.listener.interaction.SlotClickedListener; +import dev.bnjc.blockgamejournal.listener.screen.DrawSlotListener; +import dev.bnjc.blockgamejournal.listener.screen.ScreenOpenedListener; +import dev.bnjc.blockgamejournal.listener.screen.ScreenReceivedInventoryListener; +import dev.bnjc.blockgamejournal.util.ItemUtil; +import dev.bnjc.blockgamejournal.util.NbtUtil; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.client.network.ClientPlayerEntity; +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.item.ItemStack; +import net.minecraft.item.KnowledgeBookItem; +import net.minecraft.item.PlayerHeadItem; +import net.minecraft.nbt.NbtList; +import net.minecraft.network.packet.s2c.play.InventoryS2CPacket; +import net.minecraft.network.packet.s2c.play.OpenScreenS2CPacket; +import net.minecraft.screen.slot.Slot; +import net.minecraft.screen.slot.SlotActionType; +import net.minecraft.util.ActionResult; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.*; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class VendorHandler { + private static final Logger LOGGER = BlockgameJournal.getLogger("Vendor Handler"); + + /** Used to track the last preview times for the Carpal Tunnel advancement */ + private static final Queue lastPreviewTimes = new ArrayDeque<>(); + + private final List<@Nullable CraftingStationItem> stationItems; + private RecipeBuilder recipeBuilder; + + private VendorState state = VendorState.INIT; + private int syncId = -1; + + /** + * The entity that the player is interacting with. Ideally, this should be the vendor entity. + */ + @Nullable + private Entity interactionEntity; + private String vendorName = ""; + + @Nullable + private CraftingStationItem lastClickedItem; + + private static final byte STATUS_NONE = 0; + private static final byte STATUS_MISSING = 1; + private static final byte STATUS_OUTDATED = 1 << 1; + private static final byte STATUS_LOCKED = 1 << 2; + private final Map statusCache = new HashMap<>(); + + public VendorHandler() { + this.stationItems = new ArrayList<>(); + this.interactionEntity = null; + + this.recipeBuilder = RecipeBuilder.create(); + } + + public void init() { + EntityAttackedListener.EVENT.register(this::handleEntityAttacked); + ScreenOpenedListener.EVENT.register(this::handleOpenScreen); + ScreenReceivedInventoryListener.EVENT.register(this::handleScreenInventory); + SlotClickedListener.EVENT.register(this::handleSlotClicked); + DrawSlotListener.EVENT.register(this::drawSlot); + } + + private ActionResult handleEntityAttacked(PlayerEntity playerEntity, Entity entity) { + // New interaction, reset the state + this.reset(); + this.interactionEntity = entity; + + return ActionResult.PASS; + } + + private ActionResult handleOpenScreen(OpenScreenS2CPacket packet) { + String screenName = packet.getName().getString(); + + // INIT -> Opened crafting station + // CRAFTING_STATION -> Refreshed crafting station + // RECIPE_PREVIEW -> From recipe preview to crafting station + if (this.state == VendorState.INIT || this.state == VendorState.CRAFTING_STATION || this.state == VendorState.RECIPE_PREVIEW) { + // If the player is not interacting with any entity, we aren't in a vendor interaction screen + if (this.interactionEntity != null) { + String entityName = interactionEntity.getEntityName(); + + // Only use custom name if the entity is not a player + if (!(interactionEntity instanceof PlayerEntity) && interactionEntity.hasCustomName()) { + entityName = interactionEntity.getCustomName().getString(); + } + + // Look for screen name "Some Name (#page#/#max#)" - exclude "Party" from the name + Matcher matcher = Pattern.compile("^((?!Party)[\\w\\s]+)\\s\\(\\d+/\\d+\\)").matcher(screenName); + if (matcher.find() || screenName.equals(entityName)) { + this.syncId = packet.getSyncId(); + this.vendorName = entityName; + this.stationItems.clear(); + this.statusCache.clear(); + this.state = VendorState.CRAFTING_STATION; + this.lastClickedItem = null; + + LOGGER.info("[Blockgame Journal] Opened crafting station for {}", this.vendorName); + + return ActionResult.PASS; + } + } + } + + // CRAFTING_STATION -> From crafting station to recipe preview + // RECIPE_PREVIEW -> Refreshed recipe preview + // LOADING_RECIPE_PAGE -> Loading the next/previous page + if (this.state == VendorState.CRAFTING_STATION || this.state == VendorState.RECIPE_PREVIEW || this.state == VendorState.LOADING_RECIPE_PAGE) { + if (screenName.equals("Recipe Preview")) { + this.syncId = packet.getSyncId(); + + // Reset the recipe page if the screen is not the same as the previous one + if (this.state != VendorState.LOADING_RECIPE_PAGE) { + this.recipeBuilder = RecipeBuilder.create(); + } + + this.state = VendorState.RECIPE_PREVIEW; + return ActionResult.PASS; + } + } + + // Not a valid screen, reset the state + this.reset(); + return ActionResult.PASS; + } + + private ActionResult handleScreenInventory(InventoryS2CPacket packet) { + if (packet.getSyncId() != this.syncId) { + return ActionResult.PASS; + } + + if (state == VendorState.CRAFTING_STATION) { + for (ItemStack item : packet.getContents()) { + if (item == null || item.isEmpty()) { + this.stationItems.add(null); + continue; + } + + CraftingStationItem stationItem = new CraftingStationItem(item, this.stationItems.size()); + NbtList loreTag = NbtUtil.getLore(stationItem.getItem()); + if (loreTag != null) { + VendorUtil.parseStationItemLore(stationItem, loreTag); + } + + this.stationItems.add(stationItem); + } + + VendorUtil.checkForUnavailableItems(this.stationItems, this.vendorName); + } + else if (this.state == VendorState.RECIPE_PREVIEW) { + List previewItems = packet.getContents(); + boolean hasBackButton = previewItems.get(VendorUtil.BACK_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; + boolean hasCraftButton = previewItems.get(VendorUtil.CRAFT_BUTTON_INDEX).getItem() instanceof KnowledgeBookItem; + boolean isScreenValid = hasBackButton && hasCraftButton; + + // Ensure that this is a valid recipe preview screen + if (!isScreenValid) { + return ActionResult.PASS; + } + + this.storeRecipe(previewItems); + } + + return ActionResult.PASS; + } + + private ActionResult handleSlotClicked(int syncId, int slotId, int button, SlotActionType actionType, PlayerEntity player) { + if (syncId != this.syncId) { + return ActionResult.PASS; + } + + if (this.state == VendorState.CRAFTING_STATION) { + if (slotId < 0 || slotId >= this.stationItems.size()) { + LOGGER.warn("[Blockgame Journal] Vendor handler slot out of bounds: {}", slotId); + return ActionResult.PASS; + } + + CraftingStationItem item = this.stationItems.get(slotId); + if (item == null || item.getItem().isEmpty()) { + LOGGER.warn("[Blockgame Journal] Empty item clicked in slot: {}", slotId); + this.lastClickedItem = null; + return ActionResult.PASS; + } + + this.lastClickedItem = item.copy(); + } + else if (this.state == VendorState.RECIPE_PREVIEW) { + if (slotId == VendorUtil.PREV_PAGE_BUTTON_INDEX) { + this.state = VendorState.LOADING_RECIPE_PAGE; + this.recipeBuilder.goToPreviousPage(); + } else if (slotId == VendorUtil.NEXT_PAGE_BUTTON_INDEX) { + this.state = VendorState.LOADING_RECIPE_PAGE; + this.recipeBuilder.goToNextPage(); + } + } + + return ActionResult.PASS; + } + + private void drawSlot(DrawContext context, Slot slot) { + // Verify we are in the correct state + if (this.syncId == -1 || Journal.INSTANCE == null) { + return; + } + + if (this.state == VendorState.CRAFTING_STATION && !this.stationItems.isEmpty()) { + // Verify the player is still in the same screen + ClientPlayerEntity player = MinecraftClient.getInstance().player; + if (player != null && player.currentScreenHandler.syncId != this.syncId) { + this.reset(); + return; + } + + // Check for configuration settings + boolean highlightMissing = BlockgameJournal.getConfig().getGeneralConfig().highlightMissingRecipes; + boolean highlightOutdated = BlockgameJournal.getConfig().getGeneralConfig().highlightOutdatedRecipes; + boolean showRecipeLock = BlockgameJournal.getConfig().getGeneralConfig().showRecipeLock; + if (!highlightMissing && !highlightOutdated && !showRecipeLock) { + return; + } + + // Check if slot is within bounds (0-53) + if (slot.id < 0 || slot.id >= this.stationItems.size() || slot.id >= 54) { + return; + } + + // Use the cache for drawing so we don't have to recheck every frame + Byte status = this.statusCache.get(slot.id); + if (status != null) { + if ((status & STATUS_MISSING) != 0) { + VendorUtil.highlightSlot(context, slot, 0x30FF0000); + } else if ((status & STATUS_OUTDATED) != 0) { + VendorUtil.highlightSlot(context, slot, 0x40CCCC00); + } + + if ((status & STATUS_LOCKED) != 0) { + VendorUtil.drawLocked(context, slot); + } + return; + } + + // See if slot item matches inventory item + ItemStack slotItem = slot.getStack(); + CraftingStationItem stationItem = this.stationItems.get(slot.id); + if (slotItem == null || stationItem == null || slotItem.isEmpty()) { + return; + } + + if (slotItem.getItem() != stationItem.getItem().getItem()) { + LOGGER.warn("[Blockgame Journal] Slot item does not match inventory item"); + return; + } + + boolean recipeNotKnown = Boolean.FALSE.equals(stationItem.getRecipeKnown()); + boolean profRequirementNotMet = false; + if (stationItem.getRequiredLevel() != -1) { + int currLevel = Journal.INSTANCE.getMetadata().getProfessionLevels().getOrDefault(stationItem.getRequiredClass(), -1); + if (currLevel != -1 && currLevel < stationItem.getRequiredLevel()) { + profRequirementNotMet = true; + } + } + + if ((recipeNotKnown || profRequirementNotMet) && showRecipeLock) { + VendorUtil.drawLocked(context, slot); + this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_LOCKED : (byte) (v | STATUS_LOCKED)); + } + + List entries = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(stationItem.getItem()), new ArrayList<>()); + for (JournalEntry entry : entries) { + String expectedNpcName = entry.getNpcName(); + int expectedSlot = entry.getSlot(); + + // If the item is in the journal, don't highlight it unless it's outdated + if (this.vendorName.equals(expectedNpcName) && slot.id == expectedSlot) { + stationItem.setOutdated(ItemUtil.isOutdated(entry, stationItem)); + if (highlightOutdated && stationItem.getOutdated()) { + VendorUtil.highlightSlot(context, slot, 0x40CCCC00); + this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_OUTDATED : (byte) (v | STATUS_OUTDATED)); + } else { + // Negate the status if it's not outdated + this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_NONE : (byte) (v & ~STATUS_OUTDATED)); + } + return; + } + } + + // If the item is not in the journal, highlight it + if (highlightMissing) { + VendorUtil.highlightSlot(context, slot, 0x30FF0000); + this.statusCache.compute(slot.id, (k, v) -> v == null ? STATUS_MISSING : (byte) (v | STATUS_MISSING)); + } + } + else if (this.state == VendorState.RECIPE_PREVIEW) { + // Highlight + if (slot.id == VendorUtil.NEXT_PAGE_BUTTON_INDEX && !this.recipeBuilder.isStoredRecipe()) { + ItemStack item = slot.getStack(); + if (item.getItem() instanceof PlayerHeadItem) { + context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0x30FF0000); + context.drawBorder(slot.x, slot.y, 16, 16, 0xBBFF0000); + context.getMatrices().push(); + context.getMatrices().translate(0.0f, 0.0f, 200.0f); + context.drawText( + MinecraftClient.getInstance().textRenderer, + "●", + slot.x + 18 - MinecraftClient.getInstance().textRenderer.getWidth("●"), + slot.y - 3, + 0xFF3333, + true + ); + context.getMatrices().pop(); + } + } + } + } + + private void reset() { + this.state = VendorState.INIT; + this.syncId = -1; + + // Entity interaction + this.lastClickedItem = null; + this.interactionEntity = null; + + // Crafting station + this.vendorName = ""; + this.stationItems.clear(); + this.statusCache.clear(); + + // Recipe preview + this.recipeBuilder = RecipeBuilder.create(); + } + + private void storeRecipe(List previewItems) { + if (Journal.INSTANCE == null) { + LOGGER.warn("[Blockgame Journal] Journal is not loaded"); + return; + } + + if (this.interactionEntity == null) { + LOGGER.warn("[Blockgame Journal] No station entity to attribute the recipe to"); + return; + } + + // Validate we came from clicking the item + if (this.lastClickedItem == null || this.lastClickedItem.getItem().isEmpty()) { + LOGGER.warn("[Blockgame Journal] No last clicked item found"); + if (this.lastClickedItem != null) { + LOGGER.warn("[Blockgame Journal] Last clicked item: {}", ItemUtil.getName(this.lastClickedItem.getItem())); + } + return; + } + + boolean hasPrevPageButton = previewItems.get(VendorUtil.PREV_PAGE_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; + boolean hasNextPageButton = previewItems.get(VendorUtil.NEXT_PAGE_BUTTON_INDEX).getItem() instanceof PlayerHeadItem; + + if (!hasPrevPageButton) { + this.recipeBuilder.setPage(0); + } + + ItemStack recipeItem = previewItems.get(VendorUtil.ITEM_INDEX); + if (recipeItem == null || recipeItem.isEmpty()) { + LOGGER.warn("[Blockgame Journal] Recipe item is empty, cannot store recipe. Try again later."); + return; + } + + this.recipeBuilder.addIngredientsFromPreview(previewItems); + + if (hasNextPageButton) { + LOGGER.info("[Blockgame Journal] Waiting for next page to store recipe"); + return; + } + + // Store the recipe in the player's journal + LOGGER.debug("[Blockgame Journal] Storing recipe for {}", ItemUtil.getName(recipeItem)); + this.recipeBuilder.updateKnownIngredients(); + if (this.recipeBuilder.createEntry(recipeItem, this.interactionEntity, this.lastClickedItem, this.vendorName)) { + this.checkForAdvancement(); + } + } + + private void checkForAdvancement() { + if (Journal.INSTANCE == null || Journal.INSTANCE.getMetadata().hasAdvancement(JournalAdvancement.CARPAL_TUNNEL)) { + return; + } + + lastPreviewTimes.add(System.currentTimeMillis()); + + if (lastPreviewTimes.size() >= 25) { + long firstTime = lastPreviewTimes.poll(); + + // If it has been less than a minute since the first preview, grant the advancement + if (System.currentTimeMillis() - firstTime <= 60000) { + JournalAdvancement.CARPAL_TUNNEL.grant(); + } + } + } + + enum VendorState { + INIT, + CRAFTING_STATION, + RECIPE_PREVIEW, + LOADING_RECIPE_PAGE, + } +} diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorUtil.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorUtil.java new file mode 100644 index 0000000..4d244d7 --- /dev/null +++ b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/handlers/vendor/VendorUtil.java @@ -0,0 +1,152 @@ +package dev.bnjc.blockgamejournal.gamefeature.recipetracker.handlers.vendor; + +import dev.bnjc.blockgamejournal.BlockgameJournal; +import dev.bnjc.blockgamejournal.gamefeature.recipetracker.station.CraftingStationItem; +import dev.bnjc.blockgamejournal.journal.Journal; +import dev.bnjc.blockgamejournal.journal.JournalEntry; +import dev.bnjc.blockgamejournal.util.GuiUtil; +import dev.bnjc.blockgamejournal.util.ItemUtil; +import dev.bnjc.blockgamejournal.util.NbtUtil; +import net.minecraft.client.gui.DrawContext; +import net.minecraft.nbt.NbtList; +import net.minecraft.screen.slot.Slot; +import net.minecraft.text.MutableText; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class VendorUtil { + public static final int BACK_BUTTON_INDEX = 10; + public static final int ITEM_INDEX = 16; + public static final int PREV_PAGE_BUTTON_INDEX = 20; + public static final int NEXT_PAGE_BUTTON_INDEX = 24; + public static final int CRAFT_BUTTON_INDEX = 28; + public static final int CONFIRM_BUTTON_INDEX = 34; + + private static final Logger LOGGER = BlockgameJournal.getLogger("Vendor Util"); + + private static final Pattern COIN_PATTERN = Pattern.compile("([✔✖]) Requires (\\d+(?:\\.\\d+)?) Coin"); + private static final Pattern KNOWLEDGE_PATTERN = Pattern.compile("([✔✖]) Recipe (Known|Learned)"); + private static final Pattern CLASS_PATTERN = Pattern.compile("([✔✖]) Requires (\\d+) in ([A-Za-z]+)"); + private static final Pattern INGREDIENT_HEADER_PATTERN = Pattern.compile("Ingredients:"); + private static final Pattern INGREDIENT_PATTERN = Pattern.compile("([✔✖]) (\\d+) (.+?)$"); + + private VendorUtil() { + throw new UnsupportedOperationException("This is a utility class and cannot be instantiated"); + } + + /** + * Parses the metadata from the clicked item's lore. This includes the coin cost, recipe known status, class + * requirement, and expected ingredients. + */ + public static void parseStationItemLore(@Nullable CraftingStationItem item, NbtList loreTag) { + if (item == null) { + LOGGER.warn("[Blockgame Journal] No item found for lore"); + return; + } + + if (loreTag == null) { + LOGGER.warn("[Blockgame Journal] No lore tag found"); + return; + } + + boolean listingIngredients = false; + for (int i = 0; i < loreTag.size(); i++) { + String lore = loreTag.getString(i); + MutableText text = NbtUtil.parseLoreLine(lore); + if (text == null) { + LOGGER.warn("[Blockgame Journal] Failed to parse lore line: {}", lore); + continue; + } + + lore = text.getString(); + + if (listingIngredients) { + Matcher ingredientMatcher = INGREDIENT_PATTERN.matcher(lore); + if (ingredientMatcher.find()) { + String name = ingredientMatcher.group(3); + item.addExpectedIngredient(name, Integer.parseInt(ingredientMatcher.group(2))); + continue; + } + + if (lore.isBlank()) { + // If we find an empty text, we might have more ingredients on the next line + continue; + } + + // If we didn't match an ingredient, we're done listing them + listingIngredients = false; + } else { + // Check for coin cost + Matcher coinMatcher = COIN_PATTERN.matcher(lore); + if (coinMatcher.find()) { + item.setCost(Float.parseFloat(coinMatcher.group(2))); + continue; + } + + // Check for recipe known + Matcher knowledgeMatcher = KNOWLEDGE_PATTERN.matcher(lore); + if (knowledgeMatcher.find()) { + item.setRecipeKnown("✔".equals(knowledgeMatcher.group(1))); + continue; + } + + // Check for class requirement + Matcher classMatcher = CLASS_PATTERN.matcher(lore); + if (classMatcher.find()) { + item.setRequiredLevel(Integer.parseInt(classMatcher.group(2))); + item.setRequiredClass(classMatcher.group(3)); + continue; + } + + // Check for ingredients header + Matcher ingredientMatcher = INGREDIENT_HEADER_PATTERN.matcher(lore); + if (ingredientMatcher.find()) { + listingIngredients = true; + continue; + } + } + + LOGGER.debug("[Blockgame Journal] Unrecognized lore: {}", lore); + } + } + + public static void checkForUnavailableItems(List stationItems, String vendorName) { + if (Journal.INSTANCE == null) { + return; + } + + for (JournalEntry journalEntry : Journal.INSTANCE.getEntriesForVendor(vendorName)) { + boolean found = false; + + // See if there is a matching item in the inventory in the same slot + for (CraftingStationItem stationItem : stationItems) { + if (stationItem == null) { + continue; + } + + if (ItemUtil.getKey(stationItem.getItem()).equals(journalEntry.getKey())) { + found = true; + break; + } + } + + journalEntry.setUnavailable(!found); + } + } + + public static void highlightSlot(DrawContext context, Slot slot, int color) { + context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, color); + context.drawBorder(slot.x, slot.y, 16, 16, color | 0xBB000000); + } + + public static void drawLocked(DrawContext context, Slot slot) { + context.getMatrices().push(); + context.getMatrices().translate(0, 0, 150); + context.drawGuiTexture(GuiUtil.sprite("lock_icon"), slot.x + 10, slot.y - 2, 150, 8, 8); + context.getMatrices().pop(); + } +} diff --git a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/station/CraftingStationItem.java b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/station/CraftingStationItem.java index 6c1432a..188fb48 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/station/CraftingStationItem.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gamefeature/recipetracker/station/CraftingStationItem.java @@ -40,7 +40,7 @@ public class CraftingStationItem { private final Map expectedIngredients; public CraftingStationItem(ItemStack item, int slot) { - this.item = item; + this.item = item.copy(); this.slot = slot; this.expectedIngredients = new HashMap<>(); } @@ -48,4 +48,15 @@ public CraftingStationItem(ItemStack item, int slot) { public void addExpectedIngredient(String ingredient, int amount) { this.expectedIngredients.put(ingredient, amount); } + + public CraftingStationItem copy() { + CraftingStationItem copy = new CraftingStationItem(this.item, this.slot); + copy.setRecipeKnown(this.recipeKnown); + copy.setCost(this.cost); + copy.setRequiredClass(this.requiredClass); + copy.setRequiredLevel(this.requiredLevel); + copy.setOutdated(this.outdated); + this.expectedIngredients.forEach(copy::addExpectedIngredient); + return copy; + } } diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/DecompositionScreen.java b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/DecompositionScreen.java index 12a9eda..487372b 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/DecompositionScreen.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/DecompositionScreen.java @@ -137,12 +137,7 @@ private void renderEntryItem(DrawContext context) { context.drawItem(item, x, y); // Item count (in bottom right corner of item) - if (this.entry.getCount() > 1) { - context.getMatrices().push(); - context.getMatrices().translate(0.0f, 0.0f, 200.0f); - context.drawText(textRenderer, Text.literal("" + this.entry.getCount()).formatted(Formatting.WHITE), x + 8, y + 8, 0x404040, true); - context.getMatrices().pop(); - } + ItemUtil.renderItemCount(context, x, y, this.entry.getCount()); // Title MutableText title = Text.literal(ItemUtil.getName(item)).formatted(Formatting.BOLD, Formatting.WHITE); diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/JournalScreen.java b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/JournalScreen.java index f042dcb..d7ffbe8 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/JournalScreen.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/JournalScreen.java @@ -125,7 +125,131 @@ protected void init() { super.init(); - // Known Recipes + this.initLearnedRecipesButton(); + this.initTrackingWidget(); + this.initItemListWidget(); + this.initSearchWidget(); + this.initCloseButton(); + this.initItemSortButton(); + this.initInventoryToggleButton(); + this.initUnlockedToggleButton(); + this.initVendorSortButton(); + this.initVendorWidget(); + this.initModeButtons(); + } + + @Override + public void render(DrawContext context, int mouseX, int mouseY, float delta) { + super.render(context, mouseX, mouseY, delta); + + // Title + Text titleText = this.title; + if (JournalScreen.selectedNpc != null) { + titleText = Text.translatable("blockgamejournal.recipe_journal.npc", JournalScreen.selectedNpc.getNpcName().name()); + } else if (JournalScreen.selectedIngredient != null) { + titleText = Text.translatable("blockgamejournal.recipe_journal.ingredient", ItemUtil.getName(JournalScreen.selectedIngredient)); + } else if (this.currentMode == JournalMode.Type.FAVORITES) { + titleText = Text.translatable("blockgamejournal.recipe_journal.favorites"); + } else if (this.currentMode == JournalMode.Type.NPC_SEARCH) { + titleText = Text.translatable("blockgamejournal.recipe_journal.by_npc"); + } else if (this.currentMode == JournalMode.Type.INGREDIENT_SEARCH) { + titleText = Text.translatable("blockgamejournal.recipe_journal.by_ingredient"); + } + context.drawTextWrapped( + textRenderer, + titleText, + this.left + TITLE_LEFT, + this.top + TITLE_TOP, + MENU_WIDTH - (TITLE_LEFT * 2) - (2 * (BUTTON_SIZE - 2)), + 0x404040 + ); + } + + @Override + public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { + super.renderBackground(context, mouseX, mouseY, delta); + + // Background + context.drawGuiTexture(BACKGROUND_SPRITE, this.left, this.top, MENU_WIDTH, MENU_HEIGHT); + } + + @Override + public void close() { + JournalScreen.lastSearch = null; // Clear search + JournalScreen.selectedNpc = null; // Clear selected NPC + JournalScreen.selectedIngredient = null; // Clear selected ingredient + super.close(); + } + + public void setSelectedNpc(String npc) { + if (npc == null) { + JournalScreen.selectedNpc = null; + } else { + JournalScreen.selectedNpc = new NPCEntity(MinecraftClient.getInstance().world, npc); + } + this.npcWidget.setEntity(JournalScreen.selectedNpc); + + this.search.setText(""); + JournalScreen.lastSearch = ""; + + this.updateItems(null); + this.closeButton.visible = npc != null; + this.vendorSortButton.visible = npc != null; + this.refreshUnlockToggleButton(); + } + + public void setSelectedIngredient(ItemStack ingredient) { + JournalScreen.selectedIngredient = ingredient; + + this.search.setText(""); + JournalScreen.lastSearch = ""; + + this.updateItems(null); + this.closeButton.visible = ingredient != null; + this.refreshUnlockToggleButton(); + } + + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + boolean handled = this.itemList.keyPressed(keyCode, scanCode, modifiers); + + if (!handled) { + handled = super.keyPressed(keyCode, scanCode, modifiers); + } + + return handled; + } + + @Override + public boolean charTyped(char chr, int modifiers) { + boolean handled = this.itemList.charTyped(chr, modifiers); + + if (!handled) { + handled = super.charTyped(chr, modifiers); + } + + return handled; + } + + public void refreshItems() { + this.updateItems(this.search.getText()); + } + + public void refreshTracking() { + if (Journal.INSTANCE == null) { + return; + } + + this.trackingWidget.setEntries(Journal.INSTANCE.getEntries().values().stream() + .flatMap(List::stream) + .filter(JournalEntry::isTracked) + .toList() + ); + } + + // region Init Helpers + + private void initLearnedRecipesButton() { TexturedButtonWidget learnedRecipesButton = new TexturedButtonWidget( this.width - 24, 8, @@ -138,8 +262,9 @@ protected void init() { ); learnedRecipesButton.setTooltip(Tooltip.of(Text.literal("View Learned Recipes"))); this.addDrawableChild(learnedRecipesButton); + } - // Tracking + private void initTrackingWidget() { this.trackingWidget = new TrackingWidget( this, 0, @@ -149,7 +274,9 @@ protected void init() { ); this.addDrawableChild(this.trackingWidget); this.refreshTracking(); + } + private void initItemListWidget() { // Items this.itemList = new ItemListWidget(this, left + GRID_LEFT, top + GRID_TOP, GRID_COLUMNS, GRID_ROWS); @@ -158,13 +285,16 @@ protected void init() { left + MENU_WIDTH - 19, top + GRID_TOP, this.itemList.getHeight(), - Text.empty() + Text.empty(), + () -> this.itemList.getRows() - 5 )); this.scroll.setResponder(this.itemList::onScroll); - this.addDrawableChild(this.itemList); - // Search + this.itemList.setScroll(this.scroll); + } + + private void initSearchWidget() { boolean shouldFocusSearch = this.search == null || this.search.isFocused(); // shouldFocusSearch &= config.autofocusSearch; this.search = this.addDrawableChild(new SearchWidget( @@ -194,8 +324,9 @@ protected void init() { this.search.setText(I18n.translate("blockgamejournal.recipe_journal.no_journal")); this.search.setUneditableColor(0xFF4040); } + } - // Close button + private void initCloseButton() { this.closeButton = GuiUtil.close( this.left + MENU_WIDTH - (3 + BUTTON_SIZE), this.top + 5, @@ -207,8 +338,9 @@ protected void init() { ); this.closeButton.visible = JournalScreen.selectedNpc != null || JournalScreen.selectedIngredient != null; this.addDrawableChild(this.closeButton); + } - // Item sort button + private void initItemSortButton() { this.itemSortButton = new TexturedButtonWidget( this.left + MENU_WIDTH - (3 + BUTTON_SIZE), this.top + 5, @@ -235,7 +367,9 @@ protected void init() { this.itemSortButton.setTooltip(Tooltip.of(Text.translatable("blockgamejournal.sort." + JournalScreen.itemSort.name()))); this.itemSortButton.visible = this.currentMode == JournalMode.Type.ITEM_SEARCH || this.currentMode == JournalMode.Type.FAVORITES; this.addDrawableChild(this.itemSortButton); + } + private void initInventoryToggleButton() { // Inventory toggle on button this.inventoryToggleOnButton = new TexturedButtonWidget( this.left + MENU_WIDTH - 2 * (3 + BUTTON_SIZE), @@ -271,8 +405,9 @@ protected void init() { this.inventoryToggleOffButton.setTooltip(Tooltip.of(Text.translatable("blockgamejournal.filter.inventory.true"))); this.inventoryToggleOffButton.visible = (this.currentMode == JournalMode.Type.ITEM_SEARCH || this.currentMode == JournalMode.Type.FAVORITES) && JournalScreen.useInventory; this.addDrawableChild(this.inventoryToggleOffButton); + } - // Unlocked Toggle Button + private void initUnlockedToggleButton() { this.unlockedToggleButton = new UnlockedButtonWidget( this.left + MENU_WIDTH - 3 * (3 + BUTTON_SIZE), this.top + 5, @@ -282,8 +417,9 @@ protected void init() { ); this.refreshUnlockToggleButton(); this.addDrawableChild(this.unlockedToggleButton); + } - // Vendor sort button + private void initVendorSortButton() { this.vendorSortButton = new TexturedButtonWidget( this.left + MENU_WIDTH - 2 * (3 + BUTTON_SIZE), this.top + 5, @@ -299,12 +435,14 @@ protected void init() { this.vendorSortButton.setTooltip(Tooltip.of(Text.translatable("blockgamejournal.sort." + JournalScreen.vendorItemSort.name()))); this.vendorSortButton.visible = JournalScreen.selectedNpc != null; this.addDrawableChild(this.vendorSortButton); + } - // NPC Widget + private void initVendorWidget() { this.npcWidget = new NPCWidget(JournalScreen.selectedNpc, this.left + MENU_WIDTH + 4, this.top, 68, 74); this.addDrawableChild(this.npcWidget); + } - ///// Mode Buttons + private void initModeButtons() { Map buttons = new HashMap<>(); var modes = JournalMode.MODES.values().stream() @@ -353,114 +491,7 @@ protected void init() { } } - @Override - public void render(DrawContext context, int mouseX, int mouseY, float delta) { - super.render(context, mouseX, mouseY, delta); - - // Title - Text titleText = this.title; - if (JournalScreen.selectedNpc != null) { - titleText = Text.translatable("blockgamejournal.recipe_journal.npc", JournalScreen.selectedNpc.getNpcName().name()); - } else if (JournalScreen.selectedIngredient != null) { - titleText = Text.translatable("blockgamejournal.recipe_journal.ingredient", ItemUtil.getName(JournalScreen.selectedIngredient)); - } else if (this.currentMode == JournalMode.Type.FAVORITES) { - titleText = Text.translatable("blockgamejournal.recipe_journal.favorites"); - } else if (this.currentMode == JournalMode.Type.NPC_SEARCH) { - titleText = Text.translatable("blockgamejournal.recipe_journal.by_npc"); - } else if (this.currentMode == JournalMode.Type.INGREDIENT_SEARCH) { - titleText = Text.translatable("blockgamejournal.recipe_journal.by_ingredient"); - } - context.drawTextWrapped( - textRenderer, - titleText, - this.left + TITLE_LEFT, - this.top + TITLE_TOP, - MENU_WIDTH - (TITLE_LEFT * 2) - (2 * (BUTTON_SIZE - 2)), - 0x404040 - ); - } - - @Override - public void renderBackground(DrawContext context, int mouseX, int mouseY, float delta) { - super.renderBackground(context, mouseX, mouseY, delta); - - // Background - context.drawGuiTexture(BACKGROUND_SPRITE, this.left, this.top, MENU_WIDTH, MENU_HEIGHT); - } - - @Override - public void close() { - JournalScreen.lastSearch = null; // Clear search - JournalScreen.selectedNpc = null; // Clear selected NPC - JournalScreen.selectedIngredient = null; // Clear selected ingredient - super.close(); - } - - public void setSelectedNpc(String npc) { - if (npc == null) { - JournalScreen.selectedNpc = null; - } else { - JournalScreen.selectedNpc = new NPCEntity(MinecraftClient.getInstance().world, npc); - } - this.npcWidget.setEntity(JournalScreen.selectedNpc); - - this.search.setText(""); - JournalScreen.lastSearch = ""; - - this.updateItems(null); - this.closeButton.visible = npc != null; - this.vendorSortButton.visible = npc != null; - this.refreshUnlockToggleButton(); - } - - public void setSelectedIngredient(ItemStack ingredient) { - JournalScreen.selectedIngredient = ingredient; - - this.search.setText(""); - JournalScreen.lastSearch = ""; - - this.updateItems(null); - this.closeButton.visible = ingredient != null; - this.refreshUnlockToggleButton(); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - boolean handled = this.itemList.keyPressed(keyCode, scanCode, modifiers); - - if (!handled) { - handled = super.keyPressed(keyCode, scanCode, modifiers); - } - - return handled; - } - - @Override - public boolean charTyped(char chr, int modifiers) { - boolean handled = this.itemList.charTyped(chr, modifiers); - - if (!handled) { - handled = super.charTyped(chr, modifiers); - } - - return handled; - } - - public void refreshItems() { - this.updateItems(this.search.getText()); - } - - public void refreshTracking() { - if (Journal.INSTANCE == null) { - return; - } - - this.trackingWidget.setEntries(Journal.INSTANCE.getEntries().values().stream() - .flatMap(List::stream) - .filter(JournalEntry::isTracked) - .toList() - ); - } + // endregion Init Helpers private void updateItems(String filter) { if (Journal.INSTANCE == null) { @@ -630,7 +661,9 @@ private void filter(@Nullable String filter) { List filtered; if (filter == null || filter.isEmpty()) { - filtered = this.items; + filtered = this.items.stream() + .filter(item -> !item.getStack().isEmpty()) // Filter out empty items + .toList(); } else { filtered = this.items.stream() .filter(item -> SearchUtil.defaultPredicate(item.getStack(), filter) && !item.getStack().isEmpty()) diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/TrackingScreen.java b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/TrackingScreen.java index 75e5bc1..bd62446 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/screen/TrackingScreen.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/screen/TrackingScreen.java @@ -39,6 +39,8 @@ protected void init() { ); this.addDrawableChild(this.trackingWidget); this.refreshTracking(); + + this.trackingWidget.setFocused(true); } public void refreshTracking() { diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/ItemListWidget.java b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/ItemListWidget.java index 7c8c765..9a6ee82 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/ItemListWidget.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/ItemListWidget.java @@ -40,9 +40,13 @@ public class ItemListWidget extends ClickableWidget { private final Screen parent; private final int gridWidth; private final int gridHeight; + private List items = Collections.emptyList(); private int offset = 0; + @Setter + private VerticalScrollWidget scroll; + private @Nullable ItemStack hoveredItem = null; @Setter @@ -81,6 +85,15 @@ public void onScroll(float progress) { this.offset = rowOffset * this.gridWidth; } + @Override + public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { + if (this.scroll == null) { + return false; + } + + return this.scroll.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); + } + @Override protected boolean isValidClickButton(int button) { this.lastButton = button; @@ -112,7 +125,7 @@ public void onClick(double mouseX, double mouseY) { } BlockgameJournal.LOGGER.debug("Clicked item: {}, Slot: {}", item, index); - if (item == null) { + if (item == null || Journal.INSTANCE == null) { return; } @@ -122,13 +135,57 @@ public void onClick(double mouseX, double mouseY) { (this.mode == JournalMode.Type.NPC_SEARCH && JournalScreen.getSelectedNpc() != null) || (this.mode == JournalMode.Type.INGREDIENT_SEARCH && JournalScreen.getSelectedIngredient() != null)) { - @Nullable JournalEntry entry = Journal.INSTANCE.getFirstJournalEntry(ItemUtil.getKey(item)); + @Nullable JournalEntry entry = null; + + if (this.mode == JournalMode.Type.ITEM_SEARCH) { + // Just get the first entry + entry = Journal.INSTANCE.getFirstJournalEntry(ItemUtil.getKey(item)); + } + else if (this.mode == JournalMode.Type.FAVORITES) { + // Get the first entry that is favorited + entry = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(item), new ArrayList<>()) + .stream() + .filter(JournalEntry::isFavorite) + .findFirst() + .orElse(null); + } + else if (this.mode == JournalMode.Type.NPC_SEARCH) { + // Get the first entry that matches the selected NPC + entry = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(item), new ArrayList<>()) + .stream() + .filter(e -> { + if (JournalScreen.getSelectedNpc() == null) { + return true; + } + + boolean npcMatch = e.getNpcName().equals(JournalScreen.getSelectedNpc().getNpcWorldName()); + if (useSlotPositions()) { + return e.getSlot() == index && npcMatch; + } + return npcMatch; + }) + .findFirst() + .orElse(null); + } + else if (this.mode == JournalMode.Type.INGREDIENT_SEARCH) { + // Get the first entry that matches the selected ingredient + entry = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(item), new ArrayList<>()) + .stream() + .filter(e -> { + if (JournalScreen.getSelectedIngredient() == null) { + return true; + } + + return e.getIngredients().containsKey(ItemUtil.getKey(JournalScreen.getSelectedIngredient())); + }) + .findFirst() + .orElse(null); + } + if (entry != null) { entry.setTracked(!entry.isTracked()); JournalScreen js = getJournalScreen(); - if (js != null) { - js.refreshTracking(); - } + if (js != null) js.refreshTracking(); return; } } @@ -313,14 +370,15 @@ private void renderItems(DrawContext context) { for (JournalItemStack item : items) { int x = this.getX() + GRID_SLOT_SIZE * (item.getSlot() % this.gridWidth); int y = this.getY() + GRID_SLOT_SIZE * (item.getSlot() / this.gridWidth); - this.renderItem(context, item.getStack(), x, y); + this.renderItem(context, item.getStack(), x, y, item.getSlot()); } } else { for (int i = 0; i < (this.gridWidth * this.gridHeight); i++) { int x = this.getX() + GRID_SLOT_SIZE * (i % this.gridWidth); int y = this.getY() + GRID_SLOT_SIZE * (i / this.gridWidth); if (i < items.size()) { - this.renderItem(context, items.get(i).getStack(), x, y); + JournalItemStack item = items.get(i); + this.renderItem(context, item.getStack(), x, y, item.getSlot()); } } } @@ -346,8 +404,12 @@ private void renderNoItemsMessage(DrawContext context) { context.drawText(MinecraftClient.getInstance().textRenderer, message, x, y, 0x000000, false); } - private void renderItem(DrawContext context, ItemStack item, int x, int y) { - if (this.mode == JournalMode.Type.NPC_SEARCH && Journal.INSTANCE != null) { + private void renderItem(DrawContext context, ItemStack item, int x, int y, int slot) { + if (Journal.INSTANCE == null) { + return; + } + + if (this.mode == JournalMode.Type.NPC_SEARCH) { if (item.getItem() instanceof PlayerHeadItem || item.getItem() instanceof SpawnEggItem) { String npcName = item.getNbt().getString(Journal.NPC_NAME_KEY); Journal.INSTANCE.getKnownNpc(npcName).ifPresent(npc -> { @@ -357,39 +419,8 @@ private void renderItem(DrawContext context, ItemStack item, int x, int y) { }); } } - else if (this.mode == JournalMode.Type.ITEM_SEARCH || this.mode == JournalMode.Type.FAVORITES) { - if (Journal.INSTANCE != null && Journal.INSTANCE.hasJournalEntry(item)) { - List entries = Journal.INSTANCE.getEntries().getOrDefault(ItemUtil.getKey(item), new ArrayList<>()); - for (JournalEntry entry : entries) { - if (entry.isTracked()) { - context.fill(x + 1, y + 1, x + GRID_SLOT_SIZE - 1, y + GRID_SLOT_SIZE - 1, 0x80_00AA00); - break; - } - } - - // Check for locked recipes - boolean recipeKnown = true; - boolean profReqMet = true; - for (JournalEntry entry : entries) { - if (!entry.isRecipeKnown()) { - recipeKnown = false; - } - - if (!entry.meetsProfessionRequirements()) { - profReqMet = false; - } - } - if (!(recipeKnown && profReqMet) && BlockgameJournal.getConfig().getGeneralConfig().showRecipeLock) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 150); - context.drawGuiTexture(GuiUtil.sprite("lock_icon"), x + 10, y - 2, 150, 8, 8); - context.getMatrices().pop(); - } - } - } - - if (Journal.INSTANCE != null && Journal.INSTANCE.hasJournalEntry(item)) { + if (Journal.INSTANCE.hasJournalEntry(item)) { List entries = Journal.INSTANCE.getEntries() .getOrDefault(ItemUtil.getKey(item), new ArrayList<>()) .stream().filter((entry) -> { @@ -405,7 +436,12 @@ else if (this.mode == JournalMode.Type.ITEM_SEARCH || this.mode == JournalMode.T boolean recipeKnown = false; boolean profReqMet = false; boolean unavailable = true; + boolean tracked = false; for (JournalEntry entry : entries) { + if (entry.isTracked() && !(useSlotPositions() && entry.getSlot() != slot)) { + tracked = true; + } + Boolean erk = entry.recipeKnown(); if (erk == null || Boolean.TRUE.equals(erk)) { recipeKnown = true; @@ -425,6 +461,10 @@ else if (this.mode == JournalMode.Type.ITEM_SEARCH || this.mode == JournalMode.T } } + if (tracked) { + context.fill(x + 1, y + 1, x + GRID_SLOT_SIZE - 1, y + GRID_SLOT_SIZE - 1, 0x80_00AA00); + } + if (!(recipeKnown && profReqMet) && BlockgameJournal.getConfig().getGeneralConfig().showRecipeLock) { context.getMatrices().push(); context.getMatrices().translate(0, 0, 150); @@ -448,14 +488,7 @@ else if (entries.size() == 1) { count = entries.get(0).getCount(); } - if (count > 1) { - context.getMatrices().push(); - context.getMatrices().translate(0.0f, 0.0f, 200.0f); - - Text text = Text.literal("" + count).formatted(Formatting.WHITE); - context.drawText(MinecraftClient.getInstance().textRenderer, text, x + 19 - 2 - MinecraftClient.getInstance().textRenderer.getWidth(text), y + 6 + 3, 0xFFFFFF, true); - context.getMatrices().pop(); - } + ItemUtil.renderItemCount(context, x, y, count); } context.drawItem(item, x + 1, y + 1); diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/RecipeWidget.java b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/RecipeWidget.java index 732cc63..a5b4be4 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/RecipeWidget.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/RecipeWidget.java @@ -239,12 +239,7 @@ private void renderRecipeItem(DrawContext context, int mouseX, int mouseY) { } // Item count (in bottom right corner of item) - if (this.entry.getCount() > 1) { - context.getMatrices().push(); - context.getMatrices().translate(0.0f, 0.0f, 200.0f); - context.drawText(textRenderer, Text.literal("" + this.entry.getCount()).formatted(Formatting.WHITE), x + 8, y + 8, 0x404040, true); - context.getMatrices().pop(); - } + ItemUtil.renderItemCount(context, x, y, this.entry.getCount()); // Title MutableText title = Text.literal(ItemUtil.getName(item)).formatted(Formatting.BOLD, Formatting.WHITE); diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/TrackingWidget.java b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/TrackingWidget.java index 0afbcb2..6339b53 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/TrackingWidget.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/TrackingWidget.java @@ -1,5 +1,6 @@ package dev.bnjc.blockgamejournal.gui.widget; +import dev.bnjc.blockgamejournal.BlockgameJournal; import dev.bnjc.blockgamejournal.gui.screen.JournalScreen; import dev.bnjc.blockgamejournal.gui.screen.TrackingScreen; import dev.bnjc.blockgamejournal.journal.Journal; @@ -17,31 +18,33 @@ import net.minecraft.client.gui.screen.narration.NarrationMessageBuilder; import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.TexturedButtonWidget; -import net.minecraft.item.Item; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.text.MutableText; import net.minecraft.text.Text; import net.minecraft.util.Formatting; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import org.lwjgl.glfw.GLFW; import java.util.*; public class TrackingWidget extends ScrollableViewWidget { private static final int MIN_WIDTH = 150; - private static Set expandedIngredients = new HashSet<>(); + private static final Set expandedIngredients = new HashSet<>(); private static boolean flattened = false; private final Screen parent; private final TextRenderer textRenderer; private final TexturedButtonWidget showHideButton; - private JournalPlayerInventory inventory; + private final Map ingredientPositions; + private final Map itemPositions; + private final JournalPlayerInventory inventory; + private TrackingList trackingList; - private Map ingredientPositions; - private Map itemPositions; private int flattenY; private int lastY; @@ -49,6 +52,9 @@ public class TrackingWidget extends ScrollableViewWidget { private int mouseX = 0; private int mouseY = 0; + private boolean shiftDown = false; + private boolean ctrlDown = false; + public TrackingWidget(Screen parent, int x, int y, int width, int height) { super(x, y, width, height, Text.empty()); @@ -163,13 +169,7 @@ private void renderEntryHeader(DrawContext context, int x, int y, JournalEntry e } context.drawItem(item, x, y); - - if (entry.getCount() > 1) { - context.getMatrices().push(); - context.getMatrices().translate(0, 0, 200.0f); - context.drawText(textRenderer, Text.literal("" + entry.getCount()).formatted(Formatting.WHITE), x + 8, y + 8, 0x404040, true); - context.getMatrices().pop(); - } + ItemUtil.renderItemCount(context, x, y, entry.getCount()); context.drawText(textRenderer, ItemUtil.getName(item), x + 24, y + 6, 0xFFFFFF, false); @@ -295,6 +295,7 @@ private void renderIngredients(DrawContext context, List ingredients, for (ItemStack item : ingredients) { int startY = this.lastY; String ingredientKey = (parentKey != null ? parentKey + ";" : "") + ItemUtil.getKey(item); + int manualCount = Journal.INSTANCE.getMetadata().getManualCount(ingredientKey); // Render item context.drawItem(item, x, this.lastY); @@ -303,9 +304,14 @@ private void renderIngredients(DrawContext context, List ingredients, int itemCount = item.getCount() * quantity; int neededCount = this.inventory.neededCount(item, itemCount); int inventoryCount = itemCount - neededCount; - boolean hasEnough = neededCount <= 0; + boolean hasEnough = (neededCount - manualCount) <= 0; - MutableText text = Text.literal(hasEnough ? "✔ " : "✖ ").formatted(hasEnough ? Formatting.DARK_GREEN : Formatting.DARK_RED); + String icon = hasEnough ? "✔" : "✖"; + if (hasEnough && manualCount > 0 && neededCount > 0) { + // Show this icon only if the item is completed, but not fully by inventory + icon = "■"; + } + MutableText text = Text.literal(icon + " ").formatted(hasEnough ? Formatting.DARK_GREEN : Formatting.DARK_RED); MutableText itemText = Text.literal(ItemUtil.getName(item)).formatted(hasEnough ? Formatting.DARK_GRAY : Formatting.WHITE); if (hasEnough) { itemText.formatted(Formatting.STRIKETHROUGH); @@ -316,10 +322,8 @@ private void renderIngredients(DrawContext context, List ingredients, if (itemCount > 1) { MutableText countText = Text.literal(" ("); - - countText.append(Text.literal("" + inventoryCount) + countText.append(Text.literal("" + (inventoryCount + manualCount)) .formatted(hasEnough ? Formatting.DARK_GREEN : Formatting.DARK_RED)); - countText.append(Text.literal("/" + itemCount + ")")); countText.setStyle(countText.getStyle().withColor(0x8A8A8A)); text.append(countText); @@ -339,7 +343,7 @@ private void renderIngredients(DrawContext context, List ingredients, if (entries != null && !entries.isEmpty()) { JournalEntry nextEntry = entries.get(0); int nextCount = nextEntry.getCount(); - int reqCount = itemCount - inventoryCount; // Remove the count that is already in the inventory + int reqCount = itemCount - inventoryCount - manualCount; // Remove the count that is already in the inventory int quantityNeeded = (int) Math.ceil((double) reqCount / nextCount); this.renderCost(context, nextEntry.getCost() * quantityNeeded); @@ -380,7 +384,7 @@ private void renderTooltip(DrawContext context, int mouseX, int mouseY) { } tooltipText.add(Text.empty()); - tooltipText.add(Text.literal("Right-click to remove from list").formatted(Formatting.ITALIC, Formatting.GRAY)); + tooltipText.add(Text.literal("Right-click").formatted(Formatting.ITALIC, Formatting.DARK_RED).append(Text.literal(" to remove from list").formatted(Formatting.ITALIC, Formatting.GRAY))); context.getMatrices().push(); context.getMatrices().translate(0, 0, 200.0f); @@ -409,7 +413,47 @@ private void renderTooltip(DrawContext context, int mouseX, int mouseY) { tooltipText.add(Text.literal("Vendor: ").formatted(Formatting.GRAY) .append(Text.literal(journalEntry.getNpcName()).formatted(Formatting.DARK_AQUA))); tooltipText.add(Text.empty()); - tooltipText.add(Text.literal("Left-click to expand/collapse").formatted(Formatting.ITALIC, Formatting.GRAY)); + tooltipText.add(Text.literal("Left-click").formatted(Formatting.ITALIC, Formatting.GOLD).append(Text.literal(" to expand/collapse").formatted(Formatting.ITALIC, Formatting.GRAY))); + } + else { + tooltipText.add(Text.empty()); + } + + if (BlockgameJournal.getConfig().getGeneralConfig().enableManualTracking) { + Text incDecText; + if (shiftDown) { + if (ctrlDown) { + incDecText = Text.literal("-8").formatted(Formatting.DARK_RED, Formatting.ITALIC); + } else { + incDecText = Text.literal("+8").formatted(Formatting.DARK_GREEN, Formatting.ITALIC); + } + } else { + if (ctrlDown) { + incDecText = Text.literal("-1").formatted(Formatting.DARK_RED, Formatting.ITALIC); + } else { + incDecText = Text.literal("+1").formatted(Formatting.DARK_GREEN, Formatting.ITALIC); + } + } + + tooltipText.add( + Text.literal("Right-click").formatted(Formatting.ITALIC, Formatting.BLUE) + .append(Text.literal(" to manually set ").formatted(Formatting.ITALIC, Formatting.GRAY)) + .append(incDecText) + ); + + int inventoryCount = this.inventory.count(stack); + if (inventoryCount > 0) { + tooltipText.add(Text.empty()); + tooltipText.add(Text.literal("Inventory Count: ").formatted(Formatting.GRAY).append(Text.literal("" + inventoryCount).formatted(Formatting.GREEN, Formatting.BOLD))); + } + + int manualCount = Journal.INSTANCE.getMetadata().getManualCount(entry.getKey()); + if (manualCount > 0) { + if (inventoryCount <= 0) { + tooltipText.add(Text.empty()); + } + tooltipText.add(Text.literal("Manual Count: ").formatted(Formatting.GRAY).append(Text.literal("" + manualCount).formatted(Formatting.GOLD, Formatting.BOLD))); + } } context.getMatrices().push(); @@ -429,6 +473,30 @@ protected void appendClickableNarrations(NarrationMessageBuilder builder) { } + @Override + public boolean keyPressed(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_LEFT_SHIFT || keyCode == GLFW.GLFW_KEY_RIGHT_SHIFT) { + shiftDown = true; + } + else if (keyCode == GLFW.GLFW_KEY_LEFT_CONTROL || keyCode == GLFW.GLFW_KEY_RIGHT_CONTROL) { + ctrlDown = true; + } + + return super.keyPressed(keyCode, scanCode, modifiers); + } + + @Override + public boolean keyReleased(int keyCode, int scanCode, int modifiers) { + if (keyCode == GLFW.GLFW_KEY_LEFT_SHIFT || keyCode == GLFW.GLFW_KEY_RIGHT_SHIFT) { + shiftDown = false; + } + else if (keyCode == GLFW.GLFW_KEY_LEFT_CONTROL || keyCode == GLFW.GLFW_KEY_RIGHT_CONTROL) { + ctrlDown = false; + } + + return super.keyReleased(keyCode, scanCode, modifiers); + } + @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { if (!this.active || !this.visible || Journal.INSTANCE == null) { @@ -471,26 +539,90 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { return true; } } else if (button == 1) { + // Add/remove tracking for (Map.Entry entry : this.itemPositions.entrySet()) { Integer[] bounds = entry.getValue(); if (mouseY + getScrollY() >= bounds[0] && mouseY + getScrollY() <= bounds[1]) { if (entry.getKey() != null) { this.playDownSound(MinecraftClient.getInstance().getSoundManager()); + entry.getKey().setTracked(false); + // Remove item positions + this.itemPositions.remove(entry.getKey()); + if (this.parent instanceof JournalScreen journalScreen) { journalScreen.refreshTracking(); } else if (this.parent instanceof TrackingScreen trackingScreen) { trackingScreen.refreshTracking(); } + // Remove manually completed items + for (String key : removedManuallyCompleteItems()) { + Journal.INSTANCE.getMetadata().removeManualCount(key); + } + return true; } } } + + // Add/remove manual completion + for (Map.Entry entry : this.ingredientPositions.entrySet()) { + Integer[] bounds = entry.getValue(); + if (mouseY + getScrollY() >= bounds[0] && mouseY + getScrollY() <= bounds[1]) { + String itemKey = entry.getKey(); + this.playDownSound(MinecraftClient.getInstance().getSoundManager()); + + var metadata = Journal.INSTANCE.getMetadata(); + + if (shiftDown) { + if (ctrlDown) { + // Decrement by 8 + metadata.adjustManualCount(itemKey, -8); + } else { + // Increment by 8 + metadata.adjustManualCount(itemKey, 8); + } + } else { + if (ctrlDown) { + // Decrement by 1 + metadata.adjustManualCount(itemKey, -1); + } else { + // Increment by 1 + metadata.adjustManualCount(itemKey, 1); + } + } + + return true; + } + } + } + } + return super.mouseClicked(mouseX, mouseY, button); + } + + private @NotNull Set removedManuallyCompleteItems() { + if (Journal.INSTANCE == null) { + return Collections.emptySet(); + } + + Set keysToRemove = new HashSet<>(); + for (String completedKey : Journal.INSTANCE.getMetadata().getManuallyCompletedTracking().keySet()) { + boolean hasKey = false; + for (String key : this.trackingList.getIngredients().keySet()) { + String[] subKeys = key.split(";"); + if (subKeys[0].equals(completedKey.split(";")[0])) { + hasKey = true; + break; + } + } + + if (!hasKey) { + keysToRemove.add(completedKey); } } - return false; + return keysToRemove; } private void toggleFlattened() { diff --git a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/VerticalScrollWidget.java b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/VerticalScrollWidget.java index 93d82ba..b5cb00a 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/gui/widget/VerticalScrollWidget.java +++ b/src/main/java/dev/bnjc/blockgamejournal/gui/widget/VerticalScrollWidget.java @@ -1,5 +1,6 @@ package dev.bnjc.blockgamejournal.gui.widget; +import dev.bnjc.blockgamejournal.BlockgameJournal; import dev.bnjc.blockgamejournal.util.GuiUtil; import lombok.Setter; import net.minecraft.client.gui.DrawContext; @@ -12,6 +13,7 @@ import org.jetbrains.annotations.Nullable; import java.util.function.Consumer; +import java.util.function.Supplier; public class VerticalScrollWidget extends ClickableWidget { private static final Identifier BACKGROUND = GuiUtil.sprite("scroll_bar"); @@ -35,8 +37,12 @@ public class VerticalScrollWidget extends ClickableWidget { @Nullable private Consumer responder = null; - public VerticalScrollWidget(int x, int y, int height, Text message) { + private final Supplier rowCount; + + public VerticalScrollWidget(int x, int y, int height, Text message, Supplier rowCount) { super(x, y, BAR_WIDTH, height, message); + + this.rowCount = rowCount; } public void setDisabled(boolean disabled) { @@ -65,7 +71,7 @@ public boolean mouseClicked(double mouseX, double mouseY, int button) { @Override public boolean mouseScrolled(double mouseX, double mouseY, double horizontalAmount, double verticalAmount) { if (this.visible && !this.disabled) { - setProgress((float) (this.progress - verticalAmount)); + setProgress((float) (this.progress - (verticalAmount / (float) this.rowCount.get()))); } return super.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount); } diff --git a/src/main/java/dev/bnjc/blockgamejournal/journal/metadata/Metadata.java b/src/main/java/dev/bnjc/blockgamejournal/journal/metadata/Metadata.java index 33aba8a..9b02889 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/journal/metadata/Metadata.java +++ b/src/main/java/dev/bnjc/blockgamejournal/journal/metadata/Metadata.java @@ -2,6 +2,7 @@ import com.mojang.serialization.Codec; import com.mojang.serialization.codecs.RecordCodecBuilder; +import dev.bnjc.blockgamejournal.BlockgameJournal; import lombok.Getter; import lombok.Setter; import org.jetbrains.annotations.Nullable; @@ -53,11 +54,15 @@ public class Metadata { Codec.unboundedMap(JournalAdvancement.CODEC, Codec.BOOL) .xmap(HashMap::new, Function.identity()) .optionalFieldOf("advancements") - .forGetter(meta -> Optional.ofNullable(meta.advancements)) + .forGetter(meta -> Optional.ofNullable(meta.advancements)), + Codec.unboundedMap(Codec.STRING, Codec.INT) + .xmap(HashMap::new, Function.identity()) + .optionalFieldOf("manuallyCompletedTracking") + .forGetter(meta -> Optional.ofNullable(meta.manuallyCompletedTracking)) ).apply(instance, (name, lastModified, loadedTime, playerBalance, balanceLastUpdated, professionLevels, professionLastUpdated, backpackContents, backpackLastUpdated, - knownRecipes, advancements) -> { + knownRecipes, advancements, manuallyCompletedTracking) -> { Metadata meta = new Metadata( name.orElse(null), lastModified.map(Instant::ofEpochMilli).orElse(Instant.now()), @@ -75,6 +80,7 @@ public class Metadata { knownRecipes.ifPresent(meta::setKnownRecipes); advancements.ifPresent(meta::setAdvancements); + manuallyCompletedTracking.ifPresent(meta::setManuallyCompletedTracking); return meta; }) @@ -105,6 +111,9 @@ public class Metadata { @Setter private HashMap advancements; + @Setter + private HashMap manuallyCompletedTracking; + public Metadata(@Nullable String name, Instant lastModified, long loadedTime) { this.name = name; this.lastModified = lastModified; @@ -117,6 +126,7 @@ public Metadata(@Nullable String name, Instant lastModified, long loadedTime) { this.backpackLastUpdated = null; this.knownRecipes = new HashMap<>(); this.advancements = new HashMap<>(); + this.manuallyCompletedTracking = new HashMap<>(); } public static Metadata blank() { @@ -179,4 +189,28 @@ public void grantAdvancement(JournalAdvancement advancement) { public boolean hasAdvancement(JournalAdvancement advancement) { return this.advancements.getOrDefault(advancement, false); } + + public int getManualCount(String key) { + if (!BlockgameJournal.getConfig().getGeneralConfig().enableManualTracking) { + return 0; + } + + return this.manuallyCompletedTracking.getOrDefault(key, 0); + } + + public void removeManualCount(String key) { + if (!BlockgameJournal.getConfig().getGeneralConfig().enableManualTracking) { + return; + } + + this.manuallyCompletedTracking.remove(key); + } + + public void adjustManualCount(String key, int amount) { + if (!BlockgameJournal.getConfig().getGeneralConfig().enableManualTracking) { + return; + } + + this.manuallyCompletedTracking.put(key, Math.max(this.getManualCount(key) + amount, 0)); + } } diff --git a/src/main/java/dev/bnjc/blockgamejournal/journal/npc/NPCUtil.java b/src/main/java/dev/bnjc/blockgamejournal/journal/npc/NPCUtil.java index f4d96b8..e4b7077 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/journal/npc/NPCUtil.java +++ b/src/main/java/dev/bnjc/blockgamejournal/journal/npc/NPCUtil.java @@ -4,12 +4,13 @@ import dev.bnjc.blockgamejournal.journal.Journal; import net.minecraft.client.MinecraftClient; import net.minecraft.entity.Entity; +import org.jetbrains.annotations.NotNull; import java.util.HashSet; import java.util.Set; public final class NPCUtil { - public static void createOrUpdate(String name, Entity entity) { + public static void createOrUpdate(String name, @NotNull Entity entity) { if (Journal.INSTANCE == null) { return; } diff --git a/src/main/java/dev/bnjc/blockgamejournal/journal/recipe/JournalPlayerInventory.java b/src/main/java/dev/bnjc/blockgamejournal/journal/recipe/JournalPlayerInventory.java index 0ba9925..826b5d4 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/journal/recipe/JournalPlayerInventory.java +++ b/src/main/java/dev/bnjc/blockgamejournal/journal/recipe/JournalPlayerInventory.java @@ -59,6 +59,16 @@ public int neededCount(ItemStack stack, int count) { return count - inventoryStack.getCount(); } + public int count(ItemStack stack) { + String key = ItemUtil.getKey(stack); + ItemStack inventoryStack = this.inventory.get(key); + if (inventoryStack == null) { + return 0; + } + + return inventoryStack.getCount(); + } + public int consume(ItemStack stack, int count) { String key = ItemUtil.getKey(stack); ItemStack inventoryStack = this.editableInventory.get(key); diff --git a/src/main/java/dev/bnjc/blockgamejournal/storage/backend/NbtBackend.java b/src/main/java/dev/bnjc/blockgamejournal/storage/backend/NbtBackend.java index 7f7cb14..4dbdaab 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/storage/backend/NbtBackend.java +++ b/src/main/java/dev/bnjc/blockgamejournal/storage/backend/NbtBackend.java @@ -113,7 +113,7 @@ private Map loadKnownNPCs() { Path npcPath = STORAGE_DIR.resolve(NPC_CACHE_NAME + extension()); var npcResult = Timer.time(() -> FileUtil.loadFromNbt(Journal.KNOWN_NPCS_CODEC, npcPath)); if (npcResult.getFirst().isPresent()) { - LOGGER.info("[Blockgame Journal] Loaded NPCs {} in {}ns", npcPath, npcResult.getSecond()); + LOGGER.info("[Blockgame Journal] Loaded vendors {} in {}ns", npcPath, npcResult.getSecond()); return npcResult.getFirst().get(); } diff --git a/src/main/java/dev/bnjc/blockgamejournal/util/ItemUtil.java b/src/main/java/dev/bnjc/blockgamejournal/util/ItemUtil.java index 8473a9e..d6dc79e 100644 --- a/src/main/java/dev/bnjc/blockgamejournal/util/ItemUtil.java +++ b/src/main/java/dev/bnjc/blockgamejournal/util/ItemUtil.java @@ -5,11 +5,14 @@ import dev.bnjc.blockgamejournal.journal.Journal; import dev.bnjc.blockgamejournal.journal.JournalEntry; import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.DrawContext; import net.minecraft.item.ItemStack; import net.minecraft.item.Items; import net.minecraft.nbt.NbtCompound; import net.minecraft.recipe.*; import net.minecraft.registry.Registries; +import net.minecraft.text.Text; +import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; import org.jetbrains.annotations.Nullable; @@ -244,4 +247,17 @@ public static boolean isOutdated(JournalEntry entry, CraftingStationItem expecte return false; } + + public static void renderItemCount(DrawContext context, int x, int y, int count) { + if (count <= 1) { + return; + } + + context.getMatrices().push(); + context.getMatrices().translate(0.0f, 0.0f, 200.0f); + + Text text = Text.literal("" + count).formatted(Formatting.WHITE); + context.drawText(MinecraftClient.getInstance().textRenderer, text, x + 19 - 2 - MinecraftClient.getInstance().textRenderer.getWidth(text), y + 6 + 3, 0xFFFFFF, true); + context.getMatrices().pop(); + } } diff --git a/src/main/resources/assets/blockgamejournal/lang/en_us.json b/src/main/resources/assets/blockgamejournal/lang/en_us.json index 3bcfd18..6f58504 100644 --- a/src/main/resources/assets/blockgamejournal/lang/en_us.json +++ b/src/main/resources/assets/blockgamejournal/lang/en_us.json @@ -43,6 +43,8 @@ "text.autoconfig.blockgamejournal.option.generalConfig.highlightOutdatedRecipes.@Tooltip": "Whether to highlight outdated recipes in the vendor's recipe list.", "text.autoconfig.blockgamejournal.option.generalConfig.showRecipeLock": "Show Recipe Lock Icon", "text.autoconfig.blockgamejournal.option.generalConfig.showRecipeLock.@Tooltip": "Whether to show a lock icon for recipes without a learned recipe or high enough level.", + "text.autoconfig.blockgamejournal.option.generalConfig.enableManualTracking": "Enable Manual Tracking §3ʙᴇᴛᴀ", + "text.autoconfig.blockgamejournal.option.generalConfig.enableManualTracking.@Tooltip": "Whether to allow manual changes to the recipe tracking list.", "text.autoconfig.blockgamejournal.option.generalConfig.defaultMode": "Default Mode", "text.autoconfig.blockgamejournal.option.generalConfig.defaultMode.@Tooltip": "The default mode to open the recipe journal in.", "text.autoconfig.blockgamejournal.option.generalConfig.defaultNpcSort": "Default Vendor Item Sort", @@ -52,8 +54,8 @@ "text.autoconfig.blockgamejournal.option.decompositionConfig.decomposeVanillaItems.@Tooltip": "Whether recipes should decompose vanilla items into their components.", "text.autoconfig.blockgamejournal.option.decompositionConfig.partialDecomposition": "Partial Decomposition", "text.autoconfig.blockgamejournal.option.decompositionConfig.partialDecomposition.@Tooltip": "Whether to partially decompose items based on items in the player's inventory.", - "text.autoconfig.blockgamejournal.option.decompositionConfig.useBackpackItems": "Use Backpack Items", - "text.autoconfig.blockgamejournal.option.decompositionConfig.useBackpackItems.@Tooltip": "Whether to use items in the player's backpack for decomposition [BETA]", + "text.autoconfig.blockgamejournal.option.decompositionConfig.useBackpackItems": "Use Backpack Items §3ʙᴇᴛᴀ", + "text.autoconfig.blockgamejournal.option.decompositionConfig.useBackpackItems.@Tooltip": "Whether to use items in the player's backpack for decomposition", "text.autoconfig.blockgamejournal.option.storageConfig": "Storage", "text.autoconfig.blockgamejournal.option.storageConfig.backendType": "Storage Type", "text.autoconfig.blockgamejournal.option.storageConfig.backendType.@Tooltip": "The type of storage to use for the recipe journal.",