From 5c2042912439a4e64038cd39731ad3d5efe99d71 Mon Sep 17 00:00:00 2001 From: Aedial Date: Thu, 28 May 2026 20:50:13 +0200 Subject: [PATCH 1/3] Add a History button, showing finished quest Sorted in descending order, from most recent. Adding time ranges, later, may be a good addition --- .../gui/panels/lists/CanvasQuestHistory.java | 141 ++++++++++++++++++ .../client/gui2/GuiQuestHistory.java | 79 ++++++++++ .../client/gui2/GuiQuestLines.java | 33 +++- .../misc/QuestHistoryEntry.java | 18 +++ .../assets/betterquesting/lang/en_us.lang | 1 + 5 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java create mode 100644 src/main/java/betterquesting/client/gui2/GuiQuestHistory.java create mode 100644 src/main/java/betterquesting/misc/QuestHistoryEntry.java diff --git a/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java b/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java new file mode 100644 index 000000000..8d16cd2aa --- /dev/null +++ b/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java @@ -0,0 +1,141 @@ +package betterquesting.api2.client.gui.panels.lists; + +import betterquesting.api.api.QuestingAPI; +import betterquesting.api.properties.NativeProps; +import betterquesting.api.questing.IQuest; +import betterquesting.api.questing.IQuestLine; +import betterquesting.api.questing.IQuestLineEntry; +import betterquesting.api2.client.gui.controls.PanelButtonCustom; +import betterquesting.api2.client.gui.controls.PanelButtonQuest; +import betterquesting.api2.client.gui.misc.GuiRectangle; +import betterquesting.api2.client.gui.misc.IGuiRect; +import betterquesting.api2.client.gui.panels.content.PanelTextBox; +import betterquesting.api2.client.gui.themes.presets.PresetColor; +import betterquesting.api2.storage.DBEntry; +import betterquesting.api2.utils.QuestTranslation; +import betterquesting.misc.QuestHistoryEntry; +import betterquesting.questing.QuestDatabase; +import betterquesting.questing.QuestLineDatabase; +import net.minecraft.entity.player.EntityPlayer; +import net.minecraft.nbt.NBTTagCompound; + +import java.text.DateFormat; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.function.Consumer; + +public class CanvasQuestHistory extends CanvasSearch { + private List historyList; + private Consumer questOpenCallback; + private final EntityPlayer player; + private final UUID questingUUID; + + public CanvasQuestHistory(IGuiRect rect, EntityPlayer player) { + super(rect); + this.player = player; + this.questingUUID = QuestingAPI.getQuestingUUID(player); + } + + @Override + protected Iterator getIterator() { + if (historyList == null) { + historyList = collectHistory(); + } + + return historyList.iterator(); + } + + private List collectHistory() { + Map historyEntries = new HashMap<>(); + + for (DBEntry questLine : QuestLineDatabase.INSTANCE.getEntries()) { + if (questLine.getValue() == null) { + continue; + } + + for (DBEntry questLineEntry : questLine.getValue().getEntries()) { + int questId = questLineEntry.getID(); + if (historyEntries.containsKey(questId)) { + continue; + } + + IQuest quest = QuestDatabase.INSTANCE.getValue(questId); + if (quest == null) { + continue; + } + + NBTTagCompound completionInfo = quest.getCompletionInfo(questingUUID); + if (completionInfo == null) { + continue; + } + + DBEntry questEntry = new DBEntry<>(questId, quest); + long timestamp = completionInfo.getLong("timestamp"); + historyEntries.put(questId, new QuestHistoryEntry(questEntry, questLine, timestamp)); + } + } + + List sortedEntries = new ArrayList<>(historyEntries.values()); + sortedEntries.sort(Comparator.comparingLong(QuestHistoryEntry::getCompletionTimestamp) + .reversed() + .thenComparingInt(entry -> entry.getQuest().getID())); + return sortedEntries; + } + + @Override + protected void queryMatches(QuestHistoryEntry entry, String query, ArrayDeque results) { + results.add(entry); + } + + @Override + protected boolean addResult(QuestHistoryEntry entry, int index, int cachedWidth) { + GuiRectangle buttonRect = new GuiRectangle(0, index * 32, cachedWidth, 32, 0); + PanelButtonCustom buttonContainer = new PanelButtonCustom(buttonRect, 2); + buttonContainer.setCallback(panelButtonCustom -> { + if (questOpenCallback != null) { + questOpenCallback.accept(entry); + } + }); + this.addPanel(buttonContainer); + + GuiRectangle questButtonRect = new GuiRectangle(2, 2, 28, 28); + PanelButtonQuest questButton = new PanelButtonQuest(questButtonRect, 0, "", entry.getQuest()); + questButton.setCallback(value -> { + if (questOpenCallback != null) { + questOpenCallback.accept(entry); + } + }); + buttonContainer.addPanel(questButton); + + GuiRectangle questNameRect = new GuiRectangle(36, 6, cachedWidth - 36, 12); + String questNameStr = entry.getQuest().getValue().getProperty(NativeProps.NAME); + PanelTextBox questName = new PanelTextBox(questNameRect, QuestTranslation.translate(questNameStr)); + buttonContainer.addPanel(questName); + + GuiRectangle timestampRect = new GuiRectangle(36, 20, cachedWidth - 36, 10); + PanelTextBox timestamp = new PanelTextBox(timestampRect, formatTimestamp(entry.getCompletionTimestamp())); + timestamp.setColor(PresetColor.TEXT_AUX_0.getColor()); + buttonContainer.addPanel(timestamp); + return true; + } + + private String formatTimestamp(long timestamp) { + if (timestamp <= 0) { + return "-"; + } + + return DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT) + .format(new Date(timestamp)); + } + + public void setQuestOpenCallback(Consumer questOpenCallback) { + this.questOpenCallback = questOpenCallback; + } +} \ No newline at end of file diff --git a/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java new file mode 100644 index 000000000..5b5bed190 --- /dev/null +++ b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java @@ -0,0 +1,79 @@ +package betterquesting.client.gui2; + +import betterquesting.api2.client.gui.GuiScreenCanvas; +import betterquesting.api2.client.gui.controls.PanelButton; +import betterquesting.api2.client.gui.misc.GuiAlign; +import betterquesting.api2.client.gui.misc.GuiPadding; +import betterquesting.api2.client.gui.misc.GuiTransform; +import betterquesting.api2.client.gui.panels.CanvasEmpty; +import betterquesting.api2.client.gui.panels.CanvasTextured; +import betterquesting.api2.client.gui.panels.bars.PanelVScrollBar; +import betterquesting.api2.client.gui.panels.content.PanelTextBox; +import betterquesting.api2.client.gui.panels.lists.CanvasQuestHistory; +import betterquesting.api2.client.gui.themes.presets.PresetColor; +import betterquesting.api2.client.gui.themes.presets.PresetTexture; +import betterquesting.api2.utils.QuestTranslation; +import betterquesting.client.BookmarkManager; +import betterquesting.misc.QuestHistoryEntry; +import net.minecraft.client.gui.GuiScreen; + +import java.util.function.Consumer; + +public class GuiQuestHistory extends GuiScreenCanvas { + private Consumer callback; + + public GuiQuestHistory(GuiScreen parent) { + super(parent); + } + + @Override + public void initPanel() { + super.initPanel(); + GuiTransform bgTransform = new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 0, 0, 0), 0); + CanvasTextured cvBackground = new CanvasTextured(bgTransform, PresetTexture.PANEL_MAIN.getTexture()); + this.addPanel(cvBackground); + + CanvasEmpty cvInner = new CanvasEmpty(new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(8, 8, 8, 8), 0)); + cvBackground.addPanel(cvInner); + + createExitButton(cvInner); + + GuiTransform titleTransform = new GuiTransform(GuiAlign.TOP_EDGE, new GuiPadding(0, 0, 0, -16), 0); + String title = QuestTranslation.translate("betterquesting.gui.history"); + PanelTextBox txtTitle = new PanelTextBox(titleTransform, title) + .setAlignment(1) + .setColor(PresetColor.TEXT_MAIN.getColor()); + cvInner.addPanel(txtTitle); + + GuiTransform listTransform = new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 16, 8, 24), 0); + CanvasQuestHistory canvasQuestHistory = new CanvasQuestHistory(listTransform, mc.player); + canvasQuestHistory.setQuestOpenCallback(entry -> { + acceptCallback(entry); + BookmarkManager.INSTANCE.setBookmark(this, entry.getQuest().getID()); + mc.displayGuiScreen(BookmarkManager.INSTANCE.getBookmark()); + }); + cvInner.addPanel(canvasQuestHistory); + + GuiTransform scTransform = new GuiTransform(GuiAlign.RIGHT_EDGE, new GuiPadding(-8, 16, 0, 24), 0); + PanelVScrollBar scDb = new PanelVScrollBar(scTransform); + cvInner.addPanel(scDb); + canvasQuestHistory.setScrollDriverY(scDb); + } + + private void createExitButton(CanvasEmpty cvInner) { + GuiTransform btnTransform = new GuiTransform(GuiAlign.BOTTOM_CENTER, new GuiPadding(-100, -16, -100, 0), 0); + PanelButton btnExit = new PanelButton(btnTransform, 0, QuestTranslation.translate("gui.back")); + btnExit.setClickAction(b -> mc.displayGuiScreen(parent)); + cvInner.addPanel(btnExit); + } + + public void setCallback(Consumer callback) { + this.callback = callback; + } + + private void acceptCallback(QuestHistoryEntry entry) { + if (callback != null) { + callback.accept(entry); + } + } +} \ No newline at end of file diff --git a/src/main/java/betterquesting/client/gui2/GuiQuestLines.java b/src/main/java/betterquesting/client/gui2/GuiQuestLines.java index 71387fdd9..efea18b95 100644 --- a/src/main/java/betterquesting/client/gui2/GuiQuestLines.java +++ b/src/main/java/betterquesting/client/gui2/GuiQuestLines.java @@ -152,13 +152,19 @@ public void initPanel() { btnSearch.setTooltip(Collections.singletonList(QuestTranslation.translate("betterquesting.gui.search"))); cvBackground.addPanel(btnSearch); + // History Button + PanelButton btnHistory = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_LEFT, 8, -56, 32, 16, 0), -1, "").setIcon(PresetIcon.ICON_BOOKS.getTexture()); + btnHistory.setClickAction(this::openHistory); + btnHistory.setTooltip(Collections.singletonList(QuestTranslation.translate("betterquesting.gui.history"))); + cvBackground.addPanel(btnHistory); + if (canEdit) { - PanelButton btnEdit = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_LEFT, 8, -56, 16, 16, 0), -1, "").setIcon(PresetIcon.ICON_GEAR.getTexture()); + PanelButton btnEdit = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_LEFT, 8, -72, 16, 16, 0), -1, "").setIcon(PresetIcon.ICON_GEAR.getTexture()); btnEdit.setClickAction((b) -> mc.displayGuiScreen(new GuiQuestLinesEditor(this))); btnEdit.setTooltip(Collections.singletonList(QuestTranslation.translate("betterquesting.btn.edit"))); cvBackground.addPanel(btnEdit); - btnDesign = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_LEFT, 24, -56, 16, 16, 0), -1, "").setIcon(PresetIcon.ICON_SORT.getTexture()); + btnDesign = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_LEFT, 24, -72, 16, 16, 0), -1, "").setIcon(PresetIcon.ICON_SORT.getTexture()); btnDesign.setClickAction($ -> mc.displayGuiScreen(new GuiDesigner(this, selectedLine))); btnDesign.setTooltip(Collections.singletonList(QuestTranslation.translate("betterquesting.btn.designer"))); cvBackground.addPanel(btnDesign); @@ -685,15 +691,26 @@ private void refreshDesigner() { private void openSearch(PanelButton panelButton) { GuiQuestSearch guiQuestSearch = new GuiQuestSearch(this); - guiQuestSearch.setCallback(entry -> { - openQuestLine(entry.getQuestLineEntry()); - int selectedQuestId = entry.getQuest().getID(); - Optional targetQuestButton = cvQuest.getQuestButtons().stream().filter(panelButtonQuest -> panelButtonQuest.getStoredValue().getID() == selectedQuestId).findFirst(); - targetQuestButton.ifPresent(this::highlightButton); - }); + guiQuestSearch.setCallback(entry -> openQuestEntry(entry.getQuestLineEntry(), entry.getQuest().getID())); mc.displayGuiScreen(guiQuestSearch); } + private void openHistory(PanelButton panelButton) { + GuiQuestHistory guiQuestHistory = new GuiQuestHistory(this); + guiQuestHistory.setCallback(entry -> openQuestEntry(entry.getQuestLineEntry(), entry.getQuest().getID())); + mc.displayGuiScreen(guiQuestHistory); + } + + private void openQuestEntry(DBEntry questLine, int questId) { + openQuestLine(questLine); + + Optional targetQuestButton = cvQuest.getQuestButtons() + .stream() + .filter(panelButtonQuest -> panelButtonQuest.getStoredValue().getID() == questId) + .findFirst(); + targetQuestButton.ifPresent(this::highlightButton); + } + private void highlightButton(PanelButtonQuest panelButtonQuest) { GuiTextureColored newTexture = new GuiTextureColored( panelButtonQuest.txFrame, diff --git a/src/main/java/betterquesting/misc/QuestHistoryEntry.java b/src/main/java/betterquesting/misc/QuestHistoryEntry.java new file mode 100644 index 000000000..533e2b760 --- /dev/null +++ b/src/main/java/betterquesting/misc/QuestHistoryEntry.java @@ -0,0 +1,18 @@ +package betterquesting.misc; + +import betterquesting.api.questing.IQuest; +import betterquesting.api.questing.IQuestLine; +import betterquesting.api2.storage.DBEntry; + +public class QuestHistoryEntry extends QuestSearchEntry { + private final long completionTimestamp; + + public QuestHistoryEntry(DBEntry quest, DBEntry questLineEntry, long completionTimestamp) { + super(quest, questLineEntry); + this.completionTimestamp = completionTimestamp; + } + + public long getCompletionTimestamp() { + return completionTimestamp; + } +} \ No newline at end of file diff --git a/src/main/resources/assets/betterquesting/lang/en_us.lang b/src/main/resources/assets/betterquesting/lang/en_us.lang index 3247df161..8958b1d23 100644 --- a/src/main/resources/assets/betterquesting/lang/en_us.lang +++ b/src/main/resources/assets/betterquesting/lang/en_us.lang @@ -85,6 +85,7 @@ betterquesting.gui.quest_line=Quest Line betterquesting.gui.database=Database betterquesting.gui.selection=Selection betterquesting.gui.search=Search +betterquesting.gui.history=Quest History betterquesting.gui.folder=Folder betterquesting.gui.party_invites=Invites betterquesting.gui.party_members=Members From 2e535d1b67322b8a87d78aeaabf7dbecdb13c550 Mon Sep 17 00:00:00 2001 From: Aedial Date: Thu, 28 May 2026 23:17:44 +0200 Subject: [PATCH 2/3] Restore scroll bar position when using the Back button --- .../client/gui2/GuiQuestHistory.java | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java index 5b5bed190..bc0b4d595 100644 --- a/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java +++ b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java @@ -21,6 +21,9 @@ public class GuiQuestHistory extends GuiScreenCanvas { private Consumer callback; + private CanvasQuestHistory canvasQuestHistory; + private int historyScrollY; + private boolean restoreHistoryScroll; public GuiQuestHistory(GuiScreen parent) { super(parent); @@ -46,8 +49,9 @@ public void initPanel() { cvInner.addPanel(txtTitle); GuiTransform listTransform = new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 16, 8, 24), 0); - CanvasQuestHistory canvasQuestHistory = new CanvasQuestHistory(listTransform, mc.player); + canvasQuestHistory = new CanvasQuestHistory(listTransform, mc.player); canvasQuestHistory.setQuestOpenCallback(entry -> { + saveHistoryScroll(); acceptCallback(entry); BookmarkManager.INSTANCE.setBookmark(this, entry.getQuest().getID()); mc.displayGuiScreen(BookmarkManager.INSTANCE.getBookmark()); @@ -58,6 +62,7 @@ public void initPanel() { PanelVScrollBar scDb = new PanelVScrollBar(scTransform); cvInner.addPanel(scDb); canvasQuestHistory.setScrollDriverY(scDb); + restoreHistoryScroll = historyScrollY > 0; } private void createExitButton(CanvasEmpty cvInner) { @@ -76,4 +81,53 @@ private void acceptCallback(QuestHistoryEntry entry) { callback.accept(entry); } } + + @Override + public void drawPanel(int mx, int my, float partialTick) { + super.drawPanel(mx, my, partialTick); + restoreHistoryScroll(); + } + + @Override + public boolean onMouseRelease(int mx, int my, int click) { + try { + return super.onMouseRelease(mx, my, click); + } finally { + saveHistoryScroll(); + } + } + + @Override + public boolean onMouseScroll(int mx, int my, int scroll) { + try { + return super.onMouseScroll(mx, my, scroll); + } finally { + saveHistoryScroll(); + } + } + + // History entries are buffered into the list, so keep reapplying until the scroll can be restored. + private void restoreHistoryScroll() { + if (!restoreHistoryScroll || canvasQuestHistory == null) { + return; + } + + if (canvasQuestHistory.isSearching()) { + return; + } + + canvasQuestHistory.setScrollY(historyScrollY); + canvasQuestHistory.updatePanelScroll(); + restoreHistoryScroll = false; + } + + // Save the scroll position so it can be restored later + // (e.g. when opening a quest and returning back to the history list) + private void saveHistoryScroll() { + if (canvasQuestHistory == null) { + return; + } + + historyScrollY = canvasQuestHistory.getScrollY(); + } } \ No newline at end of file From 8c2aa7ccb53e9b4d151cd9b24ca1af212d6842f1 Mon Sep 17 00:00:00 2001 From: Aedial Date: Sat, 30 May 2026 00:36:50 +0200 Subject: [PATCH 3/3] Handle repeatable quests edge case An additional key has been added for completion date (defaults to timestamp if missing). The History now has a setting to decide how to show repeatables. --- .../betterquesting/api/questing/IQuest.java | 20 ++++ .../api/storage/BQ_Settings.java | 1 + .../gui/panels/lists/CanvasQuestHistory.java | 101 +++++++++++++++--- .../gui/themes/presets/PresetTexture.java | 6 +- .../client/gui2/GuiQuestHistory.java | 61 +++++++++-- .../handlers/ConfigHandler.java | 1 + .../misc/QuestHistoryEntry.java | 14 ++- .../questing/QuestInstance.java | 21 +++- .../assets/betterquesting/lang/en_us.lang | 10 +- 9 files changed, 206 insertions(+), 29 deletions(-) diff --git a/src/main/java/betterquesting/api/questing/IQuest.java b/src/main/java/betterquesting/api/questing/IQuest.java index 1e71c0c4a..c696727e0 100644 --- a/src/main/java/betterquesting/api/questing/IQuest.java +++ b/src/main/java/betterquesting/api/questing/IQuest.java @@ -11,18 +11,38 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; +import net.minecraftforge.common.util.Constants; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.util.UUID; public interface IQuest extends INBTSaveLoad, INBTProgress, IPropertyContainer { + String LAST_COMPLETED_AT_TAG = "last_completed_at"; EnumQuestState getState(EntityPlayer player); @Nullable NBTTagCompound getCompletionInfo(UUID uuid); + /** + * Get the timestamp of the last time this quest was completed by the given player. + * @param uuid The questing UUID for the player. + * @return Timestamp of last completion. + * For quests completed before this was added, it will return the timestamp value (may be inaccurate for repeatable quests). + * For quests not completed, it will return 0. + */ + default long getLastCompletedAt(UUID uuid) { + NBTTagCompound completionInfo = getCompletionInfo(uuid); + if (completionInfo == null) return 0; + + if (completionInfo.hasKey(LAST_COMPLETED_AT_TAG, Constants.NBT.TAG_LONG)) { + return completionInfo.getLong(LAST_COMPLETED_AT_TAG); + } + + return completionInfo.getLong("timestamp"); + } + void setCompletionInfo(UUID uuid, @Nullable NBTTagCompound nbt); void update(EntityPlayer player); diff --git a/src/main/java/betterquesting/api/storage/BQ_Settings.java b/src/main/java/betterquesting/api/storage/BQ_Settings.java index e3f935623..85c4f08e6 100644 --- a/src/main/java/betterquesting/api/storage/BQ_Settings.java +++ b/src/main/java/betterquesting/api/storage/BQ_Settings.java @@ -35,6 +35,7 @@ public class BQ_Settings { public static boolean claimAllConfirmation = true; public static boolean lockTray = true; public static boolean viewMode = false; + public static String historyRepeatableFilter = "SHOW_ALL"; public static boolean limitBack = false; public static String defaultVisibility = "NORMAL"; diff --git a/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java b/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java index 8d16cd2aa..9966992cf 100644 --- a/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java +++ b/src/main/java/betterquesting/api2/client/gui/panels/lists/CanvasQuestHistory.java @@ -16,6 +16,7 @@ import betterquesting.misc.QuestHistoryEntry; import betterquesting.questing.QuestDatabase; import betterquesting.questing.QuestLineDatabase; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; import net.minecraft.entity.player.EntityPlayer; import net.minecraft.nbt.NBTTagCompound; @@ -24,7 +25,6 @@ import java.util.ArrayList; import java.util.Comparator; import java.util.Date; -import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -35,12 +35,11 @@ public class CanvasQuestHistory extends CanvasSearch historyList; private Consumer questOpenCallback; private final EntityPlayer player; - private final UUID questingUUID; + private RepeatableFilter repeatableFilter = RepeatableFilter.SHOW_ALL; public CanvasQuestHistory(IGuiRect rect, EntityPlayer player) { super(rect); this.player = player; - this.questingUUID = QuestingAPI.getQuestingUUID(player); } @Override @@ -53,13 +52,10 @@ protected Iterator getIterator() { } private List collectHistory() { - Map historyEntries = new HashMap<>(); + Map historyEntries = new Int2ObjectOpenHashMap<>(); + UUID questingUUID = QuestingAPI.getQuestingUUID(player); for (DBEntry questLine : QuestLineDatabase.INSTANCE.getEntries()) { - if (questLine.getValue() == null) { - continue; - } - for (DBEntry questLineEntry : questLine.getValue().getEntries()) { int questId = questLineEntry.getID(); if (historyEntries.containsKey(questId)) { @@ -77,8 +73,14 @@ private List collectHistory() { } DBEntry questEntry = new DBEntry<>(questId, quest); - long timestamp = completionInfo.getLong("timestamp"); - historyEntries.put(questId, new QuestHistoryEntry(questEntry, questLine, timestamp)); + long timestamp = quest.getLastCompletedAt(questingUUID); + if (timestamp <= 0) { + continue; + } + + boolean repeatable = quest.getProperty(NativeProps.REPEAT_TIME) >= 0; + boolean pendingRewards = repeatable && quest.canClaimBasically(player); + historyEntries.put(questId, new QuestHistoryEntry(questEntry, questLine, timestamp, repeatable, pendingRewards)); } } @@ -91,6 +93,16 @@ private List collectHistory() { @Override protected void queryMatches(QuestHistoryEntry entry, String query, ArrayDeque results) { + if (entry.isRepeatable()) { + if (repeatableFilter == RepeatableFilter.HIDE) { + return; + } + + if (repeatableFilter == RepeatableFilter.SHOW_PENDING_REWARDS && !entry.hasPendingRewards()) { + return; + } + } + results.add(entry); } @@ -114,18 +126,41 @@ protected boolean addResult(QuestHistoryEntry entry, int index, int cachedWidth) }); buttonContainer.addPanel(questButton); - GuiRectangle questNameRect = new GuiRectangle(36, 6, cachedWidth - 36, 12); + int repeatableLabelWidth = entry.isRepeatable() ? 96 : 0; + int questNameWidth = Math.max(0, cachedWidth - 36 - repeatableLabelWidth - (entry.isRepeatable() ? 8 : 0)); + GuiRectangle questNameRect = new GuiRectangle(36, 6, questNameWidth, 12); String questNameStr = entry.getQuest().getValue().getProperty(NativeProps.NAME); PanelTextBox questName = new PanelTextBox(questNameRect, QuestTranslation.translate(questNameStr)); buttonContainer.addPanel(questName); + if (entry.isRepeatable()) { + GuiRectangle repeatableRect = new GuiRectangle(cachedWidth - repeatableLabelWidth - 4, 6, repeatableLabelWidth, 12); + PanelTextBox repeatableLabel = new PanelTextBox(repeatableRect, QuestTranslation.translate("betterquesting.gui.history.repeatable")); + repeatableLabel.setAlignment(2); + repeatableLabel.setColor(PresetColor.QUEST_LINE_COMPLETE.getColor()); + buttonContainer.addPanel(repeatableLabel); + } + GuiRectangle timestampRect = new GuiRectangle(36, 20, cachedWidth - 36, 10); - PanelTextBox timestamp = new PanelTextBox(timestampRect, formatTimestamp(entry.getCompletionTimestamp())); + PanelTextBox timestamp = new PanelTextBox(timestampRect, getHistoryDetails(entry)); timestamp.setColor(PresetColor.TEXT_AUX_0.getColor()); buttonContainer.addPanel(timestamp); return true; } + private String getHistoryDetails(QuestHistoryEntry entry) { + String timestamp = formatTimestamp(entry.getCompletionTimestamp()); + if (!entry.isRepeatable()) { + return QuestTranslation.translate("betterquesting.gui.history.completed_at", timestamp); + } + + if (entry.hasPendingRewards()) { + return QuestTranslation.translate("betterquesting.gui.history.pending_rewards_at", timestamp); + } + + return QuestTranslation.translate("betterquesting.gui.history.last_completed_at", timestamp); + } + private String formatTimestamp(long timestamp) { if (timestamp <= 0) { return "-"; @@ -138,4 +173,46 @@ private String formatTimestamp(long timestamp) { public void setQuestOpenCallback(Consumer questOpenCallback) { this.questOpenCallback = questOpenCallback; } + + public void setRepeatableFilter(RepeatableFilter repeatableFilter) { + if (this.repeatableFilter == repeatableFilter) { + return; + } + + this.repeatableFilter = repeatableFilter; + refreshSearch(); + updatePanelScroll(); + } + + public enum RepeatableFilter { + HIDE("betterquesting.gui.history.repeatable_filter.hide"), + SHOW_PENDING_REWARDS("betterquesting.gui.history.repeatable_filter.pending_rewards"), + SHOW_ALL("betterquesting.gui.history.repeatable_filter.all"); + + private static final RepeatableFilter[] VALUES = values(); + + private final String translationKey; + + RepeatableFilter(String translationKey) { + this.translationKey = translationKey; + } + + public String getTranslationKey() { + return translationKey; + } + + public static RepeatableFilter fromName(String name) { + for (RepeatableFilter value : VALUES) { + if (value.name().equalsIgnoreCase(name)) { + return value; + } + } + + return SHOW_ALL; + } + + public RepeatableFilter next() { + return VALUES[(ordinal() + 1) % VALUES.length]; + } + } } \ No newline at end of file diff --git a/src/main/java/betterquesting/api2/client/gui/themes/presets/PresetTexture.java b/src/main/java/betterquesting/api2/client/gui/themes/presets/PresetTexture.java index ad6a0b525..20ef088ac 100644 --- a/src/main/java/betterquesting/api2/client/gui/themes/presets/PresetTexture.java +++ b/src/main/java/betterquesting/api2/client/gui/themes/presets/PresetTexture.java @@ -110,9 +110,9 @@ public static void registerTextures(IThemeRegistry reg) { reg.setDefaultTexture(BTN_CLEAN_1.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(120, 0, 12, 12), new GuiPadding(2, 2, 2, 2))); reg.setDefaultTexture(BTN_CLEAN_2.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(132, 0, 12, 12), new GuiPadding(2, 2, 2, 2))); - reg.setDefaultTexture(BTN_ALT_0.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(144, 0, 12, 12), new GuiPadding(2, 2, 2, 2))); - reg.setDefaultTexture(BTN_ALT_1.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(156, 0, 12, 12), new GuiPadding(2, 2, 2, 2))); - reg.setDefaultTexture(BTN_ALT_2.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(178, 0, 12, 12), new GuiPadding(2, 2, 2, 2))); + reg.setDefaultTexture(BTN_ALT_0.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(144, 0, 12, 12), new GuiPadding(2, 2, 2, 3))); + reg.setDefaultTexture(BTN_ALT_1.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(156, 0, 12, 12), new GuiPadding(2, 2, 2, 3))); + reg.setDefaultTexture(BTN_ALT_2.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(168, 0, 12, 12), new GuiPadding(2, 2, 2, 3))); reg.setDefaultTexture(HOTBAR_0.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(190, 0, 12, 12), new GuiPadding(3, 3, 2, 2))); reg.setDefaultTexture(HOTBAR_1.key, new SlicedTexture(TX_SIMPLE, new GuiRectangle(202, 0, 12, 12), new GuiPadding(3, 3, 3, 3))); diff --git a/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java index bc0b4d595..5e25f1a18 100644 --- a/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java +++ b/src/main/java/betterquesting/client/gui2/GuiQuestHistory.java @@ -1,5 +1,6 @@ package betterquesting.client.gui2; +import betterquesting.api.storage.BQ_Settings; import betterquesting.api2.client.gui.GuiScreenCanvas; import betterquesting.api2.client.gui.controls.PanelButton; import betterquesting.api2.client.gui.misc.GuiAlign; @@ -15,15 +16,19 @@ import betterquesting.api2.utils.QuestTranslation; import betterquesting.client.BookmarkManager; import betterquesting.misc.QuestHistoryEntry; +import betterquesting.handlers.ConfigHandler; import net.minecraft.client.gui.GuiScreen; +import net.minecraftforge.common.config.Configuration; import java.util.function.Consumer; public class GuiQuestHistory extends GuiScreenCanvas { private Consumer callback; private CanvasQuestHistory canvasQuestHistory; + private PanelButton repeatableFilterButton; private int historyScrollY; private boolean restoreHistoryScroll; + private CanvasQuestHistory.RepeatableFilter repeatableFilter = CanvasQuestHistory.RepeatableFilter.fromName(BQ_Settings.historyRepeatableFilter); public GuiQuestHistory(GuiScreen parent) { super(parent); @@ -32,8 +37,7 @@ public GuiQuestHistory(GuiScreen parent) { @Override public void initPanel() { super.initPanel(); - GuiTransform bgTransform = new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 0, 0, 0), 0); - CanvasTextured cvBackground = new CanvasTextured(bgTransform, PresetTexture.PANEL_MAIN.getTexture()); + CanvasTextured cvBackground = new CanvasTextured(new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 0, 0, 0), 0), PresetTexture.PANEL_MAIN.getTexture()); this.addPanel(cvBackground); CanvasEmpty cvInner = new CanvasEmpty(new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(8, 8, 8, 8), 0)); @@ -41,15 +45,15 @@ public void initPanel() { createExitButton(cvInner); - GuiTransform titleTransform = new GuiTransform(GuiAlign.TOP_EDGE, new GuiPadding(0, 0, 0, -16), 0); - String title = QuestTranslation.translate("betterquesting.gui.history"); - PanelTextBox txtTitle = new PanelTextBox(titleTransform, title) + PanelTextBox txtTitle = new PanelTextBox(new GuiTransform(GuiAlign.TOP_EDGE, new GuiPadding(0, 0, 0, -16), 0), QuestTranslation.translate("betterquesting.gui.history")) .setAlignment(1) .setColor(PresetColor.TEXT_MAIN.getColor()); cvInner.addPanel(txtTitle); - GuiTransform listTransform = new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 16, 8, 24), 0); - canvasQuestHistory = new CanvasQuestHistory(listTransform, mc.player); + createRepeatableFilterButton(cvInner); + + canvasQuestHistory = new CanvasQuestHistory(new GuiTransform(GuiAlign.FULL_BOX, new GuiPadding(0, 32, 8, 24), 0), mc.player); + canvasQuestHistory.setRepeatableFilter(repeatableFilter); canvasQuestHistory.setQuestOpenCallback(entry -> { saveHistoryScroll(); acceptCallback(entry); @@ -58,16 +62,26 @@ public void initPanel() { }); cvInner.addPanel(canvasQuestHistory); - GuiTransform scTransform = new GuiTransform(GuiAlign.RIGHT_EDGE, new GuiPadding(-8, 16, 0, 24), 0); - PanelVScrollBar scDb = new PanelVScrollBar(scTransform); + PanelVScrollBar scDb = new PanelVScrollBar(new GuiTransform(GuiAlign.RIGHT_EDGE, new GuiPadding(-8, 32, 0, 24), 0)); cvInner.addPanel(scDb); canvasQuestHistory.setScrollDriverY(scDb); restoreHistoryScroll = historyScrollY > 0; } + private void createRepeatableFilterButton(CanvasEmpty cvInner) { + repeatableFilterButton = new PanelButton(new GuiTransform(GuiAlign.TOP_EDGE, new GuiPadding(0, 16, 8, -32), 0), 1, ""); + repeatableFilterButton.setClickAction(button -> cycleRepeatableFilter()); + repeatableFilterButton.setTextures(PresetTexture.BTN_ALT_0.getTexture(), PresetTexture.BTN_ALT_1.getTexture(), PresetTexture.BTN_ALT_2.getTexture()); + repeatableFilterButton.setTextShadow(false); + // Same color for normal and hover; the button's background should be enough to indicate interactivity + repeatableFilterButton.setTextHighlight(PresetColor.BTN_DISABLED.getColor(), PresetColor.TEXT_AUX_0.getColor(), PresetColor.TEXT_AUX_0.getColor()); + + updateRepeatableFilterButton(); + cvInner.addPanel(repeatableFilterButton); + } + private void createExitButton(CanvasEmpty cvInner) { - GuiTransform btnTransform = new GuiTransform(GuiAlign.BOTTOM_CENTER, new GuiPadding(-100, -16, -100, 0), 0); - PanelButton btnExit = new PanelButton(btnTransform, 0, QuestTranslation.translate("gui.back")); + PanelButton btnExit = new PanelButton(new GuiTransform(GuiAlign.BOTTOM_CENTER, new GuiPadding(-100, -16, -100, 0), 0), 0, QuestTranslation.translate("gui.back")); btnExit.setClickAction(b -> mc.displayGuiScreen(parent)); cvInner.addPanel(btnExit); } @@ -82,6 +96,26 @@ private void acceptCallback(QuestHistoryEntry entry) { } } + private void cycleRepeatableFilter() { + repeatableFilter = repeatableFilter.next(); + BQ_Settings.historyRepeatableFilter = repeatableFilter.name(); + ConfigHandler.config.get(Configuration.CATEGORY_GENERAL, "History Repeatable Filter", "SHOW_ALL").set(BQ_Settings.historyRepeatableFilter); + ConfigHandler.config.save(); + + saveHistoryScroll(); + restoreHistoryScroll = historyScrollY > 0; + updateRepeatableFilterButton(); + + canvasQuestHistory.setRepeatableFilter(repeatableFilter); + } + + private void updateRepeatableFilterButton() { + String filterLabel = QuestTranslation.translate(repeatableFilter.getTranslationKey()); + if (repeatableFilterButton != null) { + repeatableFilterButton.setText(QuestTranslation.translate("betterquesting.gui.history.repeatable_filter", filterLabel)); + } + } + @Override public void drawPanel(int mx, int my, float partialTick) { super.drawPanel(mx, my, partialTick); @@ -128,6 +162,11 @@ private void saveHistoryScroll() { return; } + // Keep the pending restore target intact while the list is rebuilding its filtered results. + if (restoreHistoryScroll) { + return; + } + historyScrollY = canvasQuestHistory.getScrollY(); } } \ No newline at end of file diff --git a/src/main/java/betterquesting/handlers/ConfigHandler.java b/src/main/java/betterquesting/handlers/ConfigHandler.java index 35d000ce1..56ac31331 100644 --- a/src/main/java/betterquesting/handlers/ConfigHandler.java +++ b/src/main/java/betterquesting/handlers/ConfigHandler.java @@ -38,6 +38,7 @@ public static void initConfigs() { BQ_Settings.lockTray = config.getBoolean("Lock Tray", Configuration.CATEGORY_GENERAL, false, "If true, locks the quest chapter list and opens it initially"); BQ_Settings.skipHome = config.getBoolean("Skip Home", Configuration.CATEGORY_GENERAL, false, "If true, skip the home GUI and open quests at startup. This property will be changed by the mod itself."); BQ_Settings.viewMode = config.getBoolean("View mode", Configuration.CATEGORY_GENERAL, false, "If view mode enabled, User can view all quests"); + BQ_Settings.historyRepeatableFilter = config.getString("History Repeatable Filter", Configuration.CATEGORY_GENERAL, "SHOW_ALL", "The repeatable quest filter used by the history screen"); BQ_Settings.limitBack = config.getBoolean("Limit Back", Configuration.CATEGORY_GENERAL, false, "If true, the back keybind will not return to the home screen"); BQ_Settings.defaultVisibility = config.getString("Default Quest Visibility", Configuration.CATEGORY_GENERAL, "NORMAL", "The default visibility value used when creating quests"); diff --git a/src/main/java/betterquesting/misc/QuestHistoryEntry.java b/src/main/java/betterquesting/misc/QuestHistoryEntry.java index 533e2b760..8600b2778 100644 --- a/src/main/java/betterquesting/misc/QuestHistoryEntry.java +++ b/src/main/java/betterquesting/misc/QuestHistoryEntry.java @@ -6,13 +6,25 @@ public class QuestHistoryEntry extends QuestSearchEntry { private final long completionTimestamp; + private final boolean repeatable; + private final boolean pendingRewards; - public QuestHistoryEntry(DBEntry quest, DBEntry questLineEntry, long completionTimestamp) { + public QuestHistoryEntry(DBEntry quest, DBEntry questLineEntry, long completionTimestamp, boolean repeatable, boolean pendingRewards) { super(quest, questLineEntry); this.completionTimestamp = completionTimestamp; + this.repeatable = repeatable; + this.pendingRewards = pendingRewards; } public long getCompletionTimestamp() { return completionTimestamp; } + + public boolean isRepeatable() { + return repeatable; + } + + public boolean hasPendingRewards() { + return pendingRewards; + } } \ No newline at end of file diff --git a/src/main/java/betterquesting/questing/QuestInstance.java b/src/main/java/betterquesting/questing/QuestInstance.java index e63a8825d..2735c172d 100644 --- a/src/main/java/betterquesting/questing/QuestInstance.java +++ b/src/main/java/betterquesting/questing/QuestInstance.java @@ -208,14 +208,18 @@ public void claimReward(EntityPlayer player) { synchronized (completeUsers) { NBTTagCompound entry = getCompletionInfo(pID); + long timestamp = System.currentTimeMillis(); if (entry == null) { entry = new NBTTagCompound(); this.completeUsers.put(pID, entry); } + // Protect against misuse, if setComplete wasn't called (for some reason). Should "never" happen. + ensureLastCompletedAt(entry, timestamp); + entry.setBoolean("claimed", true); - entry.setLong("timestamp", System.currentTimeMillis()); + entry.setLong("timestamp", timestamp); DirtyPlayerMarker.markDirty(pID); } @@ -232,6 +236,10 @@ public boolean canSubmit(@Nonnull EntityPlayer player) { if (!entry.getBoolean("claimed") && getProperty(NativeProps.REPEAT_TIME) >= 0) // Complete but repeatable { + // A reset repeatable keeps its completion record for history, but its submit timestamp is cleared. + // Keep reporting it as submittable until update() can stamp the new completion time. + if (entry.getLong("timestamp") <= 0) return true; + if (tasks.size() <= 0) return true; int done = 0; @@ -279,6 +287,7 @@ public void setComplete(UUID uuid, long timestamp) { entry.setBoolean("claimed", false); entry.setLong("timestamp", timestamp); + entry.setLong(IQuest.LAST_COMPLETED_AT_TAG, timestamp); DirtyPlayerMarker.markDirty(uuid); } } @@ -512,18 +521,28 @@ public void setClaimed(UUID uuid, long timestamp) { NBTTagCompound entry = this.getCompletionInfo(uuid); if (entry != null) { + ensureLastCompletedAt(entry, entry.getLong("timestamp")); entry.setBoolean("claimed", true); entry.setLong("timestamp", timestamp); } else { entry = new NBTTagCompound(); entry.setBoolean("claimed", true); entry.setLong("timestamp", timestamp); + entry.setLong(IQuest.LAST_COMPLETED_AT_TAG, timestamp); completeUsers.put(uuid, entry); } DirtyPlayerMarker.markDirty(uuid); } } + private void ensureLastCompletedAt(NBTTagCompound entry, long fallbackTimestamp) { + if (entry.hasKey(IQuest.LAST_COMPLETED_AT_TAG, Constants.NBT.TAG_LONG)) { + return; + } + + entry.setLong(IQuest.LAST_COMPLETED_AT_TAG, fallbackTimestamp); + } + @Override public T getProperty(IPropertyType prop) { return qInfo.getProperty(prop); diff --git a/src/main/resources/assets/betterquesting/lang/en_us.lang b/src/main/resources/assets/betterquesting/lang/en_us.lang index 8958b1d23..d16b0f7a8 100644 --- a/src/main/resources/assets/betterquesting/lang/en_us.lang +++ b/src/main/resources/assets/betterquesting/lang/en_us.lang @@ -85,7 +85,15 @@ betterquesting.gui.quest_line=Quest Line betterquesting.gui.database=Database betterquesting.gui.selection=Selection betterquesting.gui.search=Search -betterquesting.gui.history=Quest History +betterquesting.gui.history=Quest Completion History +betterquesting.gui.history.repeatable_filter=Repeatable Quests: %s +betterquesting.gui.history.repeatable_filter.hide=Hide +betterquesting.gui.history.repeatable_filter.pending_rewards=Show Pending Rewards +betterquesting.gui.history.repeatable_filter.all=Show All +betterquesting.gui.history.repeatable=Repeatable +betterquesting.gui.history.completed_at=Completed: %s +betterquesting.gui.history.pending_rewards_at=Pending Rewards: %s +betterquesting.gui.history.last_completed_at=Last Completed: %s betterquesting.gui.folder=Folder betterquesting.gui.party_invites=Invites betterquesting.gui.party_members=Members