From 851e88364dbc5b87b49fe40c934c53a10165bb81 Mon Sep 17 00:00:00 2001 From: Erik Date: Thu, 30 Apr 2026 09:43:27 +0200 Subject: [PATCH] =?UTF-8?q?feat(net):=20L.3b=20=E2=80=94=20capture=20per-o?= =?UTF-8?q?bject=20friction=20+=20elasticity=20from=20CreateObject?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to L.3a (a1c27b3) which ported the velocity-reflection bounce. Previously the CreateObject parser did `pos += 4` for both Friction and Elasticity floats — silently dropping the wire data so every entity got the PhysicsBody constructor default (0.05 elasticity, 0.5 friction). Server-set bouncier surfaces or stickier objects therefore felt identical to inert walls on collision. Inelastic projectiles via PhysicsState bit 0x20000 (already plumbed in Commit A) had no per- object elasticity to override. Now the parser captures the floats, surfaces them on Parsed + EntitySpawn, leaving the values at default (null) when their PhysicsDescriptionFlag bits aren't set. Subscribers (e.g., the remote-entity dead-reckoning path, future spell-projectile rendering) can apply them when they wire elasticity to PhysicsBody.Elasticity. The local player's PhysicsBody is constructed at controller init, not from a CreateObject — so this commit alone produces no user-visible local-player change. Effect lands when remote/projectile physics consume EntitySpawn.Elasticity. Files: - CreateObject.cs:284-294: declare friction + elasticity accumulators. - CreateObject.cs:467-487: parse floats instead of skipping. - CreateObject.cs:543-555: propagate to Parsed via both return paths. - WorldSession.cs:67-71: extend EntitySpawn record. - WorldSession.cs:665-668: pipe through to subscribers. Tests: 1491 still pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.Core.Net/Messages/CreateObject.cs | 44 ++++++++++++++++--- src/AcDream.Core.Net/WorldSession.cs | 12 ++++- 2 files changed, 49 insertions(+), 7 deletions(-) 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)