diff --git a/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs b/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs
new file mode 100644
index 00000000..35d466a6
--- /dev/null
+++ b/src/AcDream.Core.Net/Messages/PublicUpdatePropertyInt.cs
@@ -0,0 +1,44 @@
+using System;
+using System.Buffers.Binary;
+
+namespace AcDream.Core.Net.Messages;
+
+///
+/// Inbound PublicUpdatePropertyInt (0x02CE) — the server updates one
+/// PropertyInt on a visible object (carries the object guid). Standalone
+/// GameMessage, dispatched like / CreateObject.
+///
+///
+/// The companion PrivateUpdatePropertyInt (0x02CD) targets the player's OWN
+/// object (no guid) and is not parsed here — it has no item-icon impact.
+///
+///
+/// Wire layout (ACE GameMessagePublicUpdatePropertyInt, size hint 17):
+///
+/// u32 opcode = 0x02CE
+/// u8 sequence // single byte (ByteSequence.NextBytes) — see PrivateUpdateVital
+/// u32 guid
+/// u32 property // PropertyInt enum; UiEffects = 18
+/// i32 value
+///
+/// The sequence is parsed-past but not honored (latest-wins; divergence DR-4).
+///
+public static class PublicUpdatePropertyInt
+{
+ public const uint Opcode = 0x02CEu;
+
+ public readonly record struct Parsed(uint Guid, uint Property, int Value);
+
+ /// Parse a raw 0x02CE body. Returns null on opcode mismatch / truncation.
+ public static Parsed? TryParse(ReadOnlySpan body)
+ {
+ if (body.Length < 17) return null; // 4 + 1 + 4 + 4 + 4
+ if (BinaryPrimitives.ReadUInt32LittleEndian(body) != Opcode) return null;
+ int pos = 4;
+ pos += 1; // sequence byte (not honored)
+ uint guid = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
+ uint prop = BinaryPrimitives.ReadUInt32LittleEndian(body[pos..]); pos += 4;
+ int value = BinaryPrimitives.ReadInt32LittleEndian(body[pos..]);
+ return new Parsed(guid, prop, value);
+ }
+}
diff --git a/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs b/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs
new file mode 100644
index 00000000..bda5555a
--- /dev/null
+++ b/tests/AcDream.Core.Net.Tests/Messages/PublicUpdatePropertyIntTests.cs
@@ -0,0 +1,36 @@
+using System.Buffers.Binary;
+using AcDream.Core.Net.Messages;
+
+namespace AcDream.Core.Net.Tests.Messages;
+
+public sealed class PublicUpdatePropertyIntTests
+{
+ private static byte[] Build(uint guid, uint property, int value, byte seq = 1, uint opcode = 0x02CEu)
+ {
+ var b = new byte[17];
+ BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(0), opcode);
+ b[4] = seq;
+ BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(5), guid);
+ BinaryPrimitives.WriteUInt32LittleEndian(b.AsSpan(9), property);
+ BinaryPrimitives.WriteInt32LittleEndian(b.AsSpan(13), value);
+ return b;
+ }
+
+ [Fact]
+ public void TryParse_uiEffectsUpdate_returnsGuidPropValue()
+ {
+ var p = PublicUpdatePropertyInt.TryParse(Build(0x50000001u, property: 18u, value: 0x9));
+ Assert.NotNull(p);
+ Assert.Equal(0x50000001u, p!.Value.Guid);
+ Assert.Equal(18u, p.Value.Property);
+ Assert.Equal(0x9, p.Value.Value);
+ }
+
+ [Fact]
+ public void TryParse_wrongOpcode_returnsNull()
+ => Assert.Null(PublicUpdatePropertyInt.TryParse(Build(1, 18, 1, opcode: 0x02CDu)));
+
+ [Fact]
+ public void TryParse_truncated_returnsNull()
+ => Assert.Null(PublicUpdatePropertyInt.TryParse(new byte[16]));
+}