improve GameDataCategory lookup time

by changing it to something backed by a HashMap with
 the id as key things can be found without looping over every element.
 This reduced the startup time on my machine to about half of the original time
This commit is contained in:
OMGeeky
2025-08-31 19:18:30 +02:00
parent 297bff84db
commit 703b723322
9 changed files with 147 additions and 103 deletions

View File

@@ -290,7 +290,7 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
public void linkAll() {
for (ProjectTreeNode node : baseContent.gameData.v.getNonEmptyIterable()) {
if (node instanceof GameDataCategory<?>) {
for (GameDataElement e : ((GameDataCategory<?>) node)) {
for (GameDataElement e : ((GameDataCategory<?>) node).toList()) {
e.link();
}
}
@@ -300,7 +300,7 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
}
for (ProjectTreeNode node : alteredContent.gameData.v.getNonEmptyIterable()) {
if (node instanceof GameDataCategory<?>) {
for (GameDataElement e : ((GameDataCategory<?>) node)) {
for (GameDataElement e : ((GameDataCategory<?>) node).toList()) {
e.link();
}
}
@@ -310,7 +310,7 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
}
for (ProjectTreeNode node : createdContent.gameData.v.getNonEmptyIterable()) {
if (node instanceof GameDataCategory<?>) {
for (GameDataElement e : ((GameDataCategory<?>) node)) {
for (GameDataElement e : ((GameDataCategory<?>) node).toList()) {
e.link();
}
}
@@ -937,14 +937,14 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
public void moveToCreated(JSONElement target) {
target.childrenRemoved(new ArrayList<ProjectTreeNode>());
((GameDataCategory<?>) target.getParent()).remove(target);
((GameDataCategory<?>) target.getParent()).removeGeneric(target);
target.state = GameDataElement.State.created;
createdContent.gameData.addElement(target);
}
public void moveToAltered(JSONElement target) {
target.childrenRemoved(new ArrayList<ProjectTreeNode>());
((GameDataCategory<?>) target.getParent()).remove(target);
((GameDataCategory<?>) target.getParent()).removeGeneric(target);
target.state = GameDataElement.State.created;
((JSONElement) target).jsonFile = new File(baseContent.gameData.getGameDataElement(((JSONElement) target).getClass(), target.id).jsonFile.getAbsolutePath());
alteredContent.gameData.addElement((JSONElement) target);
@@ -1202,18 +1202,18 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
public List<String> writeDataDeltaForDataType(GameDataCategory<? extends JSONElement> created, GameDataCategory<? extends JSONElement> altered, GameDataCategory<? extends JSONElement> source, Class<? extends JSONElement> gdeClass, File targetFolder) {
List<String> filenamesToWrite = new LinkedList<String>();
Map<String, List<Map>> dataToWritePerFilename = new LinkedHashMap<String, List<Map>>();
for (JSONElement gde : altered) {
for (JSONElement gde : altered.toList()) {
if (!filenamesToWrite.contains(gde.jsonFile.getName())) {
filenamesToWrite.add(gde.jsonFile.getName());
}
}
for (JSONElement gde : created) {
for (JSONElement gde : created.toList()) {
if (!filenamesToWrite.contains(gde.jsonFile.getName())) {
filenamesToWrite.add(gde.jsonFile.getName());
}
}
for (String fName : filenamesToWrite) {
for (JSONElement gde : source) {
for (JSONElement gde : source.toList()) {
if (gde.jsonFile.getName().equals(fName)) {
if (dataToWritePerFilename.get(fName) == null) {
dataToWritePerFilename.put(fName, new ArrayList<Map>());
@@ -1222,7 +1222,7 @@ public class Project implements ProjectTreeNode, Serializable, JsonSerializable
dataToWritePerFilename.get(fName).add(getGameDataElement(gdeClass, gde.id).toJson());
}
}
for (JSONElement gde : created) {
for (JSONElement gde : created.toList()) {
if (gde.jsonFile.getName().equals(fName)) {
if (dataToWritePerFilename.get(fName) == null) {
dataToWritePerFilename.put(fName, new ArrayList<Map>());

View File

@@ -1,32 +1,115 @@
package com.gpl.rpg.atcontentstudio.model.gamedata;
import com.gpl.rpg.atcontentstudio.Notification;
import com.gpl.rpg.atcontentstudio.io.JsonPrettyWriter;
import com.gpl.rpg.atcontentstudio.model.*;
import com.gpl.rpg.atcontentstudio.model.GameSource.Type;
import com.gpl.rpg.atcontentstudio.ui.DefaultIcons;
import com.gpl.rpg.atcontentstudio.utils.FileUtils;
import org.json.simple.JSONArray;
import javax.swing.tree.TreeNode;
import java.awt.*;
import java.io.*;
import java.util.List;
import java.io.File;
import java.util.*;
import java.util.List;
public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implements ProjectTreeNode, Serializable {
public class GameDataCategory<E extends JSONElement> implements ProjectTreeNode {
//region Data
private final ArrayList<String> keyList = new ArrayList<>();
private final HashMap<String, E> dataMap = new HashMap<>();
//endregion
private static final long serialVersionUID = 5486008219704443733L;
public GameDataSet parent;
public String name;
public GameDataCategory(GameDataSet parent, String name) {
super();
this.parent = parent;
this.name = name;
}
//region Helpers
public E get(String key) {
return dataMap.get(key);
}
public E get(int index) {
String key = keyList.get(index);
return dataMap.get(key);
}
public E getIgnoreCase(String key) {
for (String k : keyList) {
if (k.equalsIgnoreCase(key)) {
return dataMap.get(k);
}
}
return null;
}
public E put(String key, E element) {
if (!dataMap.containsKey(key)) {
keyList.add(key);
}
return dataMap.put(key, element);
}
public void add(E quest) {
String key = quest.id;
put(key, quest);
}
public E remove(String key) {
if (dataMap.containsKey(key)) {
keyList.remove(key);
}
return dataMap.remove(key);
}
public E remove(int index) {
String key = keyList.get(index);
keyList.remove(index);
return dataMap.remove(key);
}
public boolean removeGeneric(JSONElement element){
return remove((E) element);
}
public boolean remove(E element) {
String key = element.id;
int index = getProject().getNodeIndex(element);
boolean result = false;
if (dataMap.containsKey(key)) {
keyList.remove(key);
dataMap.remove(key);
result = true;
}
getProject().fireElementRemoved(element, index);
return result;
}
public int size() {
return dataMap.size();
}
public int indexOf(String key) {
return keyList.indexOf(key);
}
public int indexOf(E element) {
String key = element.id;
return keyList.indexOf(key);
}
public ArrayList<E> toList() {
ArrayList<E> list = new ArrayList<>();
for (String key : keyList) {
list.add(dataMap.get(key));
}
return list;
}
//endregion
//region copied implementation of ProjectTreeNode
@Override
public TreeNode getChildAt(int childIndex) {
return get(childIndex);
@@ -44,7 +127,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
@Override
public int getIndex(TreeNode node) {
return indexOf(node);
return indexOf((E) node);
}
@Override
@@ -59,7 +142,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
@Override
public Enumeration<E> children() {
return Collections.enumeration(this);
return Collections.enumeration(toList());
}
@Override
@@ -87,7 +170,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
@Override
public void notifyCreated() {
childrenAdded(new ArrayList<ProjectTreeNode>());
for (E node : this) {
for (E node : dataMap.values()) {
node.notifyCreated();
}
}
@@ -133,10 +216,15 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
}
@Override
public Type getDataType() {
public GameSource.Type getDataType() {
return parent.getDataType();
}
@Override
public boolean isEmpty() {
return dataMap.isEmpty();
}
@SuppressWarnings("rawtypes")
public void save(File jsonFile) {
if (getDataType() != GameSource.Type.created && getDataType() != GameSource.Type.altered) {
@@ -144,7 +232,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
return;
}
List<Map> dataToSave = new ArrayList<Map>();
for (E element : this) {
for (E element : dataMap.values()) {
if (element.jsonFile.equals(jsonFile)) {
dataToSave.add(element.toJson());
}
@@ -161,7 +249,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
String toWrite = FileUtils.toJsonString(dataToSave);
if(FileUtils.writeStringToFile(toWrite, jsonFile, "JSON file '"+jsonFile.getAbsolutePath()+"'")){
for (E element : this) {
for (E element : dataMap.values()) {
element.state = GameDataElement.State.saved;
}
}
@@ -173,7 +261,8 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
GameDataCategory<? extends JSONElement> impactedCategory = null;
String impactedFileName = fileName;
Map<String, Integer> containedIds = new LinkedHashMap<String, Integer>();
for (JSONElement node : this) {
ArrayList<E> list = toList();
for (JSONElement node : list) {
if (node.getDataType() == GameSource.Type.created && getProject().baseContent.gameData.getGameDataElement(node.getClass(), node.id) != null) {
if (getProject().alteredContent.gameData.getGameDataElement(node.getClass(), node.id) != null) {
events.add(new SaveEvent(SaveEvent.Type.moveToAltered, node, true, "Element ID matches one already present in the altered game content. Change this ID before saving."));
@@ -202,7 +291,7 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
for (String key : containedIds.keySet()) {
if (containedIds.get(key) > 1) {
E node = null;
for (E n : this) {
for (E n : list) {
if (key.equals(n.id)) {
node = n;
break;
@@ -218,19 +307,15 @@ public class GameDataCategory<E extends JSONElement> extends ArrayList<E> implem
return events;
}
public boolean remove(E o) {
int index = getProject().getNodeIndex(o);
boolean result = super.remove(o);
getProject().fireElementRemoved(o, index);
return result;
}
@Override
public boolean needsSaving() {
for (E node : this) {
for (E node : dataMap.values()) {
if (node.needsSaving()) return true;
}
return false;
}
//endregion
}

View File

@@ -67,7 +67,7 @@ public class GameDataSet implements ProjectTreeNode, Serializable {
items = new GameDataCategory<Item>(this, Item.getStaticDesc());
itemCategories = new GameDataCategory<ItemCategory>(this, ItemCategory.getStaticDesc());
npcs = new GameDataCategory<NPC>(this, NPC.getStaticDesc());
quests = new GameDataCategory<Quest>(this, Quest.getStaticDesc());
quests = new GameDataCategory<>(this, Quest.getStaticDesc());
v.add(actorConditions);
v.add(dialogues);
@@ -256,82 +256,42 @@ public class GameDataSet implements ProjectTreeNode, Serializable {
public ActorCondition getActorCondition(String id) {
if (actorConditions == null) return null;
for (ActorCondition gde : actorConditions) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return actorConditions.get(id);
}
public Dialogue getDialogue(String id) {
if (dialogues == null) return null;
for (Dialogue gde : dialogues) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return dialogues.get(id);
}
public Droplist getDroplist(String id) {
if (droplists == null) return null;
for (Droplist gde : droplists) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return droplists.get(id);
}
public Item getItem(String id) {
if (items == null) return null;
for (Item gde : items) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return items.get(id);
}
public ItemCategory getItemCategory(String id) {
if (itemCategories == null) return null;
for (ItemCategory gde : itemCategories) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return itemCategories.get(id);
}
public NPC getNPC(String id) {
if (npcs == null) return null;
for (NPC gde : npcs) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return npcs.get(id);
}
public NPC getNPCIgnoreCase(String id) {
if (npcs == null) return null;
for (NPC gde : npcs) {
if (id.equalsIgnoreCase(gde.id)) {
return gde;
}
}
return null;
return npcs.getIgnoreCase(id);
}
public Quest getQuest(String id) {
if (quests == null) return null;
for (Quest gde : quests) {
if (id.equals(gde.id)) {
return gde;
}
}
return null;
return quests.get(id);
}
@Override

View File

@@ -186,5 +186,4 @@ public class Quest extends JSONElement {
}
return null;
}
}

View File

@@ -19,12 +19,12 @@ public class PotGenerator {
GameSource gsrc = proj.baseContent;
for (ActorCondition ac : gsrc.gameData.actorConditions) {
for (ActorCondition ac : gsrc.gameData.actorConditions.toList()) {
pushString(stringsResources, resourcesStrings, ac.display_name, getPotContextComment(ac));
pushString(stringsResources, resourcesStrings, ac.description, getPotContextComment(ac) + ":description");
}
for (Dialogue d : gsrc.gameData.dialogues) {
for (Dialogue d : gsrc.gameData.dialogues.toList()) {
pushString(stringsResources, resourcesStrings, d.message, getPotContextComment(d));
if (d.replies == null) continue;
for (Dialogue.Reply r : d.replies) {
@@ -34,20 +34,20 @@ public class PotGenerator {
}
}
for (ItemCategory ic : gsrc.gameData.itemCategories) {
for (ItemCategory ic : gsrc.gameData.itemCategories.toList()) {
pushString(stringsResources, resourcesStrings, ic.name, getPotContextComment(ic));
}
for (Item i : gsrc.gameData.items) {
for (Item i : gsrc.gameData.items.toList()) {
pushString(stringsResources, resourcesStrings, i.name, getPotContextComment(i));
pushString(stringsResources, resourcesStrings, i.description, getPotContextComment(i) + ":description");
}
for (NPC npc : gsrc.gameData.npcs) {
for (NPC npc : gsrc.gameData.npcs.toList()) {
pushString(stringsResources, resourcesStrings, npc.name, getPotContextComment(npc));
}
for (Quest q : gsrc.gameData.quests) {
for (Quest q : gsrc.gameData.quests.toList()) {
if (q.visible_in_log != null && q.visible_in_log != 0) {
pushString(stringsResources, resourcesStrings, q.name, getPotContextComment(q));
for (QuestStage qs : q.stages) {

View File

@@ -3,10 +3,7 @@ package com.gpl.rpg.atcontentstudio.model.tools.resoptimizer;
import com.gpl.rpg.atcontentstudio.io.JsonPrettyWriter;
import com.gpl.rpg.atcontentstudio.model.GameDataElement;
import com.gpl.rpg.atcontentstudio.model.Project;
import com.gpl.rpg.atcontentstudio.model.gamedata.ActorCondition;
import com.gpl.rpg.atcontentstudio.model.gamedata.GameDataSet;
import com.gpl.rpg.atcontentstudio.model.gamedata.Item;
import com.gpl.rpg.atcontentstudio.model.gamedata.NPC;
import com.gpl.rpg.atcontentstudio.model.gamedata.*;
import com.gpl.rpg.atcontentstudio.model.maps.TMXMap;
import com.gpl.rpg.atcontentstudio.model.maps.TMXMapSet;
import com.gpl.rpg.atcontentstudio.model.sprites.SpriteSheetSet;
@@ -81,12 +78,13 @@ public class ResourcesCompactor {
File folder = new File(baseFolder.getAbsolutePath() + File.separator + GameDataSet.DEFAULT_REL_PATH_IN_SOURCE);
if (!folder.exists()) folder.mkdirs();
for (ActorCondition ac : proj.baseContent.gameData.actorConditions) {
ArrayList<ActorCondition> actorConditions = proj.baseContent.gameData.actorConditions.toList();
for (ActorCondition ac : actorConditions) {
if (filesCovered.contains(ac.jsonFile)) continue;
File currentFile = ac.jsonFile;
filesCovered.add(currentFile);
List<Map> dataToSave = new ArrayList<Map>();
for (ActorCondition acond : proj.baseContent.gameData.actorConditions) {
for (ActorCondition acond : actorConditions) {
if (!acond.jsonFile.equals(currentFile)) continue;
Map json = acond.toJson();
json.put("iconID", convertObjectSprite(acond.icon_id).toStringID());
@@ -96,12 +94,13 @@ public class ResourcesCompactor {
writeJson(dataToSave, target);
}
for (Item it : proj.baseContent.gameData.items) {
ArrayList<Item> items = proj.baseContent.gameData.items.toList();
for (Item it : items) {
if (filesCovered.contains(it.jsonFile)) continue;
File currentFile = it.jsonFile;
filesCovered.add(currentFile);
List<Map> dataToSave = new ArrayList<Map>();
for (Item item : proj.baseContent.gameData.items) {
for (Item item : items) {
if (!item.jsonFile.equals(currentFile)) continue;
Map json = item.toJson();
json.put("iconID", convertObjectSprite(item.icon_id).toStringID());
@@ -112,12 +111,13 @@ public class ResourcesCompactor {
}
for (NPC np : proj.baseContent.gameData.npcs) {
ArrayList<NPC> npcs = proj.baseContent.gameData.npcs.toList();
for (NPC np : npcs) {
if (filesCovered.contains(np.jsonFile)) continue;
File currentFile = np.jsonFile;
filesCovered.add(currentFile);
List<Map> dataToSave = new ArrayList<Map>();
for (NPC npc : proj.baseContent.gameData.npcs) {
for (NPC npc : npcs) {
if (!npc.jsonFile.equals(currentFile)) continue;
Map json = npc.toJson();
if (proj.getImage(npc.icon_id).getWidth(null) == TILE_WIDTH_IN_PIXELS || proj.getImage(npc.icon_id).getHeight(null) == TILE_HEIGHT_IN_PIXELS) {

View File

@@ -163,7 +163,7 @@ public class SaveItemsWizard extends JDialog {
@Override
public void actionPerformed(ActionEvent e) {
Map<GameDataCategory<JSONElement>, Set<File>> jsonToSave = new IdentityHashMap<GameDataCategory<JSONElement>, Set<File>>();
Map<GameDataCategory<JSONElement>, Set<File>> jsonToSave = new IdentityHashMap<>();
for (SaveEvent event : movedToCreatedList) {
if (event.target instanceof JSONElement) {
if (!jsonToSave.containsKey(event.target.getParent())) {

View File

@@ -121,7 +121,7 @@ public class WorkspaceActions {
if (element.getParent() instanceof GameDataCategory<?>) {
@SuppressWarnings("unchecked")
GameDataCategory<JSONElement> category = (GameDataCategory<JSONElement>) element.getParent();
category.remove(element);
category.remove((JSONElement) element);
if (impactedCategories.get(category) == null) {
impactedCategories.put(category, new HashSet<File>());
}
@@ -189,7 +189,7 @@ public class WorkspaceActions {
node.childrenRemoved(new ArrayList<ProjectTreeNode>());
if (node instanceof JSONElement) {
if (node.getParent() instanceof GameDataCategory<?>) {
((GameDataCategory<?>) node.getParent()).remove(node);
((GameDataCategory<?>) node.getParent()).removeGeneric((JSONElement) node);
List<SaveEvent> events = node.attemptSave();
if (events == null || events.isEmpty()) {
node.save();

View File

@@ -164,7 +164,7 @@ public abstract class JSONElementEditor extends Editor {
ATContentStudio.frame.closeEditor(node);
node.childrenRemoved(new ArrayList<ProjectTreeNode>());
if (node.getParent() instanceof GameDataCategory<?>) {
((GameDataCategory<?>) node.getParent()).remove(node);
((GameDataCategory<?>) node.getParent()).removeGeneric(node);
node.save();
GameDataElement newOne = proj.getGameDataElement(node.getClass(), node.id);
if (node instanceof Quest) {