diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index d574887..4b68d42 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -120,7 +120,13 @@ public static class CreateObject ushort ServerControlSequence = 0, ushort ForcePositionSequence = 0, uint? PhysicsState = null, - uint? ObjectDescriptionFlags = null); + uint? ObjectDescriptionFlags = null, + // L.3b (2026-04-30): per-object friction + elasticity from the + // wire. Default to null when their PhysicsDescriptionFlag bits + // weren't set; subscribers fall back to PhysicsBody constructor + // defaults (0.05f elasticity, 0.5f friction). + float? Friction = null, + float? Elasticity = null); /// /// The relevant subset of the server-sent MovementData / @@ -286,6 +292,13 @@ public static class CreateObject // "ObjectDescriptionFlags" at the WeenieHeader trailer. uint? physicsState = null; uint? objectDescriptionFlags = null; + // L.3b (2026-04-30): per-object friction + elasticity. Wire-encoded + // when their PhysicsDescriptionFlag bits are set. Default values + // come from PhysicsBody constructors; these overrides drive the + // velocity-reflection bounce magnitude per object (e.g., bouncier + // platforms vs. inert walls). + float? friction = null; + float? elasticity = null; try { @@ -453,8 +466,25 @@ public static class CreateObject objScale = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); pos += 4; } - if ((physicsFlags & PhysicsDescriptionFlag.Friction) != 0) pos += 4; - if ((physicsFlags & PhysicsDescriptionFlag.Elasticity) != 0) pos += 4; + if ((physicsFlags & PhysicsDescriptionFlag.Friction) != 0) + { + if (body.Length - pos < 4) return PartialResult(); + friction = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); + pos += 4; + } + if ((physicsFlags & PhysicsDescriptionFlag.Elasticity) != 0) + { + // L.3b (2026-04-30): capture instead of skipping. The wire + // float is the per-object elasticity used by the velocity- + // reflection bounce (CPhysicsObj::set_elasticity at + // acclient_2013_pseudo_c.txt:277817, clamped to [0, 0.1]). + // Was previously dropped — every object got the default + // 0.05f, so server-set bouncier surfaces felt identical to + // walls. + if (body.Length - pos < 4) return PartialResult(); + elasticity = BinaryPrimitives.ReadSingleLittleEndian(body.Slice(pos)); + pos += 4; + } if ((physicsFlags & PhysicsDescriptionFlag.Translucency) != 0) pos += 4; if ((physicsFlags & PhysicsDescriptionFlag.Velocity) != 0) pos += 12; // vec3 if ((physicsFlags & PhysicsDescriptionFlag.Acceleration) != 0) pos += 12; @@ -510,14 +540,18 @@ public static class CreateObject return new Parsed(guid, position, setupTableId, animParts, textureChanges, subPalettes, basePaletteId, objScale, name, itemType, motionState, motionTableId, instanceSeq, teleportSeq, serverControlSeq, forcePositionSeq, - physicsState, objectDescriptionFlags); + physicsState, objectDescriptionFlags, + friction, elasticity); // Local helper: if we ran out of fields past PhysicsData, still // return the useful prefix (guid/position/setup/animParts/textures/palettes/scale/motion). Parsed PartialResult() => new( guid, position, setupTableId, animParts, textureChanges, subPalettes, basePaletteId, objScale, null, null, motionState, motionTableId, - PhysicsState: physicsState, ObjectDescriptionFlags: objectDescriptionFlags); + PhysicsState: physicsState, + ObjectDescriptionFlags: objectDescriptionFlags, + Friction: friction, + Elasticity: elasticity); } catch { diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index 030573b..47ef10b 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -63,7 +63,13 @@ public sealed class WorldSession : IDisposable // ObjectDescriptionFlags: retail PWD._bitfield (acclient.h:6431-6463) // — drives IsPlayer/IsPK/IsPKLite/IsImpenetrable for PvP gating. uint? PhysicsState = null, - uint? ObjectDescriptionFlags = null); + uint? ObjectDescriptionFlags = null, + // L.3b (2026-04-30): per-object physics tuning from the wire. + // Friction defaults to PhysicsBody constructor value (0.5f). + // Elasticity defaults to 0.05f. When set, drives the velocity- + // reflection bounce magnitude (clamped to [0, 0.1] retail-side). + float? Friction = null, + float? Elasticity = null); /// Fires when the session finishes parsing a CreateObject. public event Action? EntitySpawned; @@ -657,7 +663,9 @@ public sealed class WorldSession : IDisposable parsed.Value.MotionState, parsed.Value.MotionTableId, parsed.Value.PhysicsState, - parsed.Value.ObjectDescriptionFlags)); + parsed.Value.ObjectDescriptionFlags, + parsed.Value.Friction, + parsed.Value.Elasticity)); } } else if (op == DeleteObject.Opcode)