diff --git a/src/main/java/betterquesting/api2/client/gui/controls/PanelButtonQuest.java b/src/main/java/betterquesting/api2/client/gui/controls/PanelButtonQuest.java index 2ab6f5ec1..727420f2b 100644 --- a/src/main/java/betterquesting/api2/client/gui/controls/PanelButtonQuest.java +++ b/src/main/java/betterquesting/api2/client/gui/controls/PanelButtonQuest.java @@ -100,7 +100,7 @@ private List getQuestTooltip(IQuest quest, EntityPlayer player, int qID) private List getStandardTooltip(IQuest quest, EntityPlayer player, int qID) { List list = new ArrayList<>(); - list.add(QuestTranslation.translate(quest.getProperty(NativeProps.NAME)) + (!Minecraft.getMinecraft().gameSettings.advancedItemTooltips ? "" : (" #" + qID))); + list.add(QuestTranslation.translate(quest.getProperty(NativeProps.NAME)) + (Minecraft.getMinecraft().gameSettings.advancedItemTooltips && QuestSettings.INSTANCE.getProperty(NativeProps.EDIT_MODE) ? (" #" + qID) : "")); UUID playerID = QuestingAPI.getQuestingUUID(player); diff --git a/src/main/java/betterquesting/api2/storage/AbstractDatabase.java b/src/main/java/betterquesting/api2/storage/AbstractDatabase.java new file mode 100644 index 000000000..b414e9e62 --- /dev/null +++ b/src/main/java/betterquesting/api2/storage/AbstractDatabase.java @@ -0,0 +1,127 @@ +package betterquesting.api2.storage; + +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; + +public abstract class AbstractDatabase implements IDatabase { + + /** + * If the cache size would somehow exceed 24MB (on 64bit machines) we stop. + */ + public static int CACHE_MAX_SIZE = 24 * 1024 * 1024 / 8; + + /** + * If {@code mapDB.size < SPARSE_RATIO * (mapDB.lastKey() - mapDB.firstKey())} the database will be considered + * sparse and an cache array won't be built to save memory. + *

+ * Under this sparsity a 10k element database will roughly result in a 0.5MB cache which is more than enough reasonable. + */ + public static double SPARSE_RATIO = 0.15d; + + final TreeMap mapDB = new TreeMap<>(); + + private LookupLogicType type = null; + private LookupLogic logic = null; + + private LookupLogic getLookupLogic() { + if (type != null) + return logic; + LookupLogicType newType = LookupLogicType.determine(this); + type = newType; + logic = newType.get(this); + return logic; + } + + private void updateLookupLogic() { + if (type == null) + return; + LookupLogicType newType = LookupLogicType.determine(this); + if (newType != type) { + type = null; + logic = null; + } else { + logic.onDataChange(); + } + } + + @Override + public synchronized DBEntry add(int id, T value) { + if (value == null) { + throw new NullPointerException("Value cannot be null"); + } else if (id < 0) { + throw new IllegalArgumentException("ID cannot be negative"); + } else { + if (mapDB.putIfAbsent(id, value) == null) { + updateLookupLogic(); + return new DBEntry<>(id, value); + } else { + throw new IllegalArgumentException("ID or value is already contained within database"); + } + } + } + + @Override + public synchronized boolean removeID(int key) { + if (key < 0) + return false; + + if (mapDB.remove(key) != null) { + updateLookupLogic(); + return true; + } + return false; + } + + @Override + public synchronized boolean removeValue(T value) { + return value != null && removeID(getID(value)); + } + + @Override + public synchronized int getID(T value) { + if (value == null) + return -1; + + for (DBEntry entry : getEntries()) { + if (entry.getValue() == value) + return entry.getID(); + } + + return -1; + } + + @Override + public synchronized T getValue(int id) { + if (id < 0 || mapDB.size() <= 0) + return null; + return mapDB.get(id); + } + + @Override + public synchronized int size() { + return mapDB.size(); + } + + @Override + public synchronized void reset() { + mapDB.clear(); + type = null; + logic = null; + } + + @Override + public synchronized List> getEntries() { + return mapDB.isEmpty() ? Collections.emptyList() : getLookupLogic().getRefCache(); + } + + /** + * First try to use array cache. + * If memory usage would be too high try use sort merge join if keys is large. + * Otherwise look up each key separately via {@link TreeMap#get(Object)}. + */ + @Override + public synchronized List> bulkLookup(int... keys) { + return mapDB.isEmpty() || keys.length == 0 ? Collections.emptyList() : getLookupLogic().bulkLookup(keys); + } +} diff --git a/src/main/java/betterquesting/api2/storage/ArrayCacheLookupLogic.java b/src/main/java/betterquesting/api2/storage/ArrayCacheLookupLogic.java index 7fc2a7886..6d7ac3e9e 100644 --- a/src/main/java/betterquesting/api2/storage/ArrayCacheLookupLogic.java +++ b/src/main/java/betterquesting/api2/storage/ArrayCacheLookupLogic.java @@ -8,8 +8,8 @@ class ArrayCacheLookupLogic extends LookupLogic { private DBEntry[] cache = null; private int offset = -1; - public ArrayCacheLookupLogic(SimpleDatabase simpleDatabase) { - super(simpleDatabase); + public ArrayCacheLookupLogic(AbstractDatabase abstractDatabase) { + super(abstractDatabase); } @Override @@ -48,10 +48,10 @@ public List> bulkLookup(int[] keys) { @SuppressWarnings("unchecked") private void computeCache() { if (cache != null) return; - cache = new DBEntry[simpleDatabase.mapDB.lastKey() - simpleDatabase.mapDB.firstKey() + 1]; - offset = simpleDatabase.mapDB.firstKey(); + cache = new DBEntry[abstractDatabase.mapDB.lastKey() - abstractDatabase.mapDB.firstKey() + 1]; + offset = abstractDatabase.mapDB.firstKey(); if (refCache == null) { - for (Map.Entry entry : simpleDatabase.mapDB.entrySet()) { + for (Map.Entry entry : abstractDatabase.mapDB.entrySet()) { cache[entry.getKey() - offset] = new DBEntry<>(entry.getKey(), entry.getValue()); } } else { diff --git a/src/main/java/betterquesting/api2/storage/EmptyLookupLogic.java b/src/main/java/betterquesting/api2/storage/EmptyLookupLogic.java index f52d98582..e816f3264 100644 --- a/src/main/java/betterquesting/api2/storage/EmptyLookupLogic.java +++ b/src/main/java/betterquesting/api2/storage/EmptyLookupLogic.java @@ -5,8 +5,8 @@ public class EmptyLookupLogic extends LookupLogic { - public EmptyLookupLogic(SimpleDatabase simpleDatabase) { - super(simpleDatabase); + public EmptyLookupLogic(AbstractDatabase abstractDatabase) { + super(abstractDatabase); } @Override diff --git a/src/main/java/betterquesting/api2/storage/LookupLogic.java b/src/main/java/betterquesting/api2/storage/LookupLogic.java index 695959f4c..2d11d854b 100644 --- a/src/main/java/betterquesting/api2/storage/LookupLogic.java +++ b/src/main/java/betterquesting/api2/storage/LookupLogic.java @@ -7,11 +7,11 @@ abstract class LookupLogic { - protected final SimpleDatabase simpleDatabase; + protected final AbstractDatabase abstractDatabase; protected List> refCache = null; - public LookupLogic(SimpleDatabase simpleDatabase) { - this.simpleDatabase = simpleDatabase; + public LookupLogic(AbstractDatabase abstractDatabase) { + this.abstractDatabase = abstractDatabase; } public void onDataChange() { @@ -28,7 +28,7 @@ public List> getRefCache() { protected void computeRefCache() { List> temp = new ArrayList<>(); - for (Map.Entry entry : simpleDatabase.mapDB.entrySet()) { + for (Map.Entry entry : abstractDatabase.mapDB.entrySet()) { temp.add(new DBEntry<>(entry.getKey(), entry.getValue())); } refCache = Collections.unmodifiableList(temp); diff --git a/src/main/java/betterquesting/api2/storage/LookupLogicType.java b/src/main/java/betterquesting/api2/storage/LookupLogicType.java index 034c16ed6..1c91e6fe1 100644 --- a/src/main/java/betterquesting/api2/storage/LookupLogicType.java +++ b/src/main/java/betterquesting/api2/storage/LookupLogicType.java @@ -11,15 +11,15 @@ enum LookupLogicType { Empty(db -> db.mapDB.isEmpty(), EmptyLookupLogic::new), ArrayCache(db -> db.mapDB.size() < CACHE_MAX_SIZE && db.mapDB.size() > SPARSE_RATIO * (db.mapDB.lastKey() - db.mapDB.firstKey()), ArrayCacheLookupLogic::new), Naive(db -> true, NaiveLookupLogic::new); - private final Predicate> shouldUse; - private final Function, LookupLogic> factory; + private final Predicate> shouldUse; + private final Function, LookupLogic> factory; - LookupLogicType(Predicate> shouldUse, Function, LookupLogic> factory) { + LookupLogicType(Predicate> shouldUse, Function, LookupLogic> factory) { this.shouldUse = shouldUse; this.factory = factory; } - static LookupLogicType determine(SimpleDatabase db) { + static LookupLogicType determine(AbstractDatabase db) { for (LookupLogicType type : values()) { if (type.shouldUse.test(db)) return type; @@ -28,7 +28,7 @@ static LookupLogicType determine(SimpleDatabase db) { } @SuppressWarnings("unchecked") - LookupLogic get(SimpleDatabase db) { + LookupLogic get(AbstractDatabase db) { return (LookupLogic) factory.apply(db); } } diff --git a/src/main/java/betterquesting/api2/storage/NaiveLookupLogic.java b/src/main/java/betterquesting/api2/storage/NaiveLookupLogic.java index 1d889d64e..1d5c101f0 100644 --- a/src/main/java/betterquesting/api2/storage/NaiveLookupLogic.java +++ b/src/main/java/betterquesting/api2/storage/NaiveLookupLogic.java @@ -10,8 +10,8 @@ class NaiveLookupLogic extends LookupLogic { private TIntObjectMap> backingMap; - public NaiveLookupLogic(SimpleDatabase simpleDatabase) { - super(simpleDatabase); + public NaiveLookupLogic(AbstractDatabase abstractDatabase) { + super(abstractDatabase); } @Override @@ -23,7 +23,7 @@ public void onDataChange() { @Override public List> bulkLookup(int[] keys) { if (backingMap == null) { - backingMap = new TIntObjectHashMap<>(simpleDatabase.mapDB.size()); + backingMap = new TIntObjectHashMap<>(abstractDatabase.mapDB.size()); for (DBEntry entry : getRefCache()) { backingMap.put(entry.getID(), entry); } diff --git a/src/main/java/betterquesting/api2/storage/RandomIndexDatabase.java b/src/main/java/betterquesting/api2/storage/RandomIndexDatabase.java new file mode 100644 index 000000000..c493486c1 --- /dev/null +++ b/src/main/java/betterquesting/api2/storage/RandomIndexDatabase.java @@ -0,0 +1,22 @@ +package betterquesting.api2.storage; + +import java.util.Random; + +public class RandomIndexDatabase extends AbstractDatabase { + + private final Random random = new Random(); + + @Override + public synchronized int nextID() { + int id; + do { + // id >= 0 + id = random.nextInt() & 0x7fff_ffff; + } + // The new id doesn't conflict with existing ones. + // However, new ids created by different players could conflict with each other. + while (mapDB.containsKey(id)); + return id; + } + +} diff --git a/src/main/java/betterquesting/api2/storage/SimpleDatabase.java b/src/main/java/betterquesting/api2/storage/SimpleDatabase.java index fd2e9c7f2..1de0070ed 100644 --- a/src/main/java/betterquesting/api2/storage/SimpleDatabase.java +++ b/src/main/java/betterquesting/api2/storage/SimpleDatabase.java @@ -5,45 +5,9 @@ import java.util.List; import java.util.TreeMap; -public class SimpleDatabase implements IDatabase { - - /** - * If the cache size would somehow exceed 24MB (on 64bit machines) we stop. - */ - public static int CACHE_MAX_SIZE = 24 * 1024 * 1024 / 8; - - /** - * If {@code mapDB.size < SPARSE_RATIO * (mapDB.lastKey() - mapDB.firstKey())} the database will be considered - * sparse and an cache array won't be built to save memory. - *

- * Under this sparsity a 10k element database will roughly result in a 0.5MB cache which is more than enough reasonable. - */ - public static double SPARSE_RATIO = 0.15d; - - final TreeMap mapDB = new TreeMap<>(); +public class SimpleDatabase extends AbstractDatabase { private final BitSet idMap = new BitSet(); - private LookupLogicType type = null; - private LookupLogic logic = null; - - private LookupLogic getLookupLogic() { - if (type != null) return logic; - LookupLogicType newType = LookupLogicType.determine(this); - type = newType; - logic = newType.get(this); - return logic; - } - - private void updateLookupLogic() { - if (type == null) return; - LookupLogicType newType = LookupLogicType.determine(this); - if (newType != type) { - type = null; - logic = null; - } else { - logic.onDataChange(); - } - } @Override public synchronized int nextID() { @@ -52,80 +16,23 @@ public synchronized int nextID() { @Override public synchronized DBEntry add(int id, T value) { - if (value == null) { - throw new NullPointerException("Value cannot be null"); - } else if (id < 0) { - throw new IllegalArgumentException("ID cannot be negative"); - } else { - if (mapDB.putIfAbsent(id, value) == null) { - idMap.set(id); - updateLookupLogic(); - return new DBEntry<>(id, value); - } else { - throw new IllegalArgumentException("ID or value is already contained within database"); - } - } + DBEntry result = super.add(id, value); + // Don't add when an exception is thrown + idMap.set(id); + return result; } @Override public synchronized boolean removeID(int key) { - if (key < 0) return false; - - if (mapDB.remove(key) != null) { - idMap.clear(key); - updateLookupLogic(); - return true; - } - return false; - } - - @Override - public synchronized boolean removeValue(T value) { - return value != null && removeID(getID(value)); - } - - @Override - public synchronized int getID(T value) { - if (value == null) return -1; - - for (DBEntry entry : getEntries()) { - if (entry.getValue() == value) return entry.getID(); - } - - return -1; - } - - @Override - public synchronized T getValue(int id) { - if (id < 0 || mapDB.size() <= 0) return null; - return mapDB.get(id); - } - - @Override - public synchronized int size() { - return mapDB.size(); + boolean result = super.removeID(key); + if (result) idMap.clear(key); + return result; } @Override public synchronized void reset() { - mapDB.clear(); + super.reset(); idMap.clear(); - type = null; - logic = null; - } - - @Override - public synchronized List> getEntries() { - return mapDB.isEmpty() ? Collections.emptyList() : getLookupLogic().getRefCache(); } - /** - * First try to use array cache. - * If memory usage would be too high try use sort merge join if keys is large. - * Otherwise look up each key separately via {@link TreeMap#get(Object)}. - */ - @Override - public synchronized List> bulkLookup(int... keys) { - return mapDB.isEmpty() || keys.length == 0 ? Collections.emptyList() : getLookupLogic().bulkLookup(keys); - } } diff --git a/src/main/java/betterquesting/client/gui2/GuiQuestLines.java b/src/main/java/betterquesting/client/gui2/GuiQuestLines.java index da5359584..7f638c1fa 100644 --- a/src/main/java/betterquesting/client/gui2/GuiQuestLines.java +++ b/src/main/java/betterquesting/client/gui2/GuiQuestLines.java @@ -333,7 +333,7 @@ public boolean onMouseClick(int mx, int my, int click) { maxWidth = Math.max(maxWidth, Math.max(RenderUtils.getStringWidth(QuestTranslation.translate("betterquesting.btn.edit"), fr), RenderUtils.getStringWidth(QuestTranslation.translate("betterquesting.btn.designer"), fr))); } - PopContextMenu popup = new PopContextMenu(new GuiRectangle(mx, my, maxWidth + 12, questExistsUnderMouse ? 48 : 16), true); + PopContextMenu popup = new PopContextMenu(new GuiRectangle(mx, my, maxWidth + 12, questExistsUnderMouse ? 64 : 16), true); if (canEdit) { if (questExistsUnderMouse) { GuiQuestEditor editor = new GuiQuestEditor(new GuiQuestLines(parent), cvQuest.getButtonAt(mx, my).getStoredValue().getID()); @@ -350,6 +350,18 @@ public boolean onMouseClick(int mx, int my, int click) { mc.displayGuiScreen(null); }; popup.addButton(QuestTranslation.translate("betterquesting.btn.share_quest"), null, questSharer); + + Runnable questId = () -> { + String id = String.valueOf(cvQuest.getButtonAt(mx, my).getStoredValue().getID()); + try { + GuiScreen.setClipboardString(id); + mc.player.sendMessage(new TextComponentTranslation("betterquesting.msg.copy_quest_copied", id)); + closePopup(); + } catch (IllegalStateException e) { + mc.player.sendMessage(new TextComponentTranslation("betterquesting.msg.copy_quest_failed", id)); + } + }; + popup.addButton(QuestTranslation.translate("betterquesting.btn.copy_id"), null, questId); } openPopup(popup); return true; diff --git a/src/main/java/betterquesting/client/toolbox/tools/ToolboxToolCopy.java b/src/main/java/betterquesting/client/toolbox/tools/ToolboxToolCopy.java index 556945938..5130a245f 100644 --- a/src/main/java/betterquesting/client/toolbox/tools/ToolboxToolCopy.java +++ b/src/main/java/betterquesting/client/toolbox/tools/ToolboxToolCopy.java @@ -177,25 +177,10 @@ public boolean onMouseClick(int mx, int my, int click) { } private int[] getNextIDs(int num) { - List> listDB = QuestDatabase.INSTANCE.getEntries(); int[] nxtIDs = new int[num]; - - if (listDB.size() <= 0 || listDB.get(listDB.size() - 1).getID() == listDB.size() - 1) { - for (int i = 0; i < num; i++) nxtIDs[i] = listDB.size() + i; - return nxtIDs; - } - - int n1 = 0; - int n2 = 0; for (int i = 0; i < num; i++) { - while (n2 < listDB.size() && listDB.get(n2).getID() == n1) { - n1++; - n2++; - } - - nxtIDs[i] = n1++; + nxtIDs[i] = QuestDatabase.INSTANCE.nextID(); } - return nxtIDs; } diff --git a/src/main/java/betterquesting/handlers/EventHandler.java b/src/main/java/betterquesting/handlers/EventHandler.java index 3f3110c4f..706f74538 100644 --- a/src/main/java/betterquesting/handlers/EventHandler.java +++ b/src/main/java/betterquesting/handlers/EventHandler.java @@ -139,7 +139,7 @@ public void onClientChatReceived(ClientChatReceivedEvent event) { return; } String questName = quest.getProperty(NativeProps.NAME); - ITextComponent translated = new TextComponentTranslation("betterquesting.msg.share_quest", questId, questName); + ITextComponent translated = new TextComponentTranslation("betterquesting.msg.share_quest", questName); ITextComponent newMessage = new TextComponentString(text.substring(0, index) + translated.getFormattedText() + text.substring(endIndex)); Style newMessageStyle; EntityPlayerSP player = Minecraft.getMinecraft().player; diff --git a/src/main/java/betterquesting/importers/ftbq/FTBQQuestImporter.java b/src/main/java/betterquesting/importers/ftbq/FTBQQuestImporter.java index 2ab692aa6..3808e1dd1 100644 --- a/src/main/java/betterquesting/importers/ftbq/FTBQQuestImporter.java +++ b/src/main/java/betterquesting/importers/ftbq/FTBQQuestImporter.java @@ -165,7 +165,14 @@ private void startImport(IQuestDatabase questDB, IQuestLineDatabase lineDB, NBTT // === QUEST DATA === String hexID = questFile.getName().substring(0, questFile.getName().length() - (isSnbt ? ".snbt".length() : ".nbt".length())); - int questID = questDB.nextID(); + int questID; + try { + questID = Integer.parseInt(hexID, 16) & 0x7fff_ffff; + if (lineDB.getValue(questID) != null) + questID = questDB.nextID(); + } catch (Exception e) { + questID = questDB.nextID(); + } IQuest quest = questDB.createNew(questID); IQuestLineEntry qle = questLine.createNew(questID); ID_MAP.put(hexID, new FTBEntry(questID, quest, FTBEntryType.QUEST)); // Add this to the weird ass ID mapping diff --git a/src/main/java/betterquesting/importers/hqm/HQMQuestImporter.java b/src/main/java/betterquesting/importers/hqm/HQMQuestImporter.java index 7303ed903..c65878049 100644 --- a/src/main/java/betterquesting/importers/hqm/HQMQuestImporter.java +++ b/src/main/java/betterquesting/importers/hqm/HQMQuestImporter.java @@ -28,10 +28,7 @@ import java.io.FileInputStream; import java.io.InputStreamReader; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; +import java.util.*; import java.util.Map.Entry; import java.util.concurrent.Future; import java.util.function.Function; @@ -169,7 +166,14 @@ private IQuest GetNewQuest(String oldID, IQuestDatabase qdb) { if (idMap.containsKey(oldID)) { return idMap.get(oldID); } else { - IQuest quest = qdb.createNew(qdb.nextID()); + int newID; + try { + newID = (int) (UUID.fromString(oldID).getMostSignificantBits() & 0x7fff_ffff); + if (qdb.getValue(newID) != null) newID = qdb.nextID(); + } catch (Exception e) { + newID = qdb.nextID(); + } + IQuest quest = qdb.createNew(newID); idMap.put(oldID, quest); return quest; } diff --git a/src/main/java/betterquesting/questing/QuestDatabase.java b/src/main/java/betterquesting/questing/QuestDatabase.java index 37377d96a..d20df64fe 100644 --- a/src/main/java/betterquesting/questing/QuestDatabase.java +++ b/src/main/java/betterquesting/questing/QuestDatabase.java @@ -3,7 +3,7 @@ import betterquesting.api.questing.IQuest; import betterquesting.api.questing.IQuestDatabase; import betterquesting.api2.storage.DBEntry; -import betterquesting.api2.storage.SimpleDatabase; +import betterquesting.api2.storage.RandomIndexDatabase; import net.minecraft.nbt.NBTTagCompound; import net.minecraft.nbt.NBTTagList; @@ -11,7 +11,7 @@ import java.util.List; import java.util.UUID; -public final class QuestDatabase extends SimpleDatabase implements IQuestDatabase { +public final class QuestDatabase extends RandomIndexDatabase implements IQuestDatabase { public static final QuestDatabase INSTANCE = new QuestDatabase(); @Override diff --git a/src/main/resources/assets/betterquesting/lang/en_us.lang b/src/main/resources/assets/betterquesting/lang/en_us.lang index fcd2e73a2..1eeddcafb 100644 --- a/src/main/resources/assets/betterquesting/lang/en_us.lang +++ b/src/main/resources/assets/betterquesting/lang/en_us.lang @@ -65,6 +65,7 @@ betterquesting.btn.visible_always=Always show dependency line betterquesting.btn.visible_implicit=Hover to show dependency line betterquesting.btn.view_mode=View Mode betterquesting.btn.share_quest=Share to Chat +betterquesting.btn.copy_id=Copy Quest ID betterquesting.btn.edit_name_desc.just_close=Just Close betterquesting.btn.edit_name_desc.open_window=Open Window @@ -120,10 +121,12 @@ betterquesting.notice.update=Quest Updated betterquesting.notice.unlock=Quest Unlocked betterquesting.msg.heart_disabled=Hardcore lives are not enabled. Use "/bq_admin hardcore" to toggle it -betterquesting.msg.share_quest=§b[Quest: #%d - %s§b] +betterquesting.msg.share_quest=§b[Quest: %s§b] betterquesting.msg.share_quest_invalid=§cCannot view Quest %s betterquesting.msg.share_quest_hover_text_success=§aClick to View Quest betterquesting.msg.share_quest_hover_text_failure=§cQuest is Currently Inaccessible +betterquesting.msg.copy_quest_copied=§eCopied to clipboard: §b%s +betterquesting.msg.copy_quest_failed=§cCould not copy §b%s§r§c to clipboard! key.betterquesting.quests=Open Quests key.betterquesting.party=Party Manager