Commit graph

52 commits

Author SHA1 Message Date
Erik
6ab24c9982 feat(net+app): AnimPartChanges + Name extraction — characters clothed,
statue identified (Phase 4.7h/i/j/k/l)

Makes three big improvements to the CreateObject decode path:

1. Extract AnimPartChanges from the ModelData section instead of
   skipping them. Each change is (PartIndex, NewModelId); the server
   uses these to replace base Setup parts with armor/clothing/statue
   meshes. The player character has ~34 of them on a normal login.

2. Flow AnimPartChanges through WorldSession.EntitySpawn into
   GameWindow.OnLiveEntitySpawned, which now patches the flattened
   Setup's part list BEFORE uploading GfxObjs. Patching is a simple
   "parts[change.PartIndex] = new MeshRef(change.NewModelId, oldTransform)"
   keeping the base Setup's placement transform but swapping the mesh.

3. Read the WeenieHeader Name (String16L) that follows the PhysicsData
   section. Required walking past every remaining physics flag (Parent,
   Children, ObjScale, Friction, Elasticity, Translucency, Velocity,
   Acceleration, Omega, DefaultScript, DefaultScriptIntensity) plus the
   9 sequence timestamps (2 bytes each) plus 4-byte alignment. The
   Name field is then the second thing in the WeenieHeader after
   u32 weenieFlags.

Critical bug fix in the same commit: ACE's WritePackedDwordOfKnownType
STRIPS the known-type high-byte prefix (e.g. 0x01000000 for GfxObj ids)
before writing the PackedDword. The first version of AnimPartChange
decoding called plain ReadPackedDword, so it got 0x0000XXXX instead of
0x0100XXXX and every GfxObj dat lookup silently failed — the drop
counter showed 19+ noMeshRef drops including +Acdream himself.

Added ReadPackedDwordOfKnownType that ORs the knownType bit back in
on read (with zero preserved as the "no value" sentinel). After the
fix, noMeshRef drops = 0 across a full login.

LIVE RUN after all three changes:

  live: spawn guid=0x5000000A name="+Acdream" setup=0x02000001
        pos=(58.5,156.2,66.0)@0xA9B40017 animParts=34
  live: spawn guid=0x7A9B4035 name="Holtburg" setup=0x020006EF
        pos=(94.6,156.0,66.0)@0xA9B4001F animParts=0
  live: spawn guid=0x7A9B4000 name="Door" setup=0x020019FF
        pos=(84.1,131.5,66.1)@0xA9B40100 animParts=0
  live: spawn guid=0x7A9B4001 name="Chest" setup=0x0200007C
        pos=(78.1,136.9,69.5)@0xA9B40105 animParts=0
  live: spawn guid=0x7A9B4036 name="Well" setup=0x02000180
        pos=(90.1,157.8,66.0)@0xA9B4001F animParts=0
  live: spawn guid=0x800005FD name="Wide Breeches" setup=0x02000210
        pos=no-pos animParts=1
  live: spawn guid=0x800005FC name="Smock" setup=0x020000D4
        pos=no-pos animParts=1
  live: spawn guid=0x800005FE name="Shoes" setup=0x020000DE
        pos=no-pos animParts=1
  live: spawn guid=0x80000697 name="Facility Hub Portal Gem"
        setup=0x02000921 pos=no-pos animParts=0
  live: spawn guid=0x7A9B404B name="Nullified Statue of a Drudge"
        setup=0x020007DD pos=(65.3,156.8,72.8)@0xA9B40017 animParts=1

  summary recv=60 hydrated=43 drops: noPos=17 noSetup=0
                                     setupMissing=0 noMesh=0

The statue's exact data is now known and the hydration path runs
without errors. The user's "look at the Name field in the CreateObject
body" insight turned this from an unbounded visual hunt into a targeted
grep of ~60 log lines.

Tests: 77 core + 83 net = 160 passing (offline suite unchanged).
Live handshake + enter-world tests still pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:48:13 +02:00
Erik
713bec256b feat(net+app): WorldSession class + GameWindow live-mode wiring (Phase 4.7e/f)
The end-to-end pipeline. acdream can now connect to a live ACE server,
complete the full handshake + character-select + enter-world flow, and
stream CreateObject messages straight into the existing IGameState and
static mesh renderer. Gated behind ACDREAM_LIVE=1 so the default
offline run path is untouched.

Added:
  - AcDream.Core.Net.WorldSession: high-level session type that owns a
    NetClient, drives the 3-leg handshake, parses CharacterList, sends
    CharacterEnterWorldRequest + CharacterEnterWorld, and converts the
    post-login fragment stream into C# events. State machine:
    Disconnected → Handshaking → InCharacterSelect → EnteringWorld →
    InWorld (or Failed). Public API:
      * Connect(user, pass)  — blocks until CharacterList received
      * EnterWorld(user, characterIndex) — blocks until ServerReady
      * Tick() — non-blocking, call per game-loop frame
      * event EntitySpawned
      * event StateChanged
      * Characters property (populated after Connect)

  - NetClient.TryReceive: non-blocking variant that returns immediately
    with null if the kernel buffer is empty. Enables draining packets
    per frame from the main thread without stalling.

  - GameWindow live-mode hookup:
      * AcDream.Core.Net project reference
      * TryStartLiveSession() called after dat hydration, gated behind
        ACDREAM_LIVE=1 + ACDREAM_TEST_USER/ACDREAM_TEST_PASS env vars
      * Subscribes EntitySpawned to OnLiveEntitySpawned
      * Calls Connect() then EnterWorld(0) synchronously on startup
      * OnLiveEntitySpawned hydrates mesh refs from the Setup dat
        (same SetupMesh.Flatten + GfxObjMesh.Build + StaticMesh.EnsureUploaded
        path used by scenery), publishes a WorldEntitySnapshot via
        _worldGameState.Add + _worldEvents.FireEntitySpawned, and
        appends to _entities so the next frame picks it up
      * OnUpdate calls _liveSession?.Tick() each frame
      * OnClosing disposes the session
      * Position translation: server sends (LandblockId, local XYZ +
        quaternion); we map landblock to world origin relative to the
        rendered 3x3 center, add local XYZ, translate AC's (W,X,Y,Z)
        quaternion wire order to System.Numerics.Quaternion (X,Y,Z,W)

LIVE RUN OUTPUT (ACDREAM_LIVE=1 against localhost ACE, testaccount):

  [dats loaded, 1133 static entities hydrated]
  live: connecting to 127.0.0.1:9000 as testaccount
  live: entering world as 0x5000000A +Acdream
  live: in world — CreateObject stream active (so far: 0 received, 0 hydrated)
  live: spawned guid=0x5000000A setup=0x02000001 world=(104.9,15.1,94.0)
  live: spawned guid=0x7A9B4013 setup=0x0200007C world=(135.7,9.9,97.0)
  live: spawned guid=0x7A9B4014 setup=0x0200007C world=(132.5,9.9,97.0)
  live: spawned guid=0x7A9B4015 setup=0x020019FF world=(132.6,17.1,94.1)
  live: spawned guid=0x7A9B4016 setup=0x020019FF world=(136.3,5.2,94.1)
  live: spawned guid=0x7A9B4017 setup=0x020019FF world=(104.1,31.0,94.1)
  live: spawned guid=0x7A9B4037 setup=0x02000975 world=(109.7,33.0,95.0)
  live: spawned guid=0x7A9B4018 setup=0x020019FF world=(110.9,31.0,94.1)
  live: spawned guid=0x7A9B4019 setup=0x020019FF world=(107.5,31.5,94.1)
  live: spawned guid=0x7A9B403B setup=0x02000B8E world=(150.5,17.9,94.0)
  live: (suppressing further spawn logs)

First line: +Acdream himself. setup=0x02000001 is ACE's default humanoid
player mesh. world coords match Holtburg (landblock 0xA9B4 local
space). Subsequent spawns are weenies at various setup ids — likely
the foundry statue, street lamps, drums, etc. The 0x7A9B4xxx GUID
pattern is ACE's convention: scenery-type (0x7) + landblock (0xA9B4) +
per-object index.

All spawns flow through the SAME SetupMesh/GfxObjMesh/StaticMeshRenderer
pipeline used by scenery and interiors today. The plugin system's
EntitySpawned event fires on every new entity, so plugins can see
them without any networking awareness.

Tests: 160 passing offline (77 core + 83 net). The live handshake and
enter-world tests are gated and still pass when ACDREAM_LIVE=1.

User visual verification is the final acceptance for Phase 4. Run
with ACDREAM_DAT_DIR + ACDREAM_LIVE=1 + ACDREAM_TEST_USER=testaccount
+ ACDREAM_TEST_PASS=testpassword and look for +Acdream's model + the
foundry statue standing on top of the Holtburg foundry.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 15:25:41 +02:00