diff --git a/src/AcDream.Core/Items/ClientObjectTable.cs b/src/AcDream.Core/Items/ClientObjectTable.cs
index 356fa7b6..5ed850a8 100644
--- a/src/AcDream.Core/Items/ClientObjectTable.cs
+++ b/src/AcDream.Core/Items/ClientObjectTable.cs
@@ -163,6 +163,32 @@ public sealed class ClientObjectTable
return true;
}
+ ///
+ /// Apply a patch, creating the object if it does not
+ /// exist yet. Used for the local player's own properties from PlayerDescription
+ /// (0x0013), which may arrive BEFORE the player's CreateObject — unlike
+ /// , which no-ops on an unknown object. Fires
+ /// ObjectAdded on create, else ObjectUpdated.
+ ///
+ public void UpsertProperties(uint guid, PropertyBundle incoming)
+ {
+ ArgumentNullException.ThrowIfNull(incoming);
+ bool existed = _objects.TryGetValue(guid, out var item);
+ if (!existed || item is null)
+ {
+ item = new ClientObject { ObjectId = guid };
+ _objects[guid] = item;
+ }
+ foreach (var kv in incoming.Ints) item.Properties.Ints[kv.Key] = kv.Value;
+ foreach (var kv in incoming.Int64s) item.Properties.Int64s[kv.Key] = kv.Value;
+ foreach (var kv in incoming.Bools) item.Properties.Bools[kv.Key] = kv.Value;
+ foreach (var kv in incoming.Floats) item.Properties.Floats[kv.Key] = kv.Value;
+ foreach (var kv in incoming.Strings) item.Properties.Strings[kv.Key] = kv.Value;
+ foreach (var kv in incoming.DataIds) item.Properties.DataIds[kv.Key] = kv.Value;
+ foreach (var kv in incoming.InstanceIds) item.Properties.InstanceIds[kv.Key] = kv.Value;
+ if (!existed) ObjectAdded?.Invoke(item); else ObjectUpdated?.Invoke(item);
+ }
+
///
/// Apply a single PropertyInt update (from PublicUpdatePropertyInt 0x02CE) to an
/// object: store it in the bundle and, for known typed ints, mirror to the typed
diff --git a/tests/AcDream.Core.Tests/Items/ClientObjectTableUpdateTests.cs b/tests/AcDream.Core.Tests/Items/ClientObjectTableUpdateTests.cs
new file mode 100644
index 00000000..b5ec1c78
--- /dev/null
+++ b/tests/AcDream.Core.Tests/Items/ClientObjectTableUpdateTests.cs
@@ -0,0 +1,41 @@
+using AcDream.Core.Items;
+using Xunit;
+
+namespace AcDream.Core.Tests.Items;
+
+public sealed class ClientObjectTableUpdateTests
+{
+ [Fact]
+ public void UpsertProperties_unknownObject_createsThenMerges()
+ {
+ var t = new ClientObjectTable();
+ bool added = false;
+ t.ObjectAdded += _ => added = true;
+
+ var bundle = new PropertyBundle();
+ bundle.Ints[5] = 1234; // EncumbranceVal
+
+ t.UpsertProperties(0x50000001u, bundle);
+
+ var o = t.Get(0x50000001u);
+ Assert.NotNull(o);
+ Assert.Equal(1234, o!.Properties.Ints[5]);
+ Assert.True(added);
+ }
+
+ [Fact]
+ public void UpsertProperties_existingObject_mergesAndFiresUpdated()
+ {
+ var t = new ClientObjectTable();
+ t.AddOrUpdate(new ClientObject { ObjectId = 0x50000001u });
+ bool updated = false;
+ t.ObjectUpdated += _ => updated = true;
+
+ var bundle = new PropertyBundle();
+ bundle.Ints[5] = 99;
+ t.UpsertProperties(0x50000001u, bundle);
+
+ Assert.Equal(99, t.Get(0x50000001u)!.Properties.Ints[5]);
+ Assert.True(updated);
+ }
+}