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 objectStack = new ArrayDeque<>(6); - private Deque stateStack = new ArrayDeque<>(6); - private UnrealName nextPropertyOrKey; - private int nextStaticArrayIndex; - - private static enum VisitorState { - PROPERTY_NAME, PROPERTY_VALUE, ARRAY_ELEMENT, MAP_KEY, MAP_VALUE - } - - public GenericObjectVisitor(GenericObject rootObject) { - this.rootObject = rootObject; - } - - public GenericObject getRootObject() { - return rootObject; - } - - @Override - public void visitStructStart(UnrealName type) { - if (objectStack.isEmpty()) { - rootObject = rootObject == null ? new GenericObject(type) : rootObject.deepClone(); - objectStack.push(rootObject); - stateStack.push(VisitorState.PROPERTY_NAME); - } else { - GenericObject struct = null; - if (stateStack.peek() == VisitorState.PROPERTY_VALUE) { - // struct is a property or static array element - // these are delta'd, so we must look for an existing value - var existingValue = ((GenericObject) objectStack.peek()).properties.get(nextPropertyOrKey); - if (existingValue != null) { - if (existingValue instanceof GenericObject obj) { // property - struct = obj; - } else { // must be List (static array element) - @SuppressWarnings("unchecked") - List list = (List) existingValue; - if (list.size() > nextStaticArrayIndex) { - struct = list.get(nextStaticArrayIndex); - } - } - } - } - if (struct == null) { - struct = new GenericObject(type); - } - pushAndVisitValue(struct, VisitorState.PROPERTY_NAME); - } - } - - @Override - public void visitStructEnd() { - popAndCheckState(VisitorState.PROPERTY_NAME); - } - - @Override - public void visitDynamicArrayStart(int size) { - pushAndVisitValue(new ArrayList<>(size), VisitorState.ARRAY_ELEMENT); - } - - @Override - public void visitDynamicArrayEnd() { - popAndCheckState(VisitorState.ARRAY_ELEMENT); - } - - @Override - public void visitMapStart(int size) { - pushAndVisitValue(new HashMap<>(size), VisitorState.MAP_KEY); - } - - @Override - public void visitMapEnd() { - popAndCheckState(VisitorState.MAP_KEY); - } - - @Override - public void visitUnparseableData(ByteBuffer data) { - var copy = ByteBuffer.allocate(data.remaining()).order(ByteOrder.LITTLE_ENDIAN).put(data).flip(); - if (stateStack.peek() == VisitorState.PROPERTY_NAME) { - ((GenericObject) objectStack.peek()).properties.put(NATIVE_VARS, copy); - } else { - visitValue(copy); - } - } - - @Override - public void visitProperty(UnrealName propertyName, int staticArrayIndex) { - var state = stateStack.pop(); - if (state != VisitorState.PROPERTY_NAME) { - throw new IllegalStateException("Unexpected state: " + state); - } - nextPropertyOrKey = propertyName; - nextStaticArrayIndex = staticArrayIndex; - stateStack.push(VisitorState.PROPERTY_VALUE); - } - - @Override - public void visitBooleanValue(boolean value) { - visitValue(value); - } - - @Override - public void visitByteValue(byte value) { - visitValue(value); - } - - @Override - public void visitEnumValue(UnrealName enumType, UnrealName value) { - visitValue(value); - } - - @Override - public void visitFloatValue(float value) { - visitValue(value); - } - - @Override - public void visitDoubleValue(double value) { - visitValue(value); - } - - @Override - public void visitIntValue(int value) { - visitValue(value); - } - - @Override - public void visitNameValue(UnrealName value) { - visitValue(value); - } - - @Override - public void visitStringValue(String value) { - visitValue(value); - } - - @Override - public void visitBasicDelegateValue(UnrealName delegateName, String declaringClass) { - visitValue(delegateName); - } - - @Override - public void visitBasicInterfaceValue(UnrealName objectName) { - visitValue(objectName); - } - - @Override - public void visitBasicObjectValue(UnrealName objectName) { - visitValue(objectName); - } - - @Override - public void visitHistoryDelegateValue(int objectIndex, UnrealName delegateName, String declaringClass) { - visitValue(objectIndex); - } - - @Override - public void visitHistoryInterfaceValue(int objectIndex) { - visitValue(objectIndex); - } - - @Override - public void visitHistoryObjectValue(int objectIndex) { - visitValue(objectIndex); - } - - private void popAndCheckState(VisitorState expectedState) { - var actualState = stateStack.pop(); - if (actualState != expectedState) { - throw new IllegalStateException("Expected state " + expectedState + " but found " + actualState); - } - objectStack.pop(); - } - - private void pushAndVisitValue(Object value, VisitorState state) { - visitValue(value); - objectStack.push(value); - stateStack.push(state); - } - - @SuppressWarnings("unchecked") - private void visitValue(Object value) { - var state = stateStack.pop(); - if (state == VisitorState.PROPERTY_VALUE) { - var properties = ((GenericObject) objectStack.peek()).properties; - var existingValue = properties.get(nextPropertyOrKey); - - if (nextStaticArrayIndex > 0 && !(existingValue instanceof List)) { - List list = new ArrayList<>(); - if (existingValue != null) { - list.add(existingValue); - } - existingValue = list; - properties.put(nextPropertyOrKey, list); - } - - if (existingValue instanceof List && !(value instanceof List)) { - List list = (List) existingValue; - while (list.size() <= nextStaticArrayIndex) { - list.add(null); - } - list.set(nextStaticArrayIndex, value); - } else { - properties.put(nextPropertyOrKey, value); - } - - stateStack.push(VisitorState.PROPERTY_NAME); - } else if (state == VisitorState.ARRAY_ELEMENT) { - ((List) objectStack.peek()).add(value); - stateStack.push(VisitorState.ARRAY_ELEMENT); - } else if (state == VisitorState.MAP_KEY) { - nextPropertyOrKey = UnrealName.from(value); - stateStack.push(VisitorState.MAP_VALUE); - } else if (state == VisitorState.MAP_VALUE) { - ((Map) objectStack.peek()).put(nextPropertyOrKey, value); - stateStack.push(VisitorState.MAP_KEY); - } else { - throw new IllegalStateException("Unexpected state " + state); - } - } - -} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFile.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFile.java index 5bce1b4..e9cf340 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFile.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFile.java @@ -2,30 +2,39 @@ import java.util.List; +import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner; import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; -import com.github.rcd47.x2data.lib.unreal.mappings.base.XComGameStateHistory; public class HistoryFile { - private final XComGameStateHistory history; + private final int randomSeed; + private final int numArchivedFrames; private final List frames; private final List singletons; private final List problems; private final List contextTypes; private final List objectTypes; + private final PrimitiveInterner interner; - public HistoryFile(XComGameStateHistory history, List frames, List singletons, - List problems, List contextTypes, List objectTypes) { - this.history = history; + public HistoryFile(int randomSeed, int numArchivedFrames, List frames, + List singletons, List problems, List contextTypes, + List objectTypes, PrimitiveInterner interner) { + this.randomSeed = randomSeed; + this.numArchivedFrames = numArchivedFrames; this.frames = frames; this.singletons = singletons; this.problems = problems; this.contextTypes = contextTypes; this.objectTypes = objectTypes; + this.interner = interner; } - public XComGameStateHistory getHistory() { - return history; + public int getRandomSeed() { + return randomSeed; + } + + public int getNumArchivedFrames() { + return numArchivedFrames; } public List getFrames() { @@ -47,5 +56,9 @@ public List getContextTypes() { public List getObjectTypes() { return objectTypes; } + + public PrimitiveInterner getInterner() { + return interner; + } } diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFileReader.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFileReader.java index 87ac13c..a4b1f3f 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFileReader.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFileReader.java @@ -6,17 +6,28 @@ import java.nio.file.Files; import java.nio.file.StandardOpenOption; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; import java.util.Comparator; -import java.util.HashMap; import java.util.HashSet; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.function.DoubleConsumer; +import org.apache.commons.lang3.function.FailableRunnable; + +import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner; +import com.github.rcd47.x2data.explorer.file.data.VersionedObjectVisitor; +import com.github.rcd47.x2data.explorer.file.data.X2VersionedMap; import com.github.rcd47.x2data.explorer.prefs.script.ScriptPreferences; +import com.github.rcd47.x2data.lib.history.X2HistoryIndex; import com.github.rcd47.x2data.lib.history.X2HistoryIndexEntry; import com.github.rcd47.x2data.lib.history.X2HistoryReader; import com.github.rcd47.x2data.lib.unreal.mapper.ref.NullXComObjectReferenceResolver; @@ -24,15 +35,21 @@ import com.github.rcd47.x2data.lib.unreal.mappings.base.XComGameState; import com.github.rcd47.x2data.lib.unreal.mappings.base.XComGameStateHistory; +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.ints.IntArrayList; + public class HistoryFileReader { + private static final double PROGRESS_BAR_PHASES = 4; private static final UnrealName OBJECT_ID = new UnrealName("ObjectID"); - private static final UnrealName PREV_FRAME_HIST_INDEX = new UnrealName("PreviousHistoryFrameIndex"); private static final String PROBLEM_MODIFIED_OLD_STATE = """ - Found object of type %s with no ObjectID and PreviousHistoryFrameIndex pointing to the future (frame %d). + Found object of type %s with no ObjectID. This is caused by code modifying a state that has already been submitted instead of modifying a new state."""; + private static final String PROBLEM_UNKNOWN_PREV_INDEX_CORRUPTION = """ + Found object of type %s at position %d with length %d that has previous version index %d, but that index was not found. + This problem has not been observed before. Please report it on GitHub and attach the file that causes it."""; - public HistoryFile read(FileChannel in, DoubleConsumer progressPercentCallback, Consumer progressTextCallback) throws IOException { + public HistoryFile read(FileChannel in, DoubleConsumer progressPercentCallback, Consumer progressTextCallback) throws Exception { var decompressedFile = Files.createTempFile("x2hist", null); try (var decompressedIn = FileChannel.open(decompressedFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE)) { progressTextCallback.accept("Decompressing file"); @@ -53,103 +70,160 @@ public HistoryFile read(FileChannel in, DoubleConsumer progressPercentCallback, } } } + var numArchivedFrames = currentFrameNum - 1; - // first pass to parse the state and detect singletons + // first pass to detect singletons and set up work queues var numFrames = frameRefs.size(); - var rawFrames = new XComGameState[numFrames]; - var seenObjectIndexes = new HashSet(); + var parsedFrames = new HistoryFrame[numFrames]; + var contextEntries = new X2HistoryIndexEntry[numFrames]; + var objectsChangedInFrame = new ArrayList>(numFrames); + var objectStateChains = new Int2ReferenceOpenHashMap>(); var detectedSingletonTypes = new HashSet(); + var objectTypes = new HashSet(); + var contextTypes = new HashSet(); + var interner = new PrimitiveInterner(); + var visitor = new VersionedObjectVisitor(interner); + var problemsDetected = Collections.synchronizedList(new ArrayList()); for (int i = 0; i < numFrames; i++) { - progressTextCallback.accept("Parsing history frame " + currentFrameNum++); + progressTextCallback.accept("Inspecting history frame " + currentFrameNum++); XComGameState rawFrame = historyIndex.mapObject( historyIndex.getEntry(frameRefs.get(i).index()), null, NullXComObjectReferenceResolver.INSTANCE); - rawFrames[i] = rawFrame; + parsedFrames[i] = new HistoryFrame(rawFrame.HistoryIndex, rawFrame.TimeStamp); + contextEntries[i] = historyIndex.getEntry(rawFrame.StateChangeContext.index()); + contextTypes.add(contextEntries[i].getType()); + objectsChangedInFrame.add(ConcurrentHashMap.newKeySet()); for (var objRef : rawFrame.GameStates) { - if (!seenObjectIndexes.add(objRef.index())) { - // multiple frames pointing to same object index, so class must be a singleton - // note that in strategy saves, two versions of a singleton are written - // the first frame (archive frame) points to one version - // all other frames point to the other version - // this does not happen for tactical saves, where all frames point to a single version - detectedSingletonTypes.add(historyIndex.getEntry(objRef.index()).getType()); + var entryIndex = objRef.index(); + var fileEntry = historyIndex.getEntry(entryIndex); + var prevIndex = fileEntry.getPreviousVersionIndex(); + List chain; + if (prevIndex == -1) { + if (objectStateChains.containsKey(entryIndex)) { + // multiple frames pointing to same object index, so class must be a singleton + // note that in strategy saves, two versions of a singleton are written + // the first frame (archive frame) points to one version + // all other frames point to the other version + // this does not happen for tactical saves, where all frames point to a single version + detectedSingletonTypes.add(fileEntry.getType()); + continue; + } + chain = new ArrayList<>(); + objectTypes.add(fileEntry.getType()); + } else { + chain = objectStateChains.remove(prevIndex); + if (chain == null) { // broken objects have been written into the file + var parsedObject = new X2VersionedMap(0); + visitor.setRootObject(0, parsedObject); + historyIndex.parseObject(fileEntry, visitor); + var parsedObjectFields = parsedObject.getValueAt(0); + if (!detectModifyingOldState(parsedObjectFields, parsedFrames[i], fileEntry, problemsDetected)) { + // a problem we haven't seen before + problemsDetected.add(new HistoryFileProblem( + parsedFrames[i], + null, + null, + String.format( + PROBLEM_UNKNOWN_PREV_INDEX_CORRUPTION, + fileEntry.getType().getOriginal(), + fileEntry.getPosition(), + fileEntry.getLength(), + prevIndex))); + } + continue; + } } + chain.add(new GameStateObjectState(fileEntry, parsedFrames[i])); + objectStateChains.put(entryIndex, chain); } + progressPercentCallback.accept((((double) i) / numFrames) / PROGRESS_BAR_PHASES); } - // second pass to parse the state objects - Map frames = new LinkedHashMap<>(); - Map parsedObjects = new HashMap<>(); - Map stateObjects = new HashMap<>(); - Map singletonStates = new HashMap<>(); - List problemsDetected = new ArrayList<>(); - Set contextTypes = new HashSet<>(); - Set objectTypes = new HashSet<>(); - var contextSummarizer = ScriptPreferences.CONTEXT_SUMMARY.getExecutable(); - var objectSummarizer = ScriptPreferences.STATE_OBJECT_SUMMARY.getExecutable(); + // filter out singletons + progressTextCallback.accept("Filtering singleton states"); + var singletonStates = new HashSet(); + var stateObjectQueue = new ArrayBlockingQueue>(objectStateChains.size()); + for (var chain : objectStateChains.values()) { + var firstState = chain.getFirst(); + if (detectedSingletonTypes.contains(firstState.fileEntry.getType())) { + singletonStates.add(firstState); + } else { + stateObjectQueue.add(chain); + } + } + objectTypes.removeAll(detectedSingletonTypes); + + // parse state objects in parallel + progressTextCallback.accept("Preparing to parse objects"); + var stateObjectQueueSize = stateObjectQueue.size(); + var objectCounter = new AtomicInteger(stateObjectQueueSize); + readInParallel( + "GSO Parser ", + () -> readGameStateObjects( + numArchivedFrames, historyIndex, objectsChangedInFrame, interner, + problemsDetected, stateObjectQueue, objectCounter, progressTextCallback, + progressPercentCallback, stateObjectQueueSize)); + + // parse contexts in parallel + progressTextCallback.accept("Preparing to parse contexts"); + objectCounter.set(numFrames); + var contextArrayIndex = new AtomicInteger(); + readInParallel( + "GSC Parser", + () -> readGameStateContexts( + contextArrayIndex, historyIndex, contextEntries, parsedFrames, + numArchivedFrames, interner, problemsDetected, objectCounter, + progressTextCallback, progressPercentCallback, numFrames)); + + // finish building frames + var stateObjectsInFrameCold = new Int2ReferenceOpenHashMap(); + var stateObjectsInFrameHot = new Int2ReferenceOpenHashMap(); + var removedObjectsInFrame = new IntArrayList(); for (int i = 0; i < numFrames; i++) { - XComGameState rawFrame = rawFrames[i]; - var parsedFrame = new HistoryFrame(rawFrame.HistoryIndex, rawFrame.TimeStamp); - progressTextCallback.accept("Parsing objects for history frame " + rawFrame.HistoryIndex); + removedObjectsInFrame.forEach(objId -> stateObjectsInFrameHot.put(objId, null)); + removedObjectsInFrame.clear(); - var contextEntry = historyIndex.getEntry(rawFrame.StateChangeContext.index()); - contextTypes.add(contextEntry.getType()); - var contextVisitor = new GenericObjectVisitor(null); - historyIndex.parseObject(contextEntry, contextVisitor); - var parsedContext = new GameStateContext( - contextEntry.getLength(), contextVisitor.getRootObject(), parsedFrame, frames, contextSummarizer, problemsDetected); + var frame = parsedFrames[i]; + progressTextCallback.accept("Building history frame " + frame.getNumber()); - for (var stateObjectRef : rawFrame.GameStates) { - var stateObjectEntry = historyIndex.getEntry(stateObjectRef.index()); - if (detectedSingletonTypes.contains(stateObjectEntry.getType())) { - singletonStates.putIfAbsent(stateObjectEntry, rawFrame.HistoryIndex); - continue; + var hotMapUpdates = objectsChangedInFrame.get(i); + if (hotMapUpdates.isEmpty()) { + frame.setObjectsHot(stateObjectsInFrameHot); + } else { + for (var changedObject : hotMapUpdates) { + stateObjectsInFrameHot.put(changedObject.getObjectId(), changedObject); + if (changedObject.isRemoved()) { + removedObjectsInFrame.add(changedObject.getObjectId()); + } } - - objectTypes.add(stateObjectEntry.getType()); - var previousVersionIndex = stateObjectEntry.getPreviousVersionIndex(); - var previousVersion = previousVersionIndex == -1 ? null : parsedObjects.get(previousVersionIndex); - var stateObjectVisitor = new GenericObjectVisitor(previousVersion); - historyIndex.parseObject(stateObjectEntry, stateObjectVisitor); - var stateObject = stateObjectVisitor.getRootObject(); - if (stateObject.properties.get(OBJECT_ID) == null && - (int) stateObject.properties.get(PREV_FRAME_HIST_INDEX) > rawFrame.HistoryIndex) { - // object has no ID and previous frame index points to the future - // this is a sign of https://github.com/rcd47/xcom-2-data/issues/2 - // note that in this situation, the previousVersionIndex above is -1 - // so we are not corrupting our tracking of any objects by doing parseObject() - problemsDetected.add(new HistoryFileProblem( - parsedFrame, - null, - null, - String.format( - PROBLEM_MODIFIED_OLD_STATE, - stateObjectEntry.getType().getOriginal(), - stateObject.properties.get(PREV_FRAME_HIST_INDEX)))); - } else { - parsedObjects.put(stateObjectRef.index(), stateObject); - new GameStateObject(stateObjectEntry.getLength(), stateObjects, stateObject, parsedFrame, objectSummarizer, problemsDetected); // adds itself to the map + if (stateObjectsInFrameHot.size() > 100) { // 100 is an arbitrary number I picked + stateObjectsInFrameCold = HistoryFrame.combineMaps(stateObjectsInFrameCold, stateObjectsInFrameHot); + stateObjectsInFrameHot.clear(); } + frame.setObjectsHot(new Int2ReferenceOpenHashMap(stateObjectsInFrameHot)); } - parsedFrame.finish(parsedContext, Map.copyOf(stateObjects)); - frames.put(parsedFrame.getNumber(), parsedFrame); - progressPercentCallback.accept(((double) i + 1) / numFrames); + frame.setObjectsCold(stateObjectsInFrameCold); + frame.getContext().finishBuilding(); + + progressPercentCallback.accept(0.75 + ((((double) i) / numFrames) / PROGRESS_BAR_PHASES)); } + // parse singletons progressTextCallback.accept("Parsing singletons"); var singletons = singletonStates - .entrySet() .stream() - .map(entry -> { - var key = entry.getKey(); - var stateObjectVisitor = new GenericObjectVisitor(null); + .map(state -> { + var properties = new X2VersionedMap(0); + visitor.setRootObject(0, properties); try { - historyIndex.parseObject(key, stateObjectVisitor); + historyIndex.parseObject(state.fileEntry, visitor); } catch (IOException e) { // should never happen throw new UncheckedIOException(e); } - return new HistorySingletonObject(key.getLength(), entry.getValue(), stateObjectVisitor.getRootObject()); + return new HistorySingletonObject( + state.fileEntry.getLength(), state.frame.getNumber(), + state.fileEntry.getType(), properties, interner); }) .sorted(Comparator .comparing(s -> s.getType()) @@ -157,16 +231,135 @@ public HistoryFile read(FileChannel in, DoubleConsumer progressPercentCallback, .toList(); return new HistoryFile( - history, - List.copyOf(frames.values()), + history.CurrRandomSeed, + history.NumArchivedFrames, + Arrays.asList(parsedFrames), singletons, problemsDetected, contextTypes.stream().sorted().toList(), - objectTypes.stream().sorted().toList()); + objectTypes.stream().sorted().toList(), + interner); } } finally { Files.deleteIfExists(decompressedFile); } } - + + private void readInParallel(String threadNamePrefix, FailableRunnable task) throws Exception { + var errorRef = new AtomicReference(); + int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors() / 2); // TODO should be configurable + var threads = new Thread[numThreads]; + for (int i = 0; i < numThreads; i++) { + threads[i] = new Thread( + () -> { + try { + task.run(); + } catch (Throwable t) { + if (errorRef.compareAndSet(null, t)) { + for (var thread : threads) { + thread.interrupt(); + } + } else { + errorRef.get().addSuppressed(t); + } + } + }, + threadNamePrefix + i); + } + for (var thread : threads) { + thread.start(); // reminder: threads array must be fully populated before starting them + } + for (var thread : threads) { + thread.join(); // wait for all threads to finish + } + var error = errorRef.get(); + if (error != null) { + throw error instanceof Exception ex ? ex : new RuntimeException(error); + } + } + + private void readGameStateContexts( + AtomicInteger nextArrayIndex, X2HistoryIndex historyIndex, + X2HistoryIndexEntry[] contextArray, HistoryFrame[] frameArray, int frameOffset, + PrimitiveInterner interner, List problemsDetected, + AtomicInteger objectsRemaining, Consumer progressTextCallback, + DoubleConsumer progressPercentCallback, int objectsTotal) throws IOException { + var visitor = new VersionedObjectVisitor(interner); + int arrayIndex; + while (!Thread.interrupted() && (arrayIndex = nextArrayIndex.getAndIncrement()) < contextArray.length) { + var contextEntry = contextArray[arrayIndex]; + var contextObject = new X2VersionedMap(0); + visitor.setRootObject(0, contextObject); + historyIndex.parseObject(contextEntry, visitor); + contextObject.parseFinished(); + var frame = frameArray[arrayIndex]; + frame.setContext(new GameStateContext( + contextEntry.getLength(), contextEntry.getType(), contextObject, frame, frameOffset, + frameArray, interner, ScriptPreferences.CONTEXT_SUMMARY.getExecutable(), problemsDetected)); + int remaining = objectsRemaining.decrementAndGet(); + progressTextCallback.accept("Parsing contexts. " + remaining + " remaining."); + progressPercentCallback.accept(0.5 + ((1 - (((double) remaining) / objectsTotal)) / PROGRESS_BAR_PHASES)); + } + } + + private void readGameStateObjects( + int frameOffset, X2HistoryIndex historyIndex, List> objectsChangedInFrame, + PrimitiveInterner interner, List problemsDetected, + BlockingQueue> queue, AtomicInteger objectsRemaining, + Consumer progressTextCallback, DoubleConsumer progressPercentCallback, + int objectsTotal) throws IOException { + var visitor = new VersionedObjectVisitor(interner); + List states; + while (!Thread.interrupted() && (states = queue.poll()) != null) { + var parsedObject = new X2VersionedMap(states.getFirst().frame.getNumber()); + GameStateObject previousVersion = null; + for (var state : states) { + int frameNum = state.frame.getNumber(); + visitor.setRootObject(frameNum, parsedObject); + historyIndex.parseObject(state.fileEntry, visitor); + var parsedObjectFields = parsedObject.getValueAt(frameNum); + if (!detectModifyingOldState(parsedObjectFields, state.frame, state.fileEntry, problemsDetected)) { + previousVersion = new GameStateObject( + state.fileEntry.getLength(), previousVersion, parsedObject, state.fileEntry.getType(), + state.frame, interner, ScriptPreferences.STATE_OBJECT_SUMMARY.getExecutable(), problemsDetected); + objectsChangedInFrame.get(frameNum - frameOffset).add(previousVersion); + } + } + parsedObject.parseFinished(); + int remaining = objectsRemaining.decrementAndGet(); + progressTextCallback.accept("Parsing objects. " + remaining + " remaining."); + progressPercentCallback.accept(0.25 + ((1 - (((double) remaining) / objectsTotal)) / PROGRESS_BAR_PHASES)); + } + } + + private boolean detectModifyingOldState( + Map parsedObjectFields, HistoryFrame frame, + X2HistoryIndexEntry fileEntry, List problemsDetected) { + if (parsedObjectFields.get(OBJECT_ID) == null) { + // object has no ID + // this is a sign of https://github.com/rcd47/xcom-2-data/issues/2 + // originally I only noticed it with PreviousHistoryFrameIndex pointing to the future + // previousVersionIndex was always -1 in that case + // but now I have also seen cases where PreviousHistoryFrameIndex correctly points to the past + // in that situation, previousVersionIndex apparently still points to an object in a future frame + problemsDetected.add(new HistoryFileProblem( + frame, + null, + null, + String.format(PROBLEM_MODIFIED_OLD_STATE, fileEntry.getType().getOriginal()))); + return true; + } + return false; + } + + private static class GameStateObjectState { + private final X2HistoryIndexEntry fileEntry; + private final HistoryFrame frame; + + public GameStateObjectState(X2HistoryIndexEntry fileEntry, HistoryFrame frame) { + this.fileEntry = fileEntry; + this.frame = frame; + } + } + } diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFrame.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFrame.java index 0ff9362..bbfa069 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFrame.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistoryFrame.java @@ -1,24 +1,21 @@ package com.github.rcd47.x2data.explorer.file; -import java.util.Map; +import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; public class HistoryFrame implements Comparable { private final int number; private final String timestamp; private GameStateContext context; - private Map objects; + private Int2ReferenceOpenHashMap objectsCold; + private Int2ReferenceOpenHashMap objectsHot; public HistoryFrame(int number, String timestamp) { this.number = number; this.timestamp = timestamp; } - void finish(GameStateContext context, Map objects) { - this.context = context; - this.objects = objects; - } - public int getNumber() { return number; } @@ -31,8 +28,24 @@ public GameStateContext getContext() { return context; } - public Map getObjects() { - return objects; + public void setContext(GameStateContext context) { + this.context = context; + } + + public void setObjectsCold(Int2ReferenceOpenHashMap objectsCold) { + this.objectsCold = objectsCold; + } + + public void setObjectsHot(Int2ReferenceOpenHashMap objectsHot) { + this.objectsHot = objectsHot; + } + + public Int2ReferenceMap getObjectsCombined() { + return combineMaps(objectsCold, objectsHot); + } + + public GameStateObject getObject(int id) { + return objectsHot.getOrDefault(id, objectsCold.get(id)); } @Override @@ -40,4 +53,18 @@ public int compareTo(HistoryFrame o) { return Integer.compare(number, o.number); } + public static Int2ReferenceOpenHashMap combineMaps( + Int2ReferenceOpenHashMap coldMap, Int2ReferenceOpenHashMap hotMap) { + var combinedMap = new Int2ReferenceOpenHashMap<>(coldMap); + hotMap.int2ReferenceEntrySet().fastForEach(e -> { + if (e.getValue() == null) { + combinedMap.remove(e.getIntKey()); + } else { + combinedMap.put(e.getIntKey(), e.getValue()); + } + }); + combinedMap.trim(); + return combinedMap; + } + } diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistorySingletonObject.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistorySingletonObject.java index 706c3a2..a6ed7bd 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistorySingletonObject.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/HistorySingletonObject.java @@ -1,10 +1,12 @@ package com.github.rcd47.x2data.explorer.file; -import java.util.HashMap; -import java.util.Map; - +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 javafx.scene.control.TreeItem; + public class HistorySingletonObject implements ISizedObject { private static final UnrealName OBJECT_ID = new UnrealName("ObjectID"); @@ -13,15 +15,14 @@ public class HistorySingletonObject implements ISizedObject { private final int firstFrame; private final int objectId; private final UnrealName type; - private final Map fields; + private final TreeItem tree; - public HistorySingletonObject(int sizeInFile, int firstFrame, GenericObject object) { + public HistorySingletonObject(int sizeInFile, int firstFrame, UnrealName type, X2VersionedMap properties, PrimitiveInterner interner) { this.sizeInFile = sizeInFile; this.firstFrame = firstFrame; - objectId = (int) object.properties.get(OBJECT_ID); - type = object.type; - fields = new HashMap<>(); - object.properties.forEach((k, v) -> fields.put(k, new NonVersionedField(v))); + this.type = type; + objectId = (int) properties.getValueAt(0).get(OBJECT_ID); + tree = properties.getTreeNodeAt(interner, null, 0, false); } @Override @@ -41,8 +42,8 @@ public UnrealName getType() { return type; } - public Map getFields() { - return fields; + public TreeItem getTree() { + return tree; } } diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/NonVersionedField.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/NonVersionedField.java deleted file mode 100644 index a42cc09..0000000 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/NonVersionedField.java +++ /dev/null @@ -1,75 +0,0 @@ -package com.github.rcd47.x2data.explorer.file; - -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; - -import javafx.scene.control.TreeItem; - -public class NonVersionedField { - - private final Object value; - private final Map children; - - public NonVersionedField(Object in) { - if (in instanceof GenericObject object) { - value = null; - children = new HashMap<>(); - object.properties.forEach((k, v) -> children.put(k, new NonVersionedField(v))); - } else if (in instanceof List list) { - value = null; - children = new HashMap<>(); - for (int i = 0; i < list.size(); i++) { - children.put(new UnrealName(Integer.toString(i)), new NonVersionedField(list.get(i))); - } - } else if (in instanceof Map map) { - value = null; - children = new HashMap<>(); - map.forEach((k, v) -> children.put(UnrealName.from(k), new NonVersionedField(v))); - } else { - value = in; - children = null; - } - } - - // Groovy support - public Object propertyMissing(String name) { - return children == null ? null : children.get(new UnrealName(name)); - } - - public Object getValue() { - return value; - } - - public Map getChildren() { - return children; - } - - public static TreeItem> convertToTreeItems(Map fields) { - var root = new TreeItem>(); - var rootChildren = root.getChildren(); - fields.entrySet() - .stream() - .sorted((a, b) -> a.getKey().compareTo(b.getKey())) - .forEachOrdered(e -> rootChildren.add(convertToTreeItem(e))); - return root; - } - - private static TreeItem> convertToTreeItem(Entry entry) { - var item = new TreeItem>(entry); - var fieldChildren = entry.getValue().getChildren(); - if (fieldChildren != null) { - var itemChildren = item.getChildren(); - fieldChildren - .entrySet() - .stream() - .sorted((a, b) -> a.getKey().compareTo(b.getKey())) - .forEachOrdered(e -> itemChildren.add(convertToTreeItem(e))); - } - return item; - } - -} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/FieldChangeType.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/FieldChangeType.java similarity index 53% rename from x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/FieldChangeType.java rename to x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/FieldChangeType.java index cbb4966..171ab30 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/FieldChangeType.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/FieldChangeType.java @@ -1,4 +1,4 @@ -package com.github.rcd47.x2data.explorer.file; +package com.github.rcd47.x2data.explorer.file.data; public enum FieldChangeType { diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/PrimitiveInterner.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/PrimitiveInterner.java new file mode 100644 index 0000000..5dc05e6 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/PrimitiveInterner.java @@ -0,0 +1,68 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.function.Function; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +import it.unimi.dsi.fastutil.doubles.Double2ReferenceMap; +import it.unimi.dsi.fastutil.doubles.Double2ReferenceMaps; +import it.unimi.dsi.fastutil.doubles.Double2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.floats.Float2ReferenceMap; +import it.unimi.dsi.fastutil.floats.Float2ReferenceMaps; +import it.unimi.dsi.fastutil.floats.Float2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.ints.Int2ReferenceMap; +import it.unimi.dsi.fastutil.ints.Int2ReferenceMaps; +import it.unimi.dsi.fastutil.ints.Int2ReferenceOpenHashMap; + +public class PrimitiveInterner { + + private final Float2ReferenceMap floats; + private final Double2ReferenceMap doubles; + private final Int2ReferenceMap ints; + private final ConcurrentMap names; + private final ConcurrentMap strings; + private final Int2ReferenceMap treeNodeArrayIndexes; + private final ConcurrentMap treeNodeMapKeys; + + public PrimitiveInterner() { + floats = Float2ReferenceMaps.synchronize(new Float2ReferenceOpenHashMap<>()); + doubles = Double2ReferenceMaps.synchronize(new Double2ReferenceOpenHashMap<>()); + ints = Int2ReferenceMaps.synchronize(new Int2ReferenceOpenHashMap<>()); + names = new ConcurrentHashMap<>(); + strings = new ConcurrentHashMap<>(); + treeNodeArrayIndexes = Int2ReferenceMaps.synchronize(new Int2ReferenceOpenHashMap<>()); + treeNodeMapKeys = new ConcurrentHashMap<>(); + } + + public Float internFloat(float value) { + return floats.computeIfAbsent(value, k -> k); // Function.identity() would trigger auto-boxing + } + + public Double internDouble(double value) { + return doubles.computeIfAbsent(value, k -> k); // Function.identity() would trigger auto-boxing + } + + public Integer internInt(int value) { + return ints.computeIfAbsent(value, k -> k); // Function.identity() would trigger auto-boxing + } + + public UnrealName internName(UnrealName value) { + return names.computeIfAbsent(value, Function.identity()); + } + + public String internString(String value) { + return strings.computeIfAbsent(value, Function.identity()); + } + + public UnrealName internTreeNodeArrayIndex(int value) { + return treeNodeArrayIndexes.computeIfAbsent(value, k -> new UnrealName(Integer.toString(k))); + } + + public UnrealName internTreeNodeMapKey(Object value) { + // most keys are names + return value instanceof UnrealName n ? n : treeNodeMapKeys.computeIfAbsent(value, k -> new UnrealName(k.toString())); + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitor.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitor.java new file mode 100644 index 0000000..e42fc43 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitor.java @@ -0,0 +1,222 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.ArrayDeque; +import java.util.Deque; + +import com.github.rcd47.x2data.lib.unreal.IUnrealObjectVisitor; +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +public class VersionedObjectVisitor implements IUnrealObjectVisitor { + + private PrimitiveInterner interner; + private Deque> objectStack = new ArrayDeque<>(6); + private Deque stateStack = new ArrayDeque<>(6); + private int frame; + private Object nextMapKey; + private int nextStaticArrayIndex; + private int deltaDisabledDepth; + + private static enum VisitorState { + ARRAY_ELEMENT, MAP_KEY, MAP_VALUE + } + + public VersionedObjectVisitor(PrimitiveInterner interner) { + this.interner = interner; + } + + public void setRootObject(int frameNum, X2VersionedMap obj) { + if (!objectStack.isEmpty()) { + throw new IllegalStateException("objectStack is not empty"); + } + objectStack.push(obj); + frame = frameNum; + } + + @Override + public void visitStructStart(UnrealName type) { + if (!stateStack.isEmpty()) { + switch (stateStack.pop()) { + case MAP_VALUE -> { + stateStack.push(VisitorState.MAP_KEY); + var parentUntyped = objectStack.peek(); + if (parentUntyped instanceof X2VersionedMap parent) { + objectStack.push(parent.getOrCreateChild(frame, nextMapKey, nextStaticArrayIndex, () -> new X2VersionedMap(frame))); + } else if (parentUntyped instanceof X2VersionedStaticArray parent) { + objectStack.push(parent.getOrCreateElement(frame, nextStaticArrayIndex, () -> new X2VersionedMap(frame))); + } else { + throw new IllegalStateException("Map value parent is unsupported type " + parentUntyped.getClass()); + } + } + case ARRAY_ELEMENT -> { + stateStack.push(VisitorState.ARRAY_ELEMENT); + objectStack.push(((X2VersionedDynamicArray) objectStack.peek()).getOrCreateElement(frame, () -> new X2VersionedMap(frame))); + } + default -> throw new IllegalStateException("Invalid state for struct start " + stateStack.peek()); + } + } + stateStack.push(VisitorState.MAP_KEY); + } + + @Override + public void visitStructEnd() { + popAndCheckState(VisitorState.MAP_KEY); + } + + @Override + public void visitDynamicArrayStart(int size) { + switch (stateStack.pop()) { + case MAP_VALUE -> { + stateStack.push(VisitorState.MAP_KEY); + objectStack.push(((X2VersionedMap) objectStack.peek()) + .getOrCreateChild(frame, nextMapKey, nextStaticArrayIndex, () -> new X2VersionedDynamicArray(frame, size))); + } + default -> throw new IllegalStateException("Invalid state for dynamic array start " + stateStack.peek()); + } + deltaDisabledDepth++; + stateStack.push(VisitorState.ARRAY_ELEMENT); + } + + @Override + public void visitDynamicArrayEnd() { + decrementDeltaDisabledDepth(VisitorState.ARRAY_ELEMENT); + } + + @Override + public void visitMapStart(int size) { + deltaDisabledDepth++; + visitStructStart(null); + } + + @Override + public void visitMapEnd() { + decrementDeltaDisabledDepth(VisitorState.MAP_KEY); + } + + @Override + public void visitUnparseableData(ByteBuffer data) { + // decided not to intern these for now + // if their mem usage is a problem, creating proper mappings would probably be a better solution + data = ByteBuffer.allocate(data.remaining()).order(ByteOrder.LITTLE_ENDIAN).put(data).flip(); + if (stateStack.peek() == VisitorState.MAP_KEY) { + ((X2VersionedMap) objectStack.peek()) + .getOrCreateChild(frame, "native vars", 0, () -> new X2VersionedPrimitive()) + .setValueAt(frame, data); + } else { + visitPrimitiveValue(data); + } + } + + @Override + public void visitProperty(UnrealName propertyName, int staticArrayIndex) { + nextStaticArrayIndex = staticArrayIndex; + visitPrimitiveValue(interner.internName(propertyName)); + } + + @Override + public void visitBooleanValue(boolean value) { + visitPrimitiveValue(value); // no need to intern boolean since Java caches the boxed objects + } + + @Override + public void visitByteValue(byte value) { + visitPrimitiveValue(value); // no need to intern byte since Java caches the boxed objects + } + + @Override + public void visitEnumValue(UnrealName enumType, UnrealName value) { + visitPrimitiveValue(interner.internName(value)); + } + + @Override + public void visitFloatValue(float value) { + visitPrimitiveValue(interner.internFloat(value)); + } + + @Override + public void visitDoubleValue(double value) { + visitPrimitiveValue(interner.internDouble(value)); + } + + @Override + public void visitIntValue(int value) { + visitPrimitiveValue(interner.internInt(value)); + } + + @Override + public void visitNameValue(UnrealName value) { + visitPrimitiveValue(interner.internName(value)); + } + + @Override + public void visitStringValue(String value) { + visitPrimitiveValue(interner.internString(value)); + } + + @Override + public void visitBasicDelegateValue(UnrealName delegateName, String declaringClass) { + visitPrimitiveValue(interner.internName(delegateName)); + } + + @Override + public void visitBasicInterfaceValue(UnrealName objectName) { + visitPrimitiveValue(interner.internName(objectName)); + } + + @Override + public void visitBasicObjectValue(UnrealName objectName) { + visitPrimitiveValue(interner.internName(objectName)); + } + + @Override + public void visitHistoryDelegateValue(int objectIndex, UnrealName delegateName, String declaringClass) { + visitPrimitiveValue(interner.internInt(objectIndex)); + } + + @Override + public void visitHistoryInterfaceValue(int objectIndex) { + visitPrimitiveValue(interner.internInt(objectIndex)); + } + + @Override + public void visitHistoryObjectValue(int objectIndex) { + visitPrimitiveValue(interner.internInt(objectIndex)); + } + + private void decrementDeltaDisabledDepth(VisitorState expectedState) { + popAndCheckState(expectedState); + deltaDisabledDepth--; + } + + private void popAndCheckState(VisitorState expectedState) { + var actualState = stateStack.pop(); + if (actualState != expectedState) { + throw new IllegalStateException("Expected state " + expectedState + " but found " + actualState); + } + objectStack.pop().frameFinished(frame, deltaDisabledDepth > 0); + } + + private void visitPrimitiveValue(Object value) { + var state = stateStack.pop(); + switch (state) { + case MAP_KEY -> { + nextMapKey = value; + stateStack.push(VisitorState.MAP_VALUE); + } + case MAP_VALUE -> { + ((X2VersionedMap) objectStack.peek()) + .getOrCreateChild(frame, nextMapKey, nextStaticArrayIndex, () -> new X2VersionedPrimitive()) + .setValueAt(frame, value); + stateStack.push(VisitorState.MAP_KEY); + } + case ARRAY_ELEMENT -> { + ((X2VersionedDynamicArray) objectStack.peek()) + .getOrCreateElement(frame, () -> new X2VersionedPrimitive()) + .setValueAt(frame, value); + stateStack.push(VisitorState.ARRAY_ELEMENT); + } + } + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDataContainer.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDataContainer.java new file mode 100644 index 0000000..c111b01 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDataContainer.java @@ -0,0 +1,41 @@ +package com.github.rcd47.x2data.explorer.file.data; + +public abstract class X2VersionedDataContainer extends X2VersionedDatum { + + public X2VersionedDataContainer(int frame) { + frames[0] = frame; + changes[0] = FieldChangeType.ADDED; + numFrames = 1; + } + + protected abstract Iterable> getChildren(); + + protected abstract void createFrameValue(int frame); + + public void frameFinished(int frame, boolean deltaDisabled) { + if (deltaDisabled) { + var anyChildRemoved = false; + for (var child : getChildren()) { + if (child.lastFrameTouched != frame && child.changes[child.numFrames - 1] != FieldChangeType.REMOVED) { + child.markRemoved(frame); + anyChildRemoved = true; + } + } + if (anyChildRemoved) { + descendantValueSet(frame); + } + } + if (frames[numFrames - 1] == frame && changes[numFrames - 1] != FieldChangeType.REMOVED) { + createFrameValue(frame); + } + } + + @Override + public void markRemoved(int frame) { + super.markRemoved(frame); + for (var child : getChildren()) { + child.markRemoved(frame); + } + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatum.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatum.java new file mode 100644 index 0000000..3aae3f8 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatum.java @@ -0,0 +1,115 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.Arrays; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +public abstract class X2VersionedDatum { + + protected X2VersionedDataContainer parent; + protected int[] frames; + protected FieldChangeType[] changes; + protected Object[] values; + protected int numFrames; + protected int lastFrameTouched; + + public X2VersionedDatum() { + frames = new int[4]; + changes = new FieldChangeType[4]; + values = new Object[4]; + } + + protected void appendChange(int frame, FieldChangeType change) { + if (numFrames != 0 && frames[numFrames - 1] == frame) { + return; + } + if (frames.length == numFrames) { + int newLength = numFrames + Math.min(numFrames, 1024); + frames = Arrays.copyOf(frames, newLength); + changes = Arrays.copyOf(changes, newLength); + values = Arrays.copyOf(values, newLength); + } + frames[numFrames] = frame; + changes[numFrames] = change; + numFrames++; + } + + protected int getIndexForFrame(int frame) { + int index = Arrays.binarySearch(frames, 0, numFrames, frame); + return index < 0 ? ((index * -1) - 2) : index; + } + + public void markRemoved(int frame) { + appendChange(frame, FieldChangeType.REMOVED); + } + + public void descendantValueSet(int frame) { + appendChange(frame, changes[numFrames - 1] == FieldChangeType.REMOVED ? FieldChangeType.ADDED : FieldChangeType.CHANGED); + if (parent != null) { + parent.descendantValueSet(frame); + } + } + + public final void parseFinished() { + if (frames.length != numFrames) { + frames = Arrays.copyOf(frames, numFrames); + changes = Arrays.copyOf(changes, numFrames); + values = Arrays.copyOf(values, numFrames); + parseFinishedExtra(); + } + } + + protected void parseFinishedExtra() { + // no-op + } + + public TreeItem getTreeNodeAt(PrimitiveInterner interner, UnrealName nodeName, int frame, boolean onlyModified) { + var index = getIndexForFrame(frame); + if (index < 0) { + return null; + } + var frameAtIndex = frames[index]; + if (onlyModified && frameAtIndex != frame) { + return null; + } + var change = changes[index]; + if (frameAtIndex != frame) { + if (change == FieldChangeType.REMOVED) { + return null; + } + change = FieldChangeType.NONE; + } + + var hasPrev = index != 0; + var hasNext = index != numFrames - 1; + var treeItem = new TreeItem(new X2VersionedDatumTreeItem( + nodeName, + getValueForTreeNode(index), + change, + hasPrev ? getValueForTreeNode(index - 1) : null, + hasNext ? getValueForTreeNode(index + 1) : null, + hasPrev ? frames[index - 1] : Integer.MIN_VALUE, + hasNext ? frames[index + 1] : Integer.MAX_VALUE)); + addChildrenToTreeNode(interner, frame, onlyModified, treeItem.getChildren()); + return treeItem; + } + + protected Object getValueForTreeNode(int index) { + return null; + } + + protected void addChildrenToTreeNode( + PrimitiveInterner interner, int frame, boolean onlyModified, ObservableList> treeChildren) { + // no-op + } + + @SuppressWarnings("unchecked") + public T getValueAt(int frame) { + var index = getIndexForFrame(frame); + return index < 0 ? null : (T) values[getIndexForFrame(frame)]; + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatumTreeItem.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatumTreeItem.java new file mode 100644 index 0000000..2afdd4e --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDatumTreeItem.java @@ -0,0 +1,59 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +public class X2VersionedDatumTreeItem implements Comparable { + + private final UnrealName name; + private final Object value; + private final FieldChangeType changeType; + private final Object previousValue; + private final Object nextValue; + private final int previousFrame; + private final int nextFrame; + + public X2VersionedDatumTreeItem(UnrealName name, Object value, FieldChangeType changeType, Object previousValue, + Object nextValue, int previousFrame, int nextFrame) { + this.name = name; + this.value = value; + this.changeType = changeType; + this.previousValue = previousValue; + this.nextValue = nextValue; + this.previousFrame = previousFrame; + this.nextFrame = nextFrame; + } + + @Override + public int compareTo(X2VersionedDatumTreeItem o) { + return name.compareTo(o.name); + } + + public UnrealName getName() { + return name; + } + + public Object getValue() { + return value; + } + + public FieldChangeType getChangeType() { + return changeType; + } + + public Object getPreviousValue() { + return previousValue; + } + + public Object getNextValue() { + return nextValue; + } + + public int getPreviousFrame() { + return previousFrame; + } + + public int getNextFrame() { + return nextFrame; + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDynamicArray.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDynamicArray.java new file mode 100644 index 0000000..e1df511 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedDynamicArray.java @@ -0,0 +1,71 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +public class X2VersionedDynamicArray extends X2VersionedDataContainer> { + + private ArrayList> elements; + private int nextIndex; + + public X2VersionedDynamicArray(int frame, int size) { + super(frame); + elements = new ArrayList<>(size); + } + + @SuppressWarnings("unchecked") + public > T getOrCreateElement(int frame, Supplier creator) { + if (elements.size() == nextIndex) { + var newElement = creator.get(); + newElement.parent = this; + elements.add(newElement); + } + var element = elements.get(nextIndex++); + element.lastFrameTouched = frame; + return (T) element; + } + + @Override + public void frameFinished(int frame, boolean deltaDisabled) { + super.frameFinished(frame, deltaDisabled); + nextIndex = 0; + } + + @Override + protected void createFrameValue(int frame) { + var frameValue = new ArrayList<>(nextIndex); + for (int i = 0; i < nextIndex; i++) { + frameValue.add(elements.get(i).getValueAt(frame)); + } + values[numFrames - 1] = frameValue; + } + + @Override + protected void parseFinishedExtra() { + elements.trimToSize(); + for (var element : elements) { + element.parseFinished(); + } + } + + @Override + protected void addChildrenToTreeNode( + PrimitiveInterner interner, int frame, boolean onlyModified, ObservableList> treeChildren) { + for (int i = 0; i < elements.size(); i++) { + var childNode = elements.get(i).getTreeNodeAt(interner, interner.internTreeNodeArrayIndex(i), frame, onlyModified); + if (childNode != null) { + treeChildren.add(childNode); + } + } + } + + @Override + protected Iterable> getChildren() { + return elements; + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMap.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMap.java new file mode 100644 index 0000000..8dd7976 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMap.java @@ -0,0 +1,101 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.Comparator; +import java.util.Map; +import java.util.function.Supplier; + +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenCustomHashMap; +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenHashMap; +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +public class X2VersionedMap extends X2VersionedDataContainer> { + + private Object2ReferenceOpenHashMap> children; // key type is not UnrealName so that we can handle native maps + + public X2VersionedMap(int frame) { + super(frame); + children = new Object2ReferenceOpenHashMap<>(); + } + + @SuppressWarnings("unchecked") + public > T getOrCreateChild(int frame, Object key, int staticArrayIndex, Supplier creator) { + var datum = children.get(key); + + if (staticArrayIndex == 0) { + if (datum == null) { + datum = creator.get(); + datum.parent = this; + children.put(key, datum); + } + if (!(datum instanceof X2VersionedStaticArray)) { + datum.lastFrameTouched = frame; + return (T) datum; + } + } + + X2VersionedStaticArray array; + if (datum instanceof X2VersionedStaticArray datumArray) { + array = datumArray; + } else { // could be null, primitive, or struct + array = new X2VersionedStaticArray(frame, staticArrayIndex + 1); + array.parent = this; + children.put(key, array); + if (datum != null) { + var datumFinal = datum; + array.getOrCreateElement(frame, 0, () -> datumFinal); // will always create + } + } + array.lastFrameTouched = frame; + return array.getOrCreateElement(frame, staticArrayIndex, creator); + } + + @Override + public void frameFinished(int frame, boolean deltaDisabled) { + for (var child : getChildren()) { + if (child instanceof X2VersionedStaticArray array) { + array.frameFinished(frame, deltaDisabled); + } + } + super.frameFinished(frame, deltaDisabled); + } + + @Override + protected void createFrameValue(int frame) { + var plainMap = new Object2ReferenceOpenCustomHashMap(children.size(), new X2VersionedMapChildrenStrategy()); + children.forEach((k, v) -> { + var plainValue = v.getValueAt(frame); + if (plainValue != null) { + plainMap.put(k, plainValue); + } + }); + plainMap.trim(); + values[numFrames - 1] = plainMap; + } + + @Override + protected void parseFinishedExtra() { + children.trim(); + for (var child : children.values()) { + child.parseFinished(); + } + } + + @Override + protected void addChildrenToTreeNode( + PrimitiveInterner interner, int frame, boolean onlyModified, ObservableList> treeChildren) { + children.forEach((k, v) -> { + var childNode = v.getTreeNodeAt(interner, interner.internTreeNodeMapKey(k), frame, onlyModified); + if (childNode != null) { + treeChildren.add(childNode); + } + }); + treeChildren.sort(Comparator.comparing(TreeItem::getValue)); + } + + @Override + protected Iterable> getChildren() { + return children.values(); + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategy.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategy.java new file mode 100644 index 0000000..124671d --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategy.java @@ -0,0 +1,33 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.Locale; +import java.util.Objects; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +import it.unimi.dsi.fastutil.Hash.Strategy; + +public class X2VersionedMapChildrenStrategy implements Strategy { + + @Override + public int hashCode(Object o) { + // make hash match for UnrealName and equivalent strings + if (o instanceof String s) { + // TODO maybe implement a cache for the lowercase conversion? + return s.toLowerCase(Locale.ENGLISH).hashCode(); + } + return o == null ? 0 : o.hashCode(); + } + + @Override + public boolean equals(Object a, Object b) { + if (a instanceof UnrealName name && b instanceof String string) { + return string.equalsIgnoreCase(name.getOriginal()); + } + if (b instanceof UnrealName name && a instanceof String string) { + return string.equalsIgnoreCase(name.getOriginal()); + } + return Objects.equals(a, b); + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedPrimitive.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedPrimitive.java new file mode 100644 index 0000000..dc57276 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedPrimitive.java @@ -0,0 +1,33 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.Objects; + +public class X2VersionedPrimitive extends X2VersionedDatum { + + public void setValueAt(int frame, Object value) { + lastFrameTouched = frame; + + if (numFrames != 0 && Objects.equals(values[numFrames - 1], value)) { + // can occur if we're inside a non-delta'd object (dynamic array or native map) + return; + } + + FieldChangeType change; + if (value == null) { + change = FieldChangeType.REMOVED; + } else if (numFrames == 0 || values[numFrames - 1] == null) { + change = FieldChangeType.ADDED; + } else { + change = FieldChangeType.CHANGED; + } + appendChange(frame, change); + values[numFrames - 1] = value; + parent.descendantValueSet(frame); + } + + @Override + protected Object getValueForTreeNode(int index) { + return values[index]; + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedStaticArray.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedStaticArray.java new file mode 100644 index 0000000..4cd5c18 --- /dev/null +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedStaticArray.java @@ -0,0 +1,72 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Supplier; + +import javafx.collections.ObservableList; +import javafx.scene.control.TreeItem; + +public class X2VersionedStaticArray extends X2VersionedDataContainer> { + + private ArrayList> elements; + + public X2VersionedStaticArray(int frame, int size) { + super(frame); + elements = new ArrayList<>(size); + } + + @SuppressWarnings("unchecked") + public > T getOrCreateElement(int frame, int index, Supplier creator) { + for (int i = elements.size(); i <= index; i++) { + elements.add(null); + } + var element = elements.get(index); + if (element == null) { + element = creator.get(); + element.parent = this; + elements.set(index, element); + } + element.lastFrameTouched = frame; + return (T) element; + } + + @Override + protected void createFrameValue(int frame) { + var frameValue = new ArrayList<>(elements.size()); + for (var element : elements) { + frameValue.add(element == null ? null : element.getValueAt(frame)); + } + values[numFrames - 1] = frameValue; + } + + @Override + protected void parseFinishedExtra() { + elements.trimToSize(); + for (var element : elements) { + if (element != null) { + element.parseFinished(); + } + } + } + + @Override + protected void addChildrenToTreeNode( + PrimitiveInterner interner, int frame, boolean onlyModified, ObservableList> treeChildren) { + for (int i = 0; i < elements.size(); i++) { + var element = elements.get(i); + if (element != null) { + var childNode = elements.get(i).getTreeNodeAt(interner, interner.internTreeNodeArrayIndex(i), frame, onlyModified); + if (childNode != null) { + treeChildren.add(childNode); + } + } + } + } + + @Override + protected Iterable> getChildren() { + return elements; + } + +} diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/Main.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/Main.java index d482b6a..8fcadad 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/Main.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/Main.java @@ -11,16 +11,17 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.List; -import java.util.Map; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.rcd47.x2data.explorer.file.GenericObjectVisitor; import com.github.rcd47.x2data.explorer.file.HistoryFileReader; -import com.github.rcd47.x2data.explorer.file.NonVersionedField; +import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner; +import com.github.rcd47.x2data.explorer.file.data.VersionedObjectVisitor; +import com.github.rcd47.x2data.explorer.file.data.X2VersionedDatumTreeItem; +import com.github.rcd47.x2data.explorer.file.data.X2VersionedMap; import com.github.rcd47.x2data.explorer.jfx.ui.NonVersionedFieldUI; import com.github.rcd47.x2data.explorer.jfx.ui.ProgressUtils; import com.github.rcd47.x2data.explorer.jfx.ui.history.HistoryBloatAnalysisUI; @@ -339,13 +340,16 @@ protected void failed() { private void parseBasicSaveObjectFile( UnrealObjectParser parser, UnrealName type, VBox splitLeft, NonVersionedFieldUI objPropsUI, ByteBuffer buffer) { - var task = new Task>>() { + var task = new Task>() { @Override - protected TreeItem> call() throws Exception { + protected TreeItem call() throws Exception { buffer.rewind(); - var visitor = new GenericObjectVisitor(null); + var interner = new PrimitiveInterner(); + var visitor = new VersionedObjectVisitor(interner); + var parsedObject = new X2VersionedMap(0); + visitor.setRootObject(0, parsedObject); parser.parse(type, buffer, visitor); - return NonVersionedField.convertToTreeItems(new NonVersionedField(visitor.getRootObject()).getChildren()); + return parsedObject.getTreeNodeAt(interner, type, 0, false); } @Override diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/NonVersionedFieldUI.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/NonVersionedFieldUI.java index 943b1db..2b2f25f 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/NonVersionedFieldUI.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/NonVersionedFieldUI.java @@ -1,9 +1,6 @@ package com.github.rcd47.x2data.explorer.jfx.ui; -import java.util.Map.Entry; - -import com.github.rcd47.x2data.explorer.file.NonVersionedField; -import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; +import com.github.rcd47.x2data.explorer.file.data.X2VersionedDatumTreeItem; import javafx.beans.property.BooleanProperty; import javafx.beans.property.ObjectProperty; @@ -22,21 +19,21 @@ public class NonVersionedFieldUI { - private final TreeTableView> table; + private final TreeTableView table; private final Node node; @SuppressWarnings("unchecked") public NonVersionedFieldUI(BooleanProperty defaultExpand, String placeholder) { - var colPropName = new TreeTableColumn, String>("Property"); + var colPropName = new TreeTableColumn("Property"); colPropName.setPrefWidth(Region.USE_COMPUTED_SIZE); - colPropName.setCellValueFactory(f -> new ReadOnlyStringWrapper(f.getValue().getValue().getKey().getOriginal())); + colPropName.setCellValueFactory(f -> new ReadOnlyStringWrapper(f.getValue().getValue().getName().getOriginal())); StandardCellFactoryHelper.setFactoryForStringValueColumn(colPropName); - var colPropValue = new TreeTableColumn, Object>("Value"); + var colPropValue = new TreeTableColumn("Value"); colPropValue.setPrefWidth(Region.USE_COMPUTED_SIZE); - colPropValue.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue().getValue().getValue())); + colPropValue.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue().getValue())); StandardCellFactoryHelper.setFactoryForObjectValueColumn(colPropValue); - table = new TreeTableView>(); + table = new TreeTableView<>(); table.setPlaceholder(new Label(placeholder)); table.getColumns().addAll(colPropName, colPropValue); table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY_ALL_COLUMNS); @@ -59,7 +56,7 @@ public Node getNode() { return node; } - public ObjectProperty>> getRootProperty() { + public ObjectProperty> getRootProperty() { return table.rootProperty(); } diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryBloatAnalysisUI.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryBloatAnalysisUI.java index 7578c66..21942d1 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryBloatAnalysisUI.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryBloatAnalysisUI.java @@ -13,7 +13,6 @@ import com.github.rcd47.x2data.explorer.file.HistoryFile; import com.github.rcd47.x2data.explorer.file.HistorySingletonObject; import com.github.rcd47.x2data.explorer.file.ISizedObject; -import com.github.rcd47.x2data.explorer.file.NonVersionedField; import com.github.rcd47.x2data.explorer.jfx.ui.NonVersionedFieldUI; import com.github.rcd47.x2data.explorer.jfx.ui.ProgressUtils; import com.github.rcd47.x2data.explorer.jfx.ui.StandardCellFactoryHelper; @@ -61,7 +60,7 @@ protected Node call() throws Exception { var context = frame.getContext(); perContextClassStatsMap.computeIfAbsent(context.getType(), _ -> new IntSummaryStatistics()).accept(context.getSizeInFile()); largestContexts.add(context); - for (var gso : frame.getObjects().values()) { + for (var gso : frame.getObjectsCombined().values()) { if (gso.getFrame().equals(frame)) { perObjectStatsMap.computeIfAbsent(gso.getObjectId(), _ -> new GameStateObjectStats()).add(gso); (gso.getPreviousVersion() == null ? largestFullObjects : largestDeltaObjects).add(gso); @@ -178,7 +177,8 @@ protected Node call() throws Exception { deltaObjectsTable.getColumns().addAll( colDeltaObjId, colDeltaObjType, colDeltaSize, colDeltaObjSummary, colDeltaFrameNum, colDeltaCtxSummary); - var deltaSplitPane = new SplitPane(deltaObjectsTable, new ObjectPropertiesTable(null, deltaObjectsTable).getNode()); + var deltaSplitPane = new SplitPane( + deltaObjectsTable, new ObjectPropertiesTable(null, deltaObjectsTable, history.getInterner()).getNode()); deltaSplitPane.setOrientation(Orientation.HORIZONTAL); // largest full objects @@ -206,7 +206,8 @@ protected Node call() throws Exception { fullObjectsTable.getColumns().addAll( colFullObjId, colFullObjType, colFullSize, colFullObjSummary, colFullFrameNum, colFullCtxSummary); - var fullSplitPane = new SplitPane(fullObjectsTable, new ObjectPropertiesTable(null, fullObjectsTable).getNode()); + var fullSplitPane = new SplitPane( + fullObjectsTable, new ObjectPropertiesTable(null, fullObjectsTable, history.getInterner()).getNode()); fullSplitPane.setOrientation(Orientation.HORIZONTAL); // largest contexts @@ -231,7 +232,7 @@ protected Node call() throws Exception { GeneralPreferences.getEffective().getHistoryContextPropsTreeExpanded(), "Click a context to view its properties"); ctxProperties.getRootProperty().bind( - ctxTable.getSelectionModel().selectedItemProperty().map(f -> NonVersionedField.convertToTreeItems(f.getFields()))); + ctxTable.getSelectionModel().selectedItemProperty().map(f -> f.getTree(history.getInterner()))); var ctxSplitPane = new SplitPane(ctxTable, ctxProperties.getNode()); ctxSplitPane.setOrientation(Orientation.HORIZONTAL); @@ -257,7 +258,7 @@ protected Node call() throws Exception { GeneralPreferences.getEffective().getHistorySingletonPropsTreeExpanded(), "Click a singleton state to view its properties"); singletonPropsUI.getRootProperty().bind( - singletonsTable.getSelectionModel().selectedItemProperty().map(f -> NonVersionedField.convertToTreeItems(f.getFields()))); + singletonsTable.getSelectionModel().selectedItemProperty().map(f -> f.getTree())); var singletonSplitPane = new SplitPane(singletonsTable, singletonPropsUI.getNode()); singletonSplitPane.setOrientation(Orientation.HORIZONTAL); diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryFramesUI.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryFramesUI.java index 8c5fe3a..f5e81ee 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryFramesUI.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryFramesUI.java @@ -1,7 +1,6 @@ package com.github.rcd47.x2data.explorer.jfx.ui.history; import com.github.rcd47.x2data.explorer.file.HistoryFile; -import com.github.rcd47.x2data.explorer.file.NonVersionedField; import com.github.rcd47.x2data.explorer.jfx.ui.NonVersionedFieldUI; import com.github.rcd47.x2data.explorer.jfx.ui.prefs.GeneralPreferences; @@ -19,13 +18,13 @@ public HistoryFramesUI(HistoryFile history) { var objectsTableUI = new HistoryObjectsTable(history, framesTable); - var objectPropsTableUI = new ObjectPropertiesTable(framesTable, objectsTableUI.getObjectsTable()); + var objectPropsTableUI = new ObjectPropertiesTable(framesTable, objectsTableUI.getObjectsTable(), history.getInterner()); var contextPropsUI = new NonVersionedFieldUI( GeneralPreferences.getEffective().getHistoryContextPropsTreeExpanded(), "Click a frame to view its context's properties"); contextPropsUI.getRootProperty().bind( - framesTable.getSelectionModel().selectedItemProperty().map(f -> NonVersionedField.convertToTreeItems(f.getContext().getFields()))); + framesTable.getSelectionModel().selectedItemProperty().map(f -> f.getContext().getTree(history.getInterner()))); var framesSplit = new SplitPane(framesTableUI.getNode(), contextPropsUI.getNode()); framesSplit.setOrientation(Orientation.HORIZONTAL); diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryGeneralUI.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryGeneralUI.java index 549ff6b..721c651 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryGeneralUI.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryGeneralUI.java @@ -4,7 +4,6 @@ import com.github.rcd47.x2data.explorer.file.HistoryFile; import com.github.rcd47.x2data.explorer.file.HistorySingletonObject; -import com.github.rcd47.x2data.explorer.file.NonVersionedField; import com.github.rcd47.x2data.explorer.jfx.ui.NonVersionedFieldUI; import com.github.rcd47.x2data.explorer.jfx.ui.StandardCellFactoryHelper; import com.github.rcd47.x2data.explorer.jfx.ui.prefs.GeneralPreferences; @@ -54,9 +53,8 @@ public HistoryGeneralUI(X2SaveGameHeader saveHeader, HistoryFile historyFile) { } } - var history = historyFile.getHistory(); - headers.add(new HeaderPair("Current random seed", Integer.toString(history.CurrRandomSeed))); - headers.add(new HeaderPair("# of archived frames", Integer.toString(history.NumArchivedFrames))); + headers.add(new HeaderPair("Current random seed", Integer.toString(historyFile.getRandomSeed()))); + headers.add(new HeaderPair("# of archived frames", Integer.toString(historyFile.getNumArchivedFrames()))); // headers table @@ -90,7 +88,7 @@ public HistoryGeneralUI(X2SaveGameHeader saveHeader, HistoryFile historyFile) { GeneralPreferences.getEffective().getHistorySingletonPropsTreeExpanded(), "Click a singleton state to view its properties"); singletonPropsUI.getRootProperty().bind( - singletonsTable.getSelectionModel().selectedItemProperty().map(f -> NonVersionedField.convertToTreeItems(f.getFields()))); + singletonsTable.getSelectionModel().selectedItemProperty().map(f -> f.getTree())); // top-level layout diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryObjectsTable.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryObjectsTable.java index 4fc546a..cb354da 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryObjectsTable.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/HistoryObjectsTable.java @@ -117,7 +117,7 @@ public HistoryObjectsTable(HistoryFile history, TableView framesTa var selectedFrame = framesTable.getSelectionModel().selectedItemProperty().get(); return selectedFrame == null ? null : FXCollections.observableList( selectedFrame - .getObjects() + .getObjectsCombined() .values() .stream() .filter(filters.getProperty().get()) diff --git a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/ObjectPropertiesTable.java b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/ObjectPropertiesTable.java index e8a47b1..7f9e6f7 100644 --- a/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/ObjectPropertiesTable.java +++ b/x2-data-explorer/src/main/java/com/github/rcd47/x2data/explorer/jfx/ui/history/ObjectPropertiesTable.java @@ -3,11 +3,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.EnumMap; +import java.util.function.ToIntFunction; import com.github.rcd47.x2data.explorer.file.GameStateObject; -import com.github.rcd47.x2data.explorer.file.GameStateObjectField; -import com.github.rcd47.x2data.explorer.file.GameStateObjectFieldTreeNode; import com.github.rcd47.x2data.explorer.file.HistoryFrame; +import com.github.rcd47.x2data.explorer.file.data.PrimitiveInterner; +import com.github.rcd47.x2data.explorer.file.data.X2VersionedDatumTreeItem; import com.github.rcd47.x2data.explorer.jfx.ui.MultiSelectMenu; import com.github.rcd47.x2data.explorer.jfx.ui.StandardCellFactoryHelper; import com.github.rcd47.x2data.explorer.jfx.ui.TreeTableUtils; @@ -36,66 +37,66 @@ public class ObjectPropertiesTable { private final TableView framesTable; private final TableView objectsTable; - private final TreeTableView table; + private final TreeTableView table; private final VBox vbox; - public ObjectPropertiesTable(TableView framesTable, TableView objectsTable) { + public ObjectPropertiesTable(TableView framesTable, TableView objectsTable, PrimitiveInterner interner) { this.framesTable = framesTable; this.objectsTable = objectsTable; // early initialization - table = new TreeTableView(); + table = new TreeTableView<>(); // columns - var columns = new EnumMap>(ObjectPropertiesColumn.class); + var columns = new EnumMap>(ObjectPropertiesColumn.class); - var colName = new TreeTableColumn( + var colName = new TreeTableColumn( ObjectPropertiesColumn.NAME.getHeaderText()); colName.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue())); colName.setCellFactory(_ -> new ObjectPropertyNameColumnCell()); colName.setUserData(ObjectPropertiesColumn.NAME); columns.put(ObjectPropertiesColumn.NAME, colName); - var colPrevValue = new TreeTableColumn( + var colPrevValue = new TreeTableColumn( ObjectPropertiesColumn.PREV_VALUE.getHeaderText()); colPrevValue.setCellValueFactory(f -> { var field = f.getValue().getValue().getPreviousValue(); - return field == null ? null : new ReadOnlyObjectWrapper<>(field.getValue()); + return field == null ? null : new ReadOnlyObjectWrapper<>(field); }); colPrevValue.setUserData(ObjectPropertiesColumn.PREV_VALUE); StandardCellFactoryHelper.setFactoryForObjectValueColumn(colPrevValue); columns.put(ObjectPropertiesColumn.PREV_VALUE, colPrevValue); - var colCurrentValue = new TreeTableColumn( + var colCurrentValue = new TreeTableColumn( ObjectPropertiesColumn.CURRENT_VALUE.getHeaderText()); colCurrentValue.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue().getValue())); colCurrentValue.setUserData(ObjectPropertiesColumn.CURRENT_VALUE); StandardCellFactoryHelper.setFactoryForObjectValueColumn(colCurrentValue); columns.put(ObjectPropertiesColumn.CURRENT_VALUE, colCurrentValue); - var colNextValue = new TreeTableColumn( + var colNextValue = new TreeTableColumn( ObjectPropertiesColumn.NEXT_VALUE.getHeaderText()); colNextValue.setCellValueFactory(f -> { var field = f.getValue().getValue().getNextValue(); - return field == null ? null : new ReadOnlyObjectWrapper<>(field.getValue()); + return field == null ? null : new ReadOnlyObjectWrapper<>(field); }); colNextValue.setUserData(ObjectPropertiesColumn.NEXT_VALUE); StandardCellFactoryHelper.setFactoryForObjectValueColumn(colNextValue); columns.put(ObjectPropertiesColumn.NEXT_VALUE, colNextValue); - var colPrevFrame = new TreeTableColumn( + var colPrevFrame = new TreeTableColumn( ObjectPropertiesColumn.PREV_FRAME.getHeaderText()); - colPrevFrame.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue().getPreviousValue())); - colPrevFrame.setCellFactory(_ -> new ObjectPropertyFrameLinkColumnCell()); + colPrevFrame.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue())); + colPrevFrame.setCellFactory(_ -> new ObjectPropertyFrameLinkColumnCell(Integer.MIN_VALUE, n -> n.getPreviousFrame())); colPrevFrame.setUserData(ObjectPropertiesColumn.PREV_FRAME); columns.put(ObjectPropertiesColumn.PREV_FRAME, colPrevFrame); - var colNextFrame = new TreeTableColumn( + var colNextFrame = new TreeTableColumn( ObjectPropertiesColumn.NEXT_FRAME.getHeaderText()); - colNextFrame.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue().getNextValue())); - colNextFrame.setCellFactory(_ -> new ObjectPropertyFrameLinkColumnCell()); + colNextFrame.setCellValueFactory(f -> new ReadOnlyObjectWrapper<>(f.getValue().getValue())); + colNextFrame.setCellFactory(_ -> new ObjectPropertyFrameLinkColumnCell(Integer.MAX_VALUE, n -> n.getNextFrame())); colNextFrame.setUserData(ObjectPropertiesColumn.NEXT_FRAME); columns.put(ObjectPropertiesColumn.NEXT_FRAME, colNextFrame); @@ -134,7 +135,9 @@ public ObjectPropertiesTable(TableView framesTable, TableView { var object = objectsTable.getSelectionModel().getSelectedItem(); - return object == null ? null : object.getFieldsAsTreeNode(modifiedCheckbox.isSelected()); + return object == null ? + null : + object.getFields().getTreeNodeAt(interner, null, object.getFrame().getNumber(), modifiedCheckbox.isSelected()); }, modifiedCheckbox.selectedProperty(), objectsTable.getSelectionModel().selectedItemProperty())); @@ -150,13 +153,13 @@ public Node getNode() { return vbox; } - private class ObjectPropertyNameColumnCell extends TreeTableCell { + private class ObjectPropertyNameColumnCell extends TreeTableCell { public ObjectPropertyNameColumnCell() { StandardCellFactoryHelper.configureCellForValueColumn(this); } @Override - protected void updateItem(GameStateObjectFieldTreeNode item, boolean empty) { + protected void updateItem(X2VersionedDatumTreeItem item, boolean empty) { super.updateItem(item, empty); if (empty || item == null) { setText(null); @@ -175,47 +178,57 @@ protected void updateItem(GameStateObjectFieldTreeNode item, boolean empty) { } } - private class ObjectPropertyFrameLinkColumnCell extends TreeTableCell { + private class ObjectPropertyFrameLinkColumnCell extends TreeTableCell { + private final int nullValue; + private final ToIntFunction extractor; + + public ObjectPropertyFrameLinkColumnCell(int nullValue, ToIntFunction extractor) { + this.nullValue = nullValue; + this.extractor = extractor; + } + @Override - protected void updateItem(GameStateObjectField item, boolean empty) { + protected void updateItem(X2VersionedDatumTreeItem item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { + int frameNum = empty || item == null ? nullValue : extractor.applyAsInt(item); + if (frameNum == nullValue) { if (framesTable == null) { setText(null); } else { setGraphic(null); } } else { - var state = item.getLastChangedAt(); - var frame = state.getFrame(); - var frameNum = Integer.toString(frame.getNumber()); + var frameStr = Integer.toString(frameNum); if (framesTable == null) { - setText(frameNum); + setText(frameStr); } else { - var link = new Hyperlink(frameNum); + var link = new Hyperlink(frameStr); link.setOnAction(_ -> { var path = new ArrayList(); var treeItem = getTableRow().getTreeItem(); while (true) { path.add(treeItem.getValue().getName()); treeItem = treeItem.getParent(); - if (treeItem.getValue() == null) { + if (treeItem.getValue().getName() == null) { // reached root node break; } } Collections.reverse(path); - framesTable.getSelectionModel().select(frame); + var objectId = objectsTable.getSelectionModel().getSelectedItem().getObjectId(); + + framesTable.getSelectionModel().select(frameNum - framesTable.getItems().getFirst().getNumber()); framesTable.scrollTo(framesTable.getSelectionModel().getSelectedIndex()); - objectsTable.getSelectionModel().select(state); + + var frame = framesTable.getSelectionModel().getSelectedItem(); + objectsTable.getSelectionModel().select(frame.getObject(objectId)); objectsTable.scrollTo(objectsTable.getSelectionModel().getSelectedIndex()); var newTreeItem = table.getRoot(); for (var element : path) { newTreeItem.setExpanded(true); newTreeItem = newTreeItem.getChildren().stream().filter(i -> i.getValue().getName().equals(element)).findAny().get(); - } table.getSelectionModel().select(newTreeItem); table.scrollTo(table.getSelectionModel().getSelectedIndex()); diff --git a/x2-data-explorer/src/main/resources/defaultContextSummary.groovy b/x2-data-explorer/src/main/resources/defaultContextSummary.groovy index 73e6c59..2ff71f8 100644 --- a/x2-data-explorer/src/main/resources/defaultContextSummary.groovy +++ b/x2-data-explorer/src/main/resources/defaultContextSummary.groovy @@ -3,32 +3,32 @@ // XComGameStateContext_ChangeContainer if (ctx.ChangeInfo) { - return ctx.ChangeInfo.value + return ctx.ChangeInfo } if (ctx.InputContext) { // XComGameStateContext_Ability if (ctx.ResultContext) { - return ctx.InputContext.AbilityTemplateName.value.original + return ctx.InputContext.AbilityTemplateName.original } // XComGameStateContext_HeadquartersOrder if (ctx.InputContext.OrderType) { - return ctx.InputContext.OrderType.value.original + return ctx.InputContext.OrderType.original } } // XComGameStateContext_Kismet if (ctx.SeqOpName) { - return ctx.SeqOpName.value.original + return ctx.SeqOpName.original } // XComGameStateContext_WillRoll if (ctx.RollSourceFriendly) { - return ctx.TargetUnitID.value + ' ' + ctx.RollSourceFriendly.value + return ctx.TargetUnitID + ' ' + ctx.RollSourceFriendly } // XComGameStateContext_StrategyGameRule and XComGameStateContext_TacticalGameRule if (ctx.GameRuleType) { - return ctx.GameRuleType.value.original + return ctx.GameRuleType.original } diff --git a/x2-data-explorer/src/main/resources/defaultStateObjSummary.groovy b/x2-data-explorer/src/main/resources/defaultStateObjSummary.groovy index 9a2c8b3..f5a6d79 100644 --- a/x2-data-explorer/src/main/resources/defaultStateObjSummary.groovy +++ b/x2-data-explorer/src/main/resources/defaultStateObjSummary.groovy @@ -3,36 +3,36 @@ // XComGameState_Ability, XComGameState_Item, and their subclasses if (gso.m_TemplateName && gso.OwnerStateObject) { - return gso.m_TemplateName.value.original + ' for ' + gso.OwnerStateObject.ObjectID.value + return gso.m_TemplateName.original + ' for ' + gso.OwnerStateObject.ObjectID } // XComGameState_Effect and subclasses if (gso.ApplyEffectParameters) { def source; if (gso.ApplyEffectParameters.EffectRef.SourceTemplateName) { - source = gso.ApplyEffectParameters.EffectRef.SourceTemplateName.value.original + source = gso.ApplyEffectParameters.EffectRef.SourceTemplateName.original } else { - source = gso.ApplyEffectParameters.EffectRef.LookupType.value.original + source = gso.ApplyEffectParameters.EffectRef.LookupType.original } if (gso.ApplyEffectParameters.SourceStateObjectRef) { // effects applied by XCGSC_UpdateWorldEffects do not have a source - source += ' by ' + gso.ApplyEffectParameters.SourceStateObjectRef.ObjectID.value + source += ' by ' + gso.ApplyEffectParameters.SourceStateObjectRef.ObjectID } - return source + ' on ' + gso.ApplyEffectParameters.TargetStateObjectRef.ObjectID.value + return source + ' on ' + gso.ApplyEffectParameters.TargetStateObjectRef.ObjectID } // XComGameState_Unit if (gso.strFirstName || gso.strLastName) { - return gso.strFirstName?.value + ' ' + gso.strLastName?.value + return gso.strFirstName + ' ' + gso.strLastName } else if (gso.m_SoldierClassTemplateName) { - return gso.m_SoldierClassTemplateName.value.original + return gso.m_SoldierClassTemplateName.original } // XComGameState_Player if (gso.TeamFlag) { - return gso.TeamFlag.value.original + return gso.TeamFlag.original } // fallback. lots of the stock objects have m_TemplateName. if (gso.m_TemplateName) { - return gso.m_TemplateName.value.original + return gso.m_TemplateName.original } diff --git a/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitorTest.java b/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitorTest.java new file mode 100644 index 0000000..22c142f --- /dev/null +++ b/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/VersionedObjectVisitorTest.java @@ -0,0 +1,1159 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +import javafx.scene.control.TreeItem; + +public class VersionedObjectVisitorTest { + + @Test + public void testStaticArrayWithFirstIndexZero() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitIntValue(11); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitIntValue(33); + visitor.visitProperty(new UnrealName("a"), 1); + visitor.visitIntValue(44); + visitor.visitProperty(new UnrealName("a"), 4); + visitor.visitIntValue(55); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(11, null, 22))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(33, 44, 22, null, 55))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 11, FieldChangeType.ADDED, null, 33, Integer.MIN_VALUE, 2)); + var treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 4, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + var treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 44, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 22, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeA4 = findTreeItem(treeA, 0, "4"); + assertThat(treeA4.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("4"), 55, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 3, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 44, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA4 = findTreeItem(treeA, 0, "4"); + assertThat(treeA4.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("4"), 55, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + } + + @Test + public void testStaticArrayWithFirstIndexNonZero() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitIntValue(11); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, null, 11))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(22, null, 11))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 11, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + var treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 11, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + } + + @Test + public void testStaticArrayIsNotChangedButOtherStructMembersAreChanged() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitIntValue(11); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitIntValue(11); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitIntValue(33); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, null, 11), "b", 22)); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, null, 11), "b", 33)); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(2); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 11, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeB = findTreeItem(treeRoot, 0, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), 22, FieldChangeType.ADDED, null, 33, Integer.MIN_VALUE, 2)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(2); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 11, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeB = findTreeItem(treeRoot, 0, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), 33, FieldChangeType.CHANGED, 22, null, 1, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeB = findTreeItem(treeRoot, 0, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), 33, FieldChangeType.CHANGED, 22, null, 1, Integer.MAX_VALUE)); + } + + @Test + public void testStaticArrayWithNestedStruct() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(11); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(33); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(3, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 2); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(55); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, null, Map.of("aa", 11)))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, Map.of("aa", 33), Map.of("aa", 11)))); + assertThat(vmap.getValueAt(3)).usingRecursiveComparison().isEqualTo(Map.of("a", Arrays.asList(null, Map.of("aa", 33), Map.of("aa", 55)))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA2 = findTreeItem(treeA, 1, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 3)); + var treeA2AA = findTreeItem(treeA2, 0, "aa"); + assertThat(treeA2AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 11, FieldChangeType.ADDED, null, 55, Integer.MIN_VALUE, 3)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + var treeA1 = findTreeItem(treeA, 1, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 1, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 3)); + treeA2AA = findTreeItem(treeA2, 0, "aa"); + assertThat(treeA2AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 11, FieldChangeType.NONE, null, 55, Integer.MIN_VALUE, 3)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1 = findTreeItem(treeA, 1, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 3 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 1, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 1, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA2AA = findTreeItem(treeA2, 0, "aa"); + assertThat(treeA2AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + + // tree node at frame 3 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 1, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA2AA = findTreeItem(treeA2, 0, "aa"); + assertThat(treeA2AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + } + + @Test + public void testNestedStructs() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aaa"), 0); + visitor.visitStringValue("mmm"); + visitor.visitStructEnd(); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitStringValue("ppp"); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("ba"), 0); + visitor.visitStringValue("zzz"); + visitor.visitProperty(new UnrealName("bb"), 0); + visitor.visitStringValue("vvv"); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aaa"), 0); + visitor.visitStringValue("xxx"); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("bb"), 0); + visitor.visitStringValue("uuu"); + visitor.visitStructEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo( + Map.of("a", Map.of("aa", Map.of("aaa", "mmm"), "ab", Map.of("aba", "ppp")), "b", Map.of("ba", "zzz", "bb", "vvv"))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo( + Map.of("a", Map.of("aa", Map.of("aaa", "xxx"), "ab", Map.of("aba", "ppp")), "b", Map.of("ba", "zzz", "bb", "uuu"))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(2); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeAA = findTreeItem(treeA, 1, "aa"); + assertThat(treeAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeAAA = findTreeItem(treeAA, 0, "aaa"); + assertThat(treeAAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aaa"), "mmm", FieldChangeType.ADDED, null, "xxx", Integer.MIN_VALUE, 2)); + var treeAB = findTreeItem(treeA, 1, "ab"); + assertThat(treeAB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeABA = findTreeItem(treeAB, 0, "aba"); + assertThat(treeABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), "ppp", FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeB = findTreeItem(treeRoot, 2, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeBA = findTreeItem(treeB, 0, "ba"); + assertThat(treeBA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ba"), "zzz", FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + var treeBB = findTreeItem(treeB, 0, "bb"); + assertThat(treeBB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("bb"), "vvv", FieldChangeType.ADDED, null, "uuu", Integer.MIN_VALUE, 2)); + + // tree node at frame 2 when onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAA = findTreeItem(treeA, 1, "aa"); + assertThat(treeAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAAA = findTreeItem(treeAA, 0, "aaa"); + assertThat(treeAAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aaa"), "xxx", FieldChangeType.CHANGED, "mmm", null, 1, Integer.MAX_VALUE)); + treeAB = findTreeItem(treeA, 1, "ab"); + assertThat(treeAB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeABA = findTreeItem(treeAB, 0, "aba"); + assertThat(treeABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), "ppp", FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeB = findTreeItem(treeRoot, 2, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeBA = findTreeItem(treeB, 0, "ba"); + assertThat(treeBA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ba"), "zzz", FieldChangeType.NONE, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + treeBB = findTreeItem(treeB, 0, "bb"); + assertThat(treeBB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("bb"), "uuu", FieldChangeType.CHANGED, "vvv", null, 1, Integer.MAX_VALUE)); + + // tree node at frame 2 when onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAA = findTreeItem(treeA, 1, "aa"); + assertThat(treeAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAAA = findTreeItem(treeAA, 0, "aaa"); + assertThat(treeAAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aaa"), "xxx", FieldChangeType.CHANGED, "mmm", null, 1, Integer.MAX_VALUE)); + treeB = findTreeItem(treeRoot, 1, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeBB = findTreeItem(treeB, 0, "bb"); + assertThat(treeBB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("bb"), "uuu", FieldChangeType.CHANGED, "vvv", null, 1, Integer.MAX_VALUE)); + } + + @Test + public void testStructsInsideMap() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitMapStart(1); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aaa"), 0); + visitor.visitStringValue("mmm"); + visitor.visitStructEnd(); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitStringValue("ppp"); + visitor.visitStructEnd(); + visitor.visitMapEnd(); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitMapStart(1); + visitor.visitProperty(new UnrealName("ba"), 0); + visitor.visitStringValue("zzz"); + visitor.visitProperty(new UnrealName("bb"), 0); + visitor.visitStringValue("vvv"); + visitor.visitMapEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitMapStart(1); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aaa"), 0); + visitor.visitStringValue("xxx"); + visitor.visitStructEnd(); + visitor.visitMapEnd(); + visitor.visitProperty(new UnrealName("b"), 0); + visitor.visitMapStart(1); + visitor.visitProperty(new UnrealName("bb"), 0); + visitor.visitStringValue("uuu"); + visitor.visitMapEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo( + Map.of("a", Map.of("aa", Map.of("aaa", "mmm"), "ab", Map.of("aba", "ppp")), "b", Map.of("ba", "zzz", "bb", "vvv"))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo( + Map.of("a", Map.of("aa", Map.of("aaa", "xxx")), "b", Map.of("bb", "uuu"))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(2); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeAA = findTreeItem(treeA, 1, "aa"); + assertThat(treeAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeAAA = findTreeItem(treeAA, 0, "aaa"); + assertThat(treeAAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aaa"), "mmm", FieldChangeType.ADDED, null, "xxx", Integer.MIN_VALUE, 2)); + var treeAB = findTreeItem(treeA, 1, "ab"); + assertThat(treeAB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeABA = findTreeItem(treeAB, 0, "aba"); + assertThat(treeABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), "ppp", FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeB = findTreeItem(treeRoot, 2, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeBA = findTreeItem(treeB, 0, "ba"); + assertThat(treeBA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ba"), "zzz", FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeBB = findTreeItem(treeB, 0, "bb"); + assertThat(treeBB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("bb"), "vvv", FieldChangeType.ADDED, null, "uuu", Integer.MIN_VALUE, 2)); + + // tree node at frame 2 + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAA = findTreeItem(treeA, 1, "aa"); + assertThat(treeAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeAAA = findTreeItem(treeAA, 0, "aaa"); + assertThat(treeAAA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aaa"), "xxx", FieldChangeType.CHANGED, "mmm", null, 1, Integer.MAX_VALUE)); + treeAB = findTreeItem(treeA, 1, "ab"); + assertThat(treeAB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.REMOVED, null, null, 1, Integer.MAX_VALUE)); + treeABA = findTreeItem(treeAB, 0, "aba"); + assertThat(treeABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), null, FieldChangeType.REMOVED, "ppp", null, 1, Integer.MAX_VALUE)); + treeB = findTreeItem(treeRoot, 2, "b"); + assertThat(treeB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("b"), null, FieldChangeType.CHANGED, null, null, 1, Integer.MAX_VALUE)); + treeBA = findTreeItem(treeB, 0, "ba"); + assertThat(treeBA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ba"), null, FieldChangeType.REMOVED, "zzz", null, 1, Integer.MAX_VALUE)); + treeBB = findTreeItem(treeB, 0, "bb"); + assertThat(treeBB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("bb"), "uuu", FieldChangeType.CHANGED, "vvv", null, 1, Integer.MAX_VALUE)); + } + + @Test + public void testDynamicArrayWithPrimitives() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(2); + visitor.visitIntValue(11); + visitor.visitIntValue(22); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitIntValue(33); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(3, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitIntValue(33); + visitor.visitIntValue(44); + visitor.visitIntValue(55); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(11, 22))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(33))); + assertThat(vmap.getValueAt(3)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(33, 44, 55))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 11, FieldChangeType.ADDED, null, 33, Integer.MIN_VALUE, 2)); + var treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + + // tree node at frame 2 + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.REMOVED, 22, 44, 1, 3)); + + // tree node at frame 3 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 3, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.NONE, 11, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 44, FieldChangeType.ADDED, null, null, 2, Integer.MAX_VALUE)); + var treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 55, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + + // tree node at frame 3 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 44, FieldChangeType.ADDED, null, null, 2, Integer.MAX_VALUE)); + treeA2 = findTreeItem(treeA, 0, "2"); + assertThat(treeA2.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("2"), 55, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, Integer.MAX_VALUE)); + } + + @Test + public void testDynamicArrayWithStructs() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + // frame 1: two elements created + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(2); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(11); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(33); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(44); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + // frame 2: both elements changed. first element has a property different, second element has a nested array different. + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(2); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(55); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(33); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(66); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + // frame 3: first element is unchanged, second element is removed + visitor.setRootObject(3, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(55); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(22); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + // frame 4: first element has struct array removed, second element is re-added + visitor.setRootObject(4, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(2); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(55); + visitor.visitStructEnd(); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aa"), 0); + visitor.visitIntValue(33); + visitor.visitProperty(new UnrealName("ab"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("aba"), 0); + visitor.visitIntValue(66); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of( + Map.of("aa", 11, "ab", List.of(Map.of("aba", 22))), + Map.of("aa", 33, "ab", List.of(Map.of("aba", 44)))))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of( + Map.of("aa", 55, "ab", List.of(Map.of("aba", 22))), + Map.of("aa", 33, "ab", List.of(Map.of("aba", 66)))))); + assertThat(vmap.getValueAt(3)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of( + Map.of("aa", 55, "ab", List.of(Map.of("aba", 22)))))); + assertThat(vmap.getValueAt(4)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of( + Map.of("aa", 55), + Map.of("aa", 33, "ab", List.of(Map.of("aba", 66)))))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA0 = findTreeItem(treeA, 2, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA0AA = findTreeItem(treeA0, 0, "aa"); + assertThat(treeA0AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 11, FieldChangeType.ADDED, null, 55, Integer.MIN_VALUE, 2)); + var treeA0AB = findTreeItem(treeA0, 1, "ab"); + assertThat(treeA0AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 4)); + var treeA0AB0 = findTreeItem(treeA0AB, 1, "0"); + assertThat(treeA0AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 4)); + var treeA0AB0ABA = findTreeItem(treeA0AB0, 0, "aba"); + assertThat(treeA0AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 4)); + var treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 3)); + var treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 44, FieldChangeType.ADDED, null, 66, Integer.MIN_VALUE, 2)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA0 = findTreeItem(treeA, 2, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 1, 4)); + treeA0AA = findTreeItem(treeA0, 0, "aa"); + assertThat(treeA0AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + treeA0AB = findTreeItem(treeA0, 1, "ab"); + assertThat(treeA0AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA0AB0 = findTreeItem(treeA0AB, 1, "0"); + assertThat(treeA0AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA0AB0ABA = findTreeItem(treeA0AB0, 0, "aba"); + assertThat(treeA0AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 22, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 3)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 66, FieldChangeType.CHANGED, 44, null, 1, 3)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA0 = findTreeItem(treeA, 1, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 1, 4)); + treeA0AA = findTreeItem(treeA0, 0, "aa"); + assertThat(treeA0AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 1, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 66, FieldChangeType.CHANGED, 44, null, 1, 3)); + + // tree node at frame 3 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, 4)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, 4)); + treeA0 = findTreeItem(treeA, 2, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.NONE, null, null, 1, 4)); + treeA0AA = findTreeItem(treeA0, 0, "aa"); + assertThat(treeA0AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.NONE, 11, null, 1, Integer.MAX_VALUE)); + treeA0AB = findTreeItem(treeA0, 1, "ab"); + assertThat(treeA0AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA0AB0 = findTreeItem(treeA0AB, 1, "0"); + assertThat(treeA0AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA0AB0ABA = findTreeItem(treeA0AB0, 0, "aba"); + assertThat(treeA0AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 22, FieldChangeType.NONE, null, null, Integer.MIN_VALUE, 4)); + treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.REMOVED, 33, 33, 1, 4)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), null, FieldChangeType.REMOVED, 66, 66, 2, 4)); + + // tree node at frame 3 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, 4)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, 4)); + treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), null, FieldChangeType.REMOVED, 33, 33, 1, 4)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.REMOVED, null, null, 2, 4)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), null, FieldChangeType.REMOVED, 66, 66, 2, 4)); + + // tree node at frame 4 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 4, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 3, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 3, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 2, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA0AA = findTreeItem(treeA0, 0, "aa"); + assertThat(treeA0AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 55, FieldChangeType.NONE, 11, null, 1, Integer.MAX_VALUE)); + treeA0AB = findTreeItem(treeA0, 1, "ab"); + assertThat(treeA0AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.REMOVED, null, null, 1, Integer.MAX_VALUE)); + treeA0AB0 = findTreeItem(treeA0AB, 1, "0"); + assertThat(treeA0AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.REMOVED, null, null, 1, Integer.MAX_VALUE)); + treeA0AB0ABA = findTreeItem(treeA0AB0, 0, "aba"); + assertThat(treeA0AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), null, FieldChangeType.REMOVED, 22, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 66, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + + // tree node at frame 4 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 4, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 3, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 3, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 1, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA0AB = findTreeItem(treeA0, 1, "ab"); + assertThat(treeA0AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.REMOVED, null, null, 1, Integer.MAX_VALUE)); + treeA0AB0 = findTreeItem(treeA0AB, 1, "0"); + assertThat(treeA0AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.REMOVED, null, null, 1, Integer.MAX_VALUE)); + treeA0AB0ABA = findTreeItem(treeA0AB0, 0, "aba"); + assertThat(treeA0AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), null, FieldChangeType.REMOVED, 22, null, 1, Integer.MAX_VALUE)); + treeA1 = findTreeItem(treeA, 2, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AA = findTreeItem(treeA1, 0, "aa"); + assertThat(treeA1AA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aa"), 33, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB = findTreeItem(treeA1, 1, "ab"); + assertThat(treeA1AB.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("ab"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB0 = findTreeItem(treeA1AB, 1, "0"); + assertThat(treeA1AB0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), null, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + treeA1AB0ABA = findTreeItem(treeA1AB0, 0, "aba"); + assertThat(treeA1AB0ABA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("aba"), 66, FieldChangeType.ADDED, null, null, 3, Integer.MAX_VALUE)); + } + + @Test + public void testDynamicArrayElementRemovedForMultipleFrames() { + var vmap = new X2VersionedMap(1); + var visitor = new VersionedObjectVisitor(new PrimitiveInterner()); + + visitor.setRootObject(1, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(2); + visitor.visitIntValue(11); + visitor.visitIntValue(22); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(2, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitIntValue(11); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + visitor.setRootObject(3, vmap); + visitor.visitStructStart(null); + visitor.visitProperty(new UnrealName("a"), 0); + visitor.visitDynamicArrayStart(1); + visitor.visitIntValue(33); + visitor.visitDynamicArrayEnd(); + visitor.visitStructEnd(); + + assertThat(vmap.getValueAt(1)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(11, 22))); + assertThat(vmap.getValueAt(2)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(11))); + assertThat(vmap.getValueAt(3)).usingRecursiveComparison().isEqualTo(Map.of("a", List.of(33))); + + // tree node at frame 1 + var treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 1, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + var treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 11, FieldChangeType.ADDED, null, 33, Integer.MIN_VALUE, 3)); + var treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), 22, FieldChangeType.ADDED, null, null, Integer.MIN_VALUE, 2)); + + // tree node at frame 2 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 2, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 11, FieldChangeType.NONE, null, 33, Integer.MIN_VALUE, 3)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.REMOVED, 22, null, 1, Integer.MAX_VALUE)); + + // tree node at frame 2 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 2, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 1, 3)); + treeA1 = findTreeItem(treeA, 0, "1"); + assertThat(treeA1.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("1"), null, FieldChangeType.REMOVED, 22, null, 1, Integer.MAX_VALUE)); + + // tree node at frame 3 with onlyModified = false + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, false); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + + // tree node at frame 3 with onlyModified = true + treeRoot = vmap.getTreeNodeAt(new PrimitiveInterner(), new UnrealName("test"), 3, true); + assertThat(treeRoot.getChildren()).hasSize(1); + assertThat(treeRoot.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("test"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA = findTreeItem(treeRoot, 1, "a"); + assertThat(treeA.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("a"), null, FieldChangeType.CHANGED, null, null, 2, Integer.MAX_VALUE)); + treeA0 = findTreeItem(treeA, 0, "0"); + assertThat(treeA0.getValue()).usingRecursiveComparison().isEqualTo(new X2VersionedDatumTreeItem( + new UnrealName("0"), 33, FieldChangeType.CHANGED, 11, null, 1, Integer.MAX_VALUE)); + } + + private TreeItem findTreeItem(TreeItem node, int expectedChildren, String key) { + var child = node.getChildren().stream().filter(c -> c.getValue().getName().getNormalized().equals(key)).findAny().orElse(null); + assertThat(child).isNotNull(); + assertThat(child.getChildren()).hasSize(expectedChildren); + return child; + } + +} diff --git a/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategyTest.java b/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategyTest.java new file mode 100644 index 0000000..1c543e0 --- /dev/null +++ b/x2-data-explorer/src/test/java/com/github/rcd47/x2data/explorer/file/data/X2VersionedMapChildrenStrategyTest.java @@ -0,0 +1,24 @@ +package com.github.rcd47.x2data.explorer.file.data; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import com.github.rcd47.x2data.lib.unreal.mappings.UnrealName; + +import it.unimi.dsi.fastutil.objects.Object2ReferenceOpenCustomHashMap; + +public class X2VersionedMapChildrenStrategyTest { + + @Test + public void testNameStringEquivalence() { + var map = new Object2ReferenceOpenCustomHashMap<>(new X2VersionedMapChildrenStrategy()); + + map.put(new UnrealName("foo"), 1); + map.put("bar", 2); + + assertThat(map).containsEntry("FOO", 1); + assertThat(map).containsEntry(new UnrealName("BAR"), 2); + } + +} diff --git a/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/history/X2HistoryIndex.java b/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/history/X2HistoryIndex.java index c47d62d..5848290 100644 --- a/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/history/X2HistoryIndex.java +++ b/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/history/X2HistoryIndex.java @@ -5,10 +5,10 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.channels.FileChannel; -import java.util.ArrayDeque; -import java.util.Deque; import java.util.List; import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; import com.github.rcd47.x2data.lib.unreal.IUnrealObjectVisitor; import com.github.rcd47.x2data.lib.unreal.UnrealFileParseException; @@ -27,7 +27,7 @@ public class X2HistoryIndex implements Closeable { private UnrealObjectParser objectParser; private UnrealObjectMapper objectMapper; private int largestEntrySize; - private Deque bufferCache; + private BlockingQueue bufferCache; X2HistoryIndex(FileChannel file, boolean createdByWOTC, List entries, Map typings) { this.file = file; @@ -37,7 +37,7 @@ public class X2HistoryIndex implements Closeable { objectParser = new UnrealObjectParser(true, typings); objectMapper = new UnrealObjectMapper(objectParser); largestEntrySize = entries.stream().mapToInt(X2HistoryIndexEntry::getLength).max().getAsInt(); - bufferCache = new ArrayDeque<>(); + bufferCache = new ArrayBlockingQueue<>(Runtime.getRuntime().availableProcessors()); } public X2HistoryIndexEntry getEntry(int index) { @@ -54,7 +54,7 @@ public T mapObject(X2HistoryIndexEntry entry, T previousVersion, IXComObject } catch (Exception e) { throw buildParseException(entry, e); } finally { - bufferCache.offerFirst(buffer); + bufferCache.offer(buffer); } } @@ -65,7 +65,7 @@ public void parseObject(X2HistoryIndexEntry entry, IUnrealObjectVisitor visitor) } catch (Exception e) { throw buildParseException(entry, e); } finally { - bufferCache.offerFirst(buffer); + bufferCache.offer(buffer); } } @@ -81,14 +81,13 @@ private ByteBuffer prepareBuffer(X2HistoryIndexEntry entry) throws IOException { throw new IllegalArgumentException("Entry does not belong to this index"); } - var buffer = bufferCache.pollFirst(); + var buffer = bufferCache.poll(); if (buffer == null) { buffer = ByteBuffer.allocate(largestEntrySize).order(ByteOrder.LITTLE_ENDIAN); } buffer.position(0).limit(entry.getLength()); - file.position(entry.getPosition()); - file.read(buffer); + file.read(buffer, entry.getPosition()); return buffer.flip(); } diff --git a/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/unreal/mappings/UnrealName.java b/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/unreal/mappings/UnrealName.java index 5bb47ec..694e847 100644 --- a/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/unreal/mappings/UnrealName.java +++ b/x2-data-lib/src/main/java/com/github/rcd47/x2data/lib/unreal/mappings/UnrealName.java @@ -30,32 +30,12 @@ public int compareTo(UnrealName o) { @Override public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((normalized == null) ? 0 : normalized.hashCode()); - return result; + return normalized.hashCode(); } @Override public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (getClass() != obj.getClass()) { - return false; - } - UnrealName other = (UnrealName) obj; - if (normalized == null) { - if (other.normalized != null) { - return false; - } - } else if (!normalized.equals(other.normalized)) { - return false; - } - return true; + return obj == this || (obj instanceof UnrealName other && normalized.equals(other.normalized)); } @Override diff --git a/x2-data-parent/pom.xml b/x2-data-parent/pom.xml index d86e8ff..10d20a5 100644 --- a/x2-data-parent/pom.xml +++ b/x2-data-parent/pom.xml @@ -69,11 +69,26 @@ commons-codec 1.18.0 + + 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} -