From 0d4f83f37496d14704a8821edbfa235f06b86044 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:33:37 +0000 Subject: [PATCH 1/6] Initial plan From 9763d607f693884fc56b36cebdef94c043332b8d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:36:53 +0000 Subject: [PATCH 2/6] Enable testLists in JmeExporterTest to reproduce the issue Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java b/jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java index d11df242cc..beede5de25 100644 --- a/jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java +++ b/jme3-plugins/src/test/java/com/jme3/export/JmeExporterTest.java @@ -139,7 +139,7 @@ public void testSaveWithNullParent() throws IOException { public void testExporterConsistency() { // final boolean testXML = true; - final boolean testLists = false; + final boolean testLists = true; final boolean testMaps = true; final boolean printXML = false; From 6f3ae379df024f60c8efb854a470c6a68d2f1723 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:43:32 +0000 Subject: [PATCH 3/6] Fix BinaryExporter issue with multiple UserData objects containing lists Use unique field name prefixes based on identity hash code to prevent field name collisions when multiple UserData objects with lists are serialized in the same binary file. This fixes the ArrayIndexOutOfBoundsException that occurred during deserialization. Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../main/java/com/jme3/scene/UserData.java | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/UserData.java b/jme3-core/src/main/java/com/jme3/scene/UserData.java index 2fb04f44d3..bea23fec5b 100644 --- a/jme3-core/src/main/java/com/jme3/scene/UserData.java +++ b/jme3-core/src/main/java/com/jme3/scene/UserData.java @@ -139,6 +139,10 @@ public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); oc.write(type, "type", (byte) 0); + // Use a unique prefix for list/map/array field names to avoid collisions + // when multiple UserData objects are serialized in the same binary file + String uniquePrefix = String.valueOf(System.identityHashCode(this)); + switch (type) { case TYPE_INTEGER: int i = (Integer) value; @@ -165,15 +169,15 @@ public void write(JmeExporter ex) throws IOException { oc.write(sav, "savableVal", null); break; case TYPE_LIST: - this.writeList(oc, (List) value, "0"); + this.writeList(oc, (List) value, uniquePrefix + ":0"); break; case TYPE_MAP: Map map = (Map) value; - this.writeList(oc, map.keySet(), "0"); - this.writeList(oc, map.values(), "1"); + this.writeList(oc, map.keySet(), uniquePrefix + ":0"); + this.writeList(oc, map.values(), uniquePrefix + ":1"); break; case TYPE_ARRAY: - this.writeList(oc, Arrays.asList((Object[]) value), "0"); + this.writeList(oc, Arrays.asList((Object[]) value), uniquePrefix + ":0"); break; case TYPE_DOUBLE: Double d = (Double) value; @@ -196,6 +200,11 @@ public void write(JmeExporter ex) throws IOException { public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); type = ic.readByte("type", (byte) 0); + + // Use a unique prefix for list/map/array field names to avoid collisions + // when multiple UserData objects are deserialized from the same binary file + String uniquePrefix = String.valueOf(System.identityHashCode(this)); + switch (type) { case TYPE_INTEGER: value = ic.readInt("intVal", 0); @@ -216,19 +225,19 @@ public void read(JmeImporter im) throws IOException { value = ic.readSavable("savableVal", null); break; case TYPE_LIST: - value = this.readList(ic, "0"); + value = this.readList(ic, uniquePrefix + ":0"); break; case TYPE_MAP: Map map = new HashMap<>(); - List keys = this.readList(ic, "0"); - List values = this.readList(ic, "1"); + List keys = this.readList(ic, uniquePrefix + ":0"); + List values = this.readList(ic, uniquePrefix + ":1"); for (int i = 0; i < keys.size(); ++i) { map.put(keys.get(i), values.get(i)); } value = map; break; case TYPE_ARRAY: - value = this.readList(ic, "0").toArray(); + value = this.readList(ic, uniquePrefix + ":0").toArray(); break; case TYPE_DOUBLE: value = ic.readDouble("doubleVal", 0.); From 0889624abb539ec0dd022eab5ba8e851fdf36dfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:45:18 +0000 Subject: [PATCH 4/6] Fix identity hash code issue - use serialized unique ID instead Replace System.identityHashCode() with a serialized uniqueId field that persists across write/read cycles. This ensures the same field name prefixes are used during both serialization and deserialization. Also adds backwards compatibility for old files. Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../main/java/com/jme3/scene/UserData.java | 41 ++++++++++++------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/UserData.java b/jme3-core/src/main/java/com/jme3/scene/UserData.java index bea23fec5b..9b668f0efd 100644 --- a/jme3-core/src/main/java/com/jme3/scene/UserData.java +++ b/jme3-core/src/main/java/com/jme3/scene/UserData.java @@ -76,6 +76,10 @@ public final class UserData implements Savable { protected byte type; protected Object value; + + // Unique identifier for this UserData instance to prevent field name collisions + // when multiple UserData objects with lists/maps/arrays are serialized together + private transient String uniqueId; public UserData() { } @@ -139,9 +143,13 @@ public void write(JmeExporter ex) throws IOException { OutputCapsule oc = ex.getCapsule(this); oc.write(type, "type", (byte) 0); - // Use a unique prefix for list/map/array field names to avoid collisions - // when multiple UserData objects are serialized in the same binary file - String uniquePrefix = String.valueOf(System.identityHashCode(this)); + // Generate a unique ID for this instance to prevent field name collisions + // when multiple UserData objects with lists/maps/arrays are serialized together. + // Store it so it can be read back during deserialization. + if (uniqueId == null) { + uniqueId = String.valueOf(System.nanoTime()); + } + oc.write(uniqueId, "uniqueId", null); switch (type) { case TYPE_INTEGER: @@ -169,15 +177,15 @@ public void write(JmeExporter ex) throws IOException { oc.write(sav, "savableVal", null); break; case TYPE_LIST: - this.writeList(oc, (List) value, uniquePrefix + ":0"); + this.writeList(oc, (List) value, uniqueId + ":0"); break; case TYPE_MAP: Map map = (Map) value; - this.writeList(oc, map.keySet(), uniquePrefix + ":0"); - this.writeList(oc, map.values(), uniquePrefix + ":1"); + this.writeList(oc, map.keySet(), uniqueId + ":0"); + this.writeList(oc, map.values(), uniqueId + ":1"); break; case TYPE_ARRAY: - this.writeList(oc, Arrays.asList((Object[]) value), uniquePrefix + ":0"); + this.writeList(oc, Arrays.asList((Object[]) value), uniqueId + ":0"); break; case TYPE_DOUBLE: Double d = (Double) value; @@ -201,9 +209,14 @@ public void read(JmeImporter im) throws IOException { InputCapsule ic = im.getCapsule(this); type = ic.readByte("type", (byte) 0); - // Use a unique prefix for list/map/array field names to avoid collisions - // when multiple UserData objects are deserialized from the same binary file - String uniquePrefix = String.valueOf(System.identityHashCode(this)); + // Read the unique ID that was written during serialization + uniqueId = ic.readString("uniqueId", null); + + // For backwards compatibility with old files that don't have uniqueId, + // use a default prefix + if (uniqueId == null) { + uniqueId = "0"; + } switch (type) { case TYPE_INTEGER: @@ -225,19 +238,19 @@ public void read(JmeImporter im) throws IOException { value = ic.readSavable("savableVal", null); break; case TYPE_LIST: - value = this.readList(ic, uniquePrefix + ":0"); + value = this.readList(ic, uniqueId + ":0"); break; case TYPE_MAP: Map map = new HashMap<>(); - List keys = this.readList(ic, uniquePrefix + ":0"); - List values = this.readList(ic, uniquePrefix + ":1"); + List keys = this.readList(ic, uniqueId + ":0"); + List values = this.readList(ic, uniqueId + ":1"); for (int i = 0; i < keys.size(); ++i) { map.put(keys.get(i), values.get(i)); } value = map; break; case TYPE_ARRAY: - value = this.readList(ic, uniquePrefix + ":0").toArray(); + value = this.readList(ic, uniqueId + ":0").toArray(); break; case TYPE_DOUBLE: value = ic.readDouble("doubleVal", 0.); From f22d5b7853e0942fd101fc997b093cb9d3036590 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:46:28 +0000 Subject: [PATCH 5/6] Address code review feedback - use AtomicLong counter for uniqueness Replace System.nanoTime() with AtomicLong counter to ensure true uniqueness even with concurrent or rapid serialization. The counter starts at 0 and increments, so generated IDs (1, 2, 3...) never collide with the backwards-compatible fallback ("0"). Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- jme3-core/src/main/java/com/jme3/scene/UserData.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/UserData.java b/jme3-core/src/main/java/com/jme3/scene/UserData.java index 9b668f0efd..5dc8fb820a 100644 --- a/jme3-core/src/main/java/com/jme3/scene/UserData.java +++ b/jme3-core/src/main/java/com/jme3/scene/UserData.java @@ -74,6 +74,10 @@ public final class UserData implements Savable { private static final int TYPE_SHORT = 10; private static final int TYPE_BYTE = 11; + // Counter for generating unique IDs to prevent field name collisions + private static final java.util.concurrent.atomic.AtomicLong uniqueIdCounter = + new java.util.concurrent.atomic.AtomicLong(0); + protected byte type; protected Object value; @@ -147,7 +151,7 @@ public void write(JmeExporter ex) throws IOException { // when multiple UserData objects with lists/maps/arrays are serialized together. // Store it so it can be read back during deserialization. if (uniqueId == null) { - uniqueId = String.valueOf(System.nanoTime()); + uniqueId = String.valueOf(uniqueIdCounter.incrementAndGet()); } oc.write(uniqueId, "uniqueId", null); @@ -213,7 +217,7 @@ public void read(JmeImporter im) throws IOException { uniqueId = ic.readString("uniqueId", null); // For backwards compatibility with old files that don't have uniqueId, - // use a default prefix + // use the original prefix. This matches the field names in old files. if (uniqueId == null) { uniqueId = "0"; } From 4c6c7c93ebfc66b3a41a14810786a6c9283ba6dc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 22:49:13 +0000 Subject: [PATCH 6/6] Changes before error encountered Co-authored-by: riccardobl <4943530+riccardobl@users.noreply.github.com> --- .../src/main/java/com/jme3/scene/UserData.java | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/jme3-core/src/main/java/com/jme3/scene/UserData.java b/jme3-core/src/main/java/com/jme3/scene/UserData.java index 5dc8fb820a..a873a6135f 100644 --- a/jme3-core/src/main/java/com/jme3/scene/UserData.java +++ b/jme3-core/src/main/java/com/jme3/scene/UserData.java @@ -77,13 +77,17 @@ public final class UserData implements Savable { // Counter for generating unique IDs to prevent field name collisions private static final java.util.concurrent.atomic.AtomicLong uniqueIdCounter = new java.util.concurrent.atomic.AtomicLong(0); + + // Prefix used for backwards compatibility with files created before the uniqueId field was added + private static final String LEGACY_PREFIX = "0"; protected byte type; protected Object value; // Unique identifier for this UserData instance to prevent field name collisions - // when multiple UserData objects with lists/maps/arrays are serialized together - private transient String uniqueId; + // when multiple UserData objects with lists/maps/arrays are serialized together. + // Marked volatile to ensure visibility across threads during concurrent serialization. + private transient volatile String uniqueId; public UserData() { } @@ -150,8 +154,13 @@ public void write(JmeExporter ex) throws IOException { // Generate a unique ID for this instance to prevent field name collisions // when multiple UserData objects with lists/maps/arrays are serialized together. // Store it so it can be read back during deserialization. + // Use double-checked locking to ensure thread-safety. if (uniqueId == null) { - uniqueId = String.valueOf(uniqueIdCounter.incrementAndGet()); + synchronized (this) { + if (uniqueId == null) { + uniqueId = String.valueOf(uniqueIdCounter.incrementAndGet()); + } + } } oc.write(uniqueId, "uniqueId", null);