diff --git a/x2-data-explorer/docs/user-guide.adoc b/x2-data-explorer/docs/user-guide.adoc
index 47f1ccb..d7b2180 100644
--- a/x2-data-explorer/docs/user-guide.adoc
+++ b/x2-data-explorer/docs/user-guide.adoc
@@ -75,9 +75,8 @@ For expressions on frames, `f` is the candidate frame. It is a `com.github.rcd47
For a given frame or object to match the expression, the expression must return a result that is https://groovy-lang.org/semantics.html#the-groovy-truth[truthy as defined by Groovy]. This means that the result does not have to be a boolean. E.g. to find any object that contains a `m_template` field, you could simply search for `o.m_template`.
-IMPORTANT: When writing expressions to filter on fields within a game state object, remember that each field in the tree is a `com.github.rcd47.x2data.explorer.file.GameStateObjectField`, and the value must be accessed explicitly. E.g. to find a soldier whose first name is Ben, your filter would need to be `o.strFirstName.value == 'Ben'`, not just `o.strFirstName == 'Ben'`.
-IMPORTANT: When writing expressions to match fields that are Unreal names, remember that the name is a `com.github.rcd47.x2data.lib.unreal.mappings.UnrealName`, and the value must be accessed through the `original` or `normalized` fields. E.g. to find the eastern US region, your filter would need to be `o.m_TemplateName.value.original == 'WorldRegion_EastNA'`, not just `o.m_TemplateName.value == 'WorldRegion_EastNA'`.
+IMPORTANT: When writing expressions to match fields that are Unreal names, remember that the name is a `com.github.rcd47.x2data.lib.unreal.mappings.UnrealName`, and the value must be accessed through the `original` or `normalized` fields. E.g. to find the eastern US region, your filter would need to be `o.m_TemplateName.original == 'WorldRegion_EastNA'`, not just `o.m_TemplateName == 'WorldRegion_EastNA'`.
==== Context and Object Summaries
diff --git a/x2-data-explorer/pom.xml b/x2-data-explorer/pom.xml
index f73e174..8da1b78 100644
--- a/x2-data-explorer/pom.xml
+++ b/x2-data-explorer/pom.xml
@@ -35,6 +35,14 @@
commons-codec
commons-codec
+
+ it.unimi.dsi
+ fastutil
+
+
+ org.apache.commons
+ commons-lang3
+
org.apache.groovy
groovy
@@ -138,6 +146,11 @@
java.desktop
java.logging
+
+
+ -XX:+UnlockExperimentalVMOptions
+ -XX:+UseCompactObjectHeaders
+
${project.build.finalName}.${project.packaging}
com.github.rcd47.x2data.explorer.jfx.MainLauncher
X2 Data Explorer
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateContext.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateContext.java
index 2fecd23..8502f5f 100644
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateContext.java
+++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateContext.java
@@ -1,25 +1,33 @@
package com.github.rcd47.x2data.explorer.file;
-import java.util.HashMap;
+import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner;
+import com.github.rcd47.x2data.explorer.file.data.X2VersionedDatumTreeItem;
+import com.github.rcd47.x2data.explorer.file.data.X2VersionedMap;
import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
import com.github.rcd47.x2data.lib.unreal.mappings.base.EInterruptionStatus;
import com.google.common.base.Throwables;
import groovy.lang.Script;
+import javafx.scene.control.TreeItem;
public class GameStateContext implements ISizedObject {
private static final UnrealName INTERRUPTION_HISTORY_INDEX = new UnrealName("InterruptionHistoryIndex");
private static final UnrealName HISTORY_INDEX_INTERRUPTED_BY_SELF = new UnrealName("HistoryIndexInterruptedBySelf");
private static final UnrealName INTERRUPTION_STATUS = new UnrealName("InterruptionStatus");
+ private static final Map STATUS_MAP =
+ Arrays.stream(EInterruptionStatus.values()).collect(Collectors.toMap(s -> new UnrealName(s.name()), Function.identity()));
private final int sizeInFile;
private final UnrealName type;
- private final Map fields;
+ private final X2VersionedMap properties;
private final HistoryFrame frame;
private final String summary;
private final EInterruptionStatus interruptionStatus;
@@ -27,47 +35,54 @@ public class GameStateContext implements ISizedObject {
private final HistoryFrame interruptedByThis;
private HistoryFrame resumedBy;
- public GameStateContext(int sizeInFile, GenericObject object, HistoryFrame frame, Map frames, Script summarizer,
+ public GameStateContext(int sizeInFile, UnrealName type, X2VersionedMap properties, HistoryFrame frame,
+ int frameOffset, HistoryFrame[] frames, PrimitiveInterner interner, Script summarizer,
List problemsDetected) {
this.sizeInFile = sizeInFile;
+ this.type = type;
+ this.properties = properties;
this.frame = frame;
- type = object.type;
-
- fields = new HashMap<>();
- object.properties.forEach((k, v) -> fields.put(k, new NonVersionedField(v)));
+ var fields = properties.getValueAt(0);
interruptionStatus = Optional
.ofNullable(fields.get(INTERRUPTION_STATUS))
- .map(s -> EInterruptionStatus.valueOf(((UnrealName) s.getValue()).getOriginal()))
+ .map(STATUS_MAP::get)
.orElse(EInterruptionStatus.eInterruptionStatus_None);
String summaryTemp;
try {
- summarizer.getBinding().setProperty("ctx", this);
- summaryTemp = (String) summarizer.run();
+ synchronized (summarizer) {
+ summarizer.getBinding().setProperty("ctx", this);
+ summaryTemp = (String) summarizer.run();
+ }
} catch (Exception e) {
- summaryTemp = "";
+ summaryTemp = null;
problemsDetected.add(new HistoryFileProblem(
frame, this, null, "Summary script failed. Stack trace:\n" + Throwables.getStackTraceAsString(e)));
}
- summary = summaryTemp;
+ summary = summaryTemp == null ? null : interner.internString(summaryTemp);
var resumedFromIndex = fields.get(INTERRUPTION_HISTORY_INDEX);
- if (resumedFromIndex == null) {
- resumedFrom = null;
- } else {
- resumedFrom = frames.get(resumedFromIndex.getValue());
- resumedFrom.getContext().resumedBy = frame;
- }
+ resumedFrom = resumedFromIndex == null ? null : frames[(int) resumedFromIndex - frameOffset];
var interruptingIndex = fields.get(HISTORY_INDEX_INTERRUPTED_BY_SELF);
- interruptedByThis = interruptingIndex == null ? null : frames.get(interruptingIndex.getValue());
+ interruptedByThis = interruptingIndex == null ? null : frames[(int) interruptingIndex - frameOffset];
+ }
+
+ void finishBuilding() {
+ if (resumedFrom != null) {
+ resumedFrom.getContext().resumedBy = frame;
+ }
}
// Groovy support
public Object propertyMissing(String name) {
- return fields.get(new UnrealName(name));
+ return properties.getValueAt(0).get(name);
+ }
+
+ public TreeItem getTree(PrimitiveInterner interner) {
+ return properties.getTreeNodeAt(interner, null, 0, false);
}
@Override
@@ -79,10 +94,6 @@ public UnrealName getType() {
return type;
}
- public Map getFields() {
- return fields;
- }
-
public HistoryFrame getFrame() {
return frame;
}
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObject.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObject.java
index 6e38106..90209a8 100644
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObject.java
+++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObject.java
@@ -1,17 +1,13 @@
package com.github.rcd47.x2data.explorer.file;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
+import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner;
+import com.github.rcd47.x2data.explorer.file.data.X2VersionedMap;
import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
import com.google.common.base.Throwables;
import groovy.lang.Script;
-import javafx.scene.control.TreeItem;
public class GameStateObject implements ISizedObject {
@@ -23,54 +19,45 @@ public class GameStateObject implements ISizedObject {
private final boolean removed; // note that it is possible for an object to be added and removed in the same state
private final UnrealName type;
private final String summary;
- private final Map fields;
+ private final X2VersionedMap fields;
private final HistoryFrame frame;
private final GameStateObject previousVersion;
private GameStateObject nextVersion;
- public GameStateObject(int sizeInFile, Map stateObjects, GenericObject currentVersion,
- HistoryFrame frame, Script summarizer, List problemsDetected) {
+ public GameStateObject(int sizeInFile, GameStateObject previousVersion, X2VersionedMap fields,
+ UnrealName type, HistoryFrame frame, PrimitiveInterner interner, Script summarizer,
+ List problemsDetected) {
this.sizeInFile = sizeInFile;
+ this.type = type;
+ this.fields = fields;
this.frame = frame;
+ this.previousVersion = previousVersion;
- objectId = (int) currentVersion.properties.get(OBJECT_ID);
- removed = Boolean.TRUE.equals(currentVersion.properties.get(REMOVED));
- type = currentVersion.type;
- previousVersion = stateObjects.put(objectId, this);
- if (previousVersion == null) {
- fields = diffFields(this, null, currentVersion.properties);
- } else {
- fields = diffFields(this, previousVersion.fields, currentVersion.properties);
+ if (previousVersion != null) {
previousVersion.nextVersion = this;
}
+ var currentVersion = fields.getValueAt(frame.getNumber());
+ objectId = (int) currentVersion.get(OBJECT_ID);
+ removed = Boolean.TRUE.equals(currentVersion.get(REMOVED));
+
String summaryTemp;
try {
- summarizer.getBinding().setProperty("gso", this);
- summaryTemp = (String) summarizer.run();
+ synchronized (summarizer) {
+ summarizer.getBinding().setProperty("gso", this);
+ summaryTemp = (String) summarizer.run();
+ }
} catch (Exception e) {
- summaryTemp = "";
+ summaryTemp = null;
problemsDetected.add(new HistoryFileProblem(
frame, null, this, "Summary script failed. Stack trace:\n" + Throwables.getStackTraceAsString(e)));
}
- summary = summaryTemp;
+ summary = summaryTemp == null ? null : interner.internString(summaryTemp);
}
// Groovy support
public Object propertyMissing(String name) {
- return fields == null ? null : fields.get(new UnrealName(name));
- }
-
- public TreeItem getFieldsAsTreeNode(boolean onlyModified) {
- var root = new TreeItem();
- var rootChildren = root.getChildren();
- fields.values()
- .stream()
- .sorted((a, b) -> a.getName().compareTo(b.getName()))
- .map(f -> f.asTreeNode(this, onlyModified))
- .filter(f -> f != null)
- .forEachOrdered(rootChildren::add);
- return root;
+ return fields.getValueAt(frame.getNumber()).get(name);
}
@Override
@@ -94,7 +81,7 @@ public String getSummary() {
return summary;
}
- public Map getFields() {
+ public X2VersionedMap getFields() {
return fields;
}
@@ -109,70 +96,5 @@ public GameStateObject getPreviousVersion() {
public GameStateObject getNextVersion() {
return nextVersion;
}
-
- private static Map diffFields(
- GameStateObject newStateObject, Map oldFields, Map newGenericFields) {
- if (oldFields == null) {
- oldFields = Map.of();
- }
- if (newGenericFields == null) {
- newGenericFields = Map.of();
- }
- Set fieldNames = new HashSet<>(newGenericFields.keySet());
- for (var oldField : oldFields.values()) {
- if (!oldField.isTombstone()) {
- fieldNames.add(oldField.getName());
- }
- }
-
- boolean changed = false;
- Map newFields = new HashMap<>();
- for (var fieldName : fieldNames) {
- var oldField = oldFields.get(fieldName);
- var newField = diffField(newStateObject, fieldName, oldField, newGenericFields.get(fieldName));
- newFields.put(fieldName, newField);
- if (oldField != newField) { // deliberate identity comparison
- changed = true;
- }
- }
-
- return changed ? newFields : oldFields;
- }
-
- @SuppressWarnings("unchecked")
- private static GameStateObjectField diffField(
- GameStateObject newStateObject, UnrealName fieldName, GameStateObjectField oldField, Object newValue) {
- if (newValue != null) {
- Map newValueMap;
- if (newValue instanceof List> list) {
- newValueMap = new HashMap<>();
- for (int i = 0; i < list.size(); i++) {
- var child = list.get(i);
- if (child != null) { // can be null for static arrays, if child is the default value and thus not serialized
- newValueMap.put(new UnrealName(Integer.toString(i)), child);
- }
- }
- } else if (newValue instanceof GenericObject obj) {
- newValueMap = obj.properties;
- } else if (newValue instanceof Map) {
- newValueMap = (Map) newValue;
- } else {
- // must be a scalar
- return oldField != null && Objects.equals(oldField.getValue(), newValue) ?
- oldField : new GameStateObjectField(fieldName, newValue, null, newStateObject, oldField);
- }
-
- var oldFieldChildren = oldField == null ? null : oldField.getChildren();
- var newFieldChildren = diffFields(newStateObject, oldFieldChildren, newValueMap);
- return newFieldChildren.equals(oldFieldChildren) ?
- oldField : new GameStateObjectField(fieldName, null, newFieldChildren, newStateObject, oldField);
- }
-
- var oldFieldChildren = oldField.getChildren();
- return new GameStateObjectField(
- fieldName, null,
- oldFieldChildren == null ? null : diffFields(newStateObject, oldFieldChildren, null),
- newStateObject, oldField);
- }
}
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectField.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectField.java
deleted file mode 100644
index ecd9a17..0000000
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectField.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package com.github.rcd47.x2data.explorer.file;
-
-import java.util.Map;
-
-import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
-
-import javafx.scene.control.TreeItem;
-
-public class GameStateObjectField {
-
- private final UnrealName name;
- private final Object value;
- private final Map children;
- private final GameStateObject lastChangedAt;
- private final GameStateObjectField previousValue;
- private GameStateObjectField nextValue;
-
- public GameStateObjectField(UnrealName name, Object value, Map children,
- GameStateObject lastChangedAt, GameStateObjectField previousValue) {
- if (previousValue != null) {
- previousValue.nextValue = this;
- }
-
- this.name = name;
- this.value = value;
- this.children = children;
- this.lastChangedAt = lastChangedAt;
- this.previousValue = previousValue;
- }
-
- // Groovy support
- public Object propertyMissing(String name) {
- return children == null ? null : children.get(new UnrealName(name));
- }
-
- public boolean isTombstone() {
- return value == null && (children == null || children.values().stream().allMatch(c -> c.isTombstone()));
- }
-
- public TreeItem asTreeNode(GameStateObject stateObject, boolean onlyModified) {
- if (onlyModified && lastChangedAt != stateObject) { // deliberate identity comparison
- return null;
- }
-
- if (children == null) {
- FieldChangeType changeType;
- if (lastChangedAt != stateObject) { // deliberate identity comparison
- changeType = FieldChangeType.NONE;
- } else if (value == null) {
- changeType = FieldChangeType.REMOVED;
- } else if (previousValue == null || previousValue.isTombstone()) {
- changeType = FieldChangeType.ADDED;
- } else {
- changeType = FieldChangeType.CHANGED;
- }
- return new TreeItem(new GameStateObjectFieldTreeNode(name, value, changeType, previousValue, nextValue));
- }
-
- var changeType = FieldChangeType.NONE;
- var node = new TreeItem();
- var nodeChildren = node.getChildren();
- var childNodes = children
- .values()
- .stream()
- .sorted((a, b) -> a.name.compareTo(b.name))
- .map(f -> f.asTreeNode(stateObject, onlyModified))
- .iterator();
- while (childNodes.hasNext()) {
- var childNode = childNodes.next();
- if (childNode == null) { // we want only modified and child was not modified
- if (changeType != FieldChangeType.NONE) {
- changeType = FieldChangeType.CHANGED;
- }
- } else {
- nodeChildren.add(childNode);
- var childChangeType = childNode.getValue().getChangeType();
- if (changeType == FieldChangeType.NONE) {
- changeType = childChangeType;
- } else if (changeType != childChangeType) {
- changeType = FieldChangeType.CHANGED;
- }
- }
- }
- node.setValue(new GameStateObjectFieldTreeNode(name, value, changeType, previousValue, nextValue));
- return node;
- }
-
- public UnrealName getName() {
- return name;
- }
-
- public Object getValue() {
- return value;
- }
-
- public Map getChildren() {
- return children;
- }
-
- public GameStateObject getLastChangedAt() {
- return lastChangedAt;
- }
-
- public GameStateObjectField getPreviousValue() {
- return previousValue;
- }
-
- public GameStateObjectField getNextValue() {
- return nextValue;
- }
-
-}
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectFieldTreeNode.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectFieldTreeNode.java
deleted file mode 100644
index 3e93dd9..0000000
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GameStateObjectFieldTreeNode.java
+++ /dev/null
@@ -1,47 +0,0 @@
-package com.github.rcd47.x2data.explorer.file;
-
-import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
-
-public class GameStateObjectFieldTreeNode implements Comparable {
-
- private final UnrealName name;
- private final Object value;
- private final FieldChangeType changeType;
- private final GameStateObjectField previousValue;
- private final GameStateObjectField nextValue;
-
- public GameStateObjectFieldTreeNode(UnrealName name, Object value, FieldChangeType changeType,
- GameStateObjectField previousValue, GameStateObjectField nextValue) {
- this.name = name;
- this.value = value;
- this.changeType = changeType;
- this.previousValue = previousValue;
- this.nextValue = nextValue;
- }
-
- @Override
- public int compareTo(GameStateObjectFieldTreeNode o) {
- return name.compareTo(o.name);
- }
-
- public UnrealName getName() {
- return name;
- }
-
- public Object getValue() {
- return value;
- }
-
- public FieldChangeType getChangeType() {
- return changeType;
- }
-
- public GameStateObjectField getPreviousValue() {
- return previousValue;
- }
-
- public GameStateObjectField getNextValue() {
- return nextValue;
- }
-
-}
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObject.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObject.java
deleted file mode 100644
index f986316..0000000
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObject.java
+++ /dev/null
@@ -1,74 +0,0 @@
-package com.github.rcd47.x2data.explorer.file;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
-
-public class GenericObject {
-
- UnrealName type;
- Map properties = new HashMap<>();
-
- public GenericObject(UnrealName type) {
- this.type = type;
- }
-
- public GenericObject deepClone() {
- GenericObject clone = new GenericObject(type);
- properties.forEach((k, v) -> clone.properties.put(k, deepClone(v)));
- return clone;
- }
-
- private static Object deepClone(Object obj) {
- if (obj instanceof GenericObject o) {
- return o.deepClone();
- }
- if (obj instanceof List> l) {
- return l.stream().map(GenericObject::deepClone).collect(Collectors.toList());
- }
- // no need to clone Maps since those are only native and therefore are always replaced entirely
- return obj;
- }
-
- @Override
- public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + ((properties == null) ? 0 : properties.hashCode());
- result = prime * result + ((type == null) ? 0 : type.hashCode());
- return result;
- }
-
- @Override
- public boolean equals(Object obj) {
- if (this == obj) {
- return true;
- }
- if (obj == null) {
- return false;
- }
- if (getClass() != obj.getClass()) {
- return false;
- }
- GenericObject other = (GenericObject) obj;
- if (properties == null) {
- if (other.properties != null) {
- return false;
- }
- } else if (!properties.equals(other.properties)) {
- return false;
- }
- if (type == null) {
- if (other.type != null) {
- return false;
- }
- } else if (!type.equals(other.type)) {
- return false;
- }
- return true;
- }
-
-}
diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObjectVisitor.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObjectVisitor.java
deleted file mode 100644
index 168625e..0000000
--- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/GenericObjectVisitor.java
+++ /dev/null
@@ -1,239 +0,0 @@
-package com.github.rcd47.x2data.explorer.file;
-
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Deque;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import com.github.rcd47.x2data.lib.unreal.IUnrealObjectVisitor;
-import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName;
-
-public class GenericObjectVisitor implements IUnrealObjectVisitor {
-
- private static final UnrealName NATIVE_VARS = new UnrealName("native vars");
-
- private GenericObject rootObject;
- private Deque
+
+ io.smallrye
+ jandex
+ ${version.io.smallrye.jandex}
+
+
+ it.unimi.dsi
+ fastutil
+ 8.5.16
+
org.anarres.lzo
lzo-core
1.0.6
+
+ org.apache.commons
+ commons-lang3
+ 3.18.0
+
org.apache.groovy
groovy
@@ -84,11 +99,6 @@
javafx-controls
24.0.1
-
- io.smallrye
- jandex
- ${version.io.smallrye.jandex}
-