From f67f7851e67abcbfdfa702e11d3939024ef97c92 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 11 Apr 2026 18:01:14 +0200 Subject: [PATCH] feat(net+app): apply ObjScale from PhysicsData (Phase 5c) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nullified Statue of a Drudge renders correctly in color/shape after Phase 5b's SubPalette fix, but the user reported it's rendering at base drudge size when it should be larger. AC statues use the PhysicsDescriptionFlag.ObjScale field to scale the base mesh; my parser was consuming-and-skipping those 4 bytes. Changes: - CreateObject.TryParse: extract the u32 float from the ObjScale field instead of advancing past it. Declaration moved to the top of the method alongside other accumulators so the PartialResult local function at the bottom can reference it for the truncation fallback path. Same structural change for position and setupTableId since PartialResult already needed them too. - CreateObject.Parsed gains ObjScale (float?). - WorldSession.EntitySpawn gains ObjScale; propagated through the fire site in ProcessDatagram. - GameWindow.OnLiveEntitySpawned bakes a scale matrix into every MeshRef's PartTransform when ObjScale != 1.0, following the same pattern the offline scenery hydration already uses. No change to WorldEntity or StaticMeshRenderer — the scale is absorbed into the per-part transform the renderer already multiplies through. Tests: 77 core + 83 net = 160, all green. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 14 ++++++++++- src/AcDream.Core.Net/Messages/CreateObject.cs | 23 ++++++++++++++----- src/AcDream.Core.Net/WorldSession.cs | 2 ++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 4404954..733c026 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -684,6 +684,14 @@ public sealed class GameWindow : IDisposable } } + // Apply ObjScale by baking a scale matrix into each MeshRef's + // PartTransform. Scenery hydration already does this pattern + // (scaleMat baked into PartTransform at Setup flatten time). + // Fallback to 1.0 if the server didn't send ObjScale (common for + // creatures/characters whose size is intrinsic to the mesh). + float scale = spawn.ObjScale ?? 1.0f; + var scaleMat = System.Numerics.Matrix4x4.CreateScale(scale); + var meshRefs = new List(); for (int partIdx = 0; partIdx < parts.Count; partIdx++) { @@ -697,7 +705,11 @@ public sealed class GameWindow : IDisposable if (resolvedOverridesByPart is not null && resolvedOverridesByPart.TryGetValue(partIdx, out var partOverrides)) surfaceOverrides = partOverrides; - meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, mr.PartTransform) + // Pre-multiply scale so it happens FIRST (local mesh coords) before + // the part-local transform and the entity-root transform. + var transform = scale == 1.0f ? mr.PartTransform : scaleMat * mr.PartTransform; + + meshRefs.Add(new AcDream.Core.World.MeshRef(mr.GfxObjId, transform) { SurfaceOverrides = surfaceOverrides, }); diff --git a/src/AcDream.Core.Net/Messages/CreateObject.cs b/src/AcDream.Core.Net/Messages/CreateObject.cs index edafedd..da6cae2 100644 --- a/src/AcDream.Core.Net/Messages/CreateObject.cs +++ b/src/AcDream.Core.Net/Messages/CreateObject.cs @@ -89,6 +89,7 @@ public static class CreateObject IReadOnlyList TextureChanges, IReadOnlyList SubPalettes, uint? BasePaletteId, + float? ObjScale, string? Name); /// @@ -134,6 +135,13 @@ public static class CreateObject /// public static Parsed? TryParse(ReadOnlySpan body) { + // Accumulators declared at the top so PartialResult (local function + // at the bottom) can reference them before they're conditionally + // populated — C# rejects forward references otherwise. + ServerPosition? position = null; + uint? setupTableId = null; + float? objScale = null; + try { int pos = 0; @@ -229,7 +237,6 @@ public static class CreateObject pos += 4; } - ServerPosition? position = null; if ((physicsFlags & PhysicsDescriptionFlag.Position) != 0) { if (body.Length - pos < 32) return null; @@ -263,7 +270,6 @@ public static class CreateObject pos += 4; } - uint? setupTableId = null; if ((physicsFlags & PhysicsDescriptionFlag.CSetup) != 0) { if (body.Length - pos < 4) return null; @@ -288,7 +294,12 @@ public static class CreateObject if (body.Length - pos < childCount * 8) return null; pos += childCount * 8; // each child = guid u32 + locationId u32 } - if ((physicsFlags & PhysicsDescriptionFlag.ObjScale) != 0) pos += 4; + if ((physicsFlags & PhysicsDescriptionFlag.ObjScale) != 0) + { + if (body.Length - pos < 4) return PartialResult(); + 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.Translucency) != 0) pos += 4; @@ -316,13 +327,13 @@ public static class CreateObject } return new Parsed(guid, position, setupTableId, animParts, - textureChanges, subPalettes, basePaletteId, name); + textureChanges, subPalettes, basePaletteId, objScale, name); // Local helper: if we ran out of fields past PhysicsData, still - // return the useful prefix (guid/position/setup/animParts/textures/palettes). + // return the useful prefix (guid/position/setup/animParts/textures/palettes/scale). Parsed PartialResult() => new( guid, position, setupTableId, animParts, - textureChanges, subPalettes, basePaletteId, null); + textureChanges, subPalettes, basePaletteId, objScale, null); } catch { diff --git a/src/AcDream.Core.Net/WorldSession.cs b/src/AcDream.Core.Net/WorldSession.cs index c85de0e..7de33b1 100644 --- a/src/AcDream.Core.Net/WorldSession.cs +++ b/src/AcDream.Core.Net/WorldSession.cs @@ -50,6 +50,7 @@ public sealed class WorldSession : IDisposable IReadOnlyList TextureChanges, IReadOnlyList SubPalettes, uint? BasePaletteId, + float? ObjScale, string? Name); /// Fires when the session finishes parsing a CreateObject. @@ -234,6 +235,7 @@ public sealed class WorldSession : IDisposable parsed.Value.TextureChanges, parsed.Value.SubPalettes, parsed.Value.BasePaletteId, + parsed.Value.ObjScale, parsed.Value.Name)); } }