Commit graph

7 commits

Author SHA1 Message Date
Erik
29afc94b94 fix(net): Phase L.1c conform combat wire events 2026-04-28 10:54:50 +02:00
Erik
ff5ed9ec0b feat(net): #18 holtburger inbound chat parity - EmoteText, SoulEmote, ServerMessage, PlayerKilled, WeenieError + Windows-1252 codec
Five sub-changes:

1. Windows-1252 codec switch (global). Every Encoding.ASCII call site
   in src/AcDream.Core.Net/Messages/ -> Encoding.GetEncoding(1252).
   Touched HearSpeech, ChatRequests, GameEvents, AppraiseInfoParser,
   CharacterList, CreateObject, PlayerDescriptionParser, SocialActions.
   New Encodings.cs module-init registers CodePagesEncodingProvider
   (System.Text.Encoding.CodePages ships with .NET 10 SDK but isn't
   auto-registered). Matches retail + holtburger; accented names
   no longer round-trip-broken.

2. New parsers (opcodes confirmed against holtburger opcodes.rs):
   - EmoteText (0x01E0)     { u32 senderGuid, string16 senderName, string16 text }
   - SoulEmote (0x01E2)     same wire layout as EmoteText
   - ServerMessage (0xF7E0) { string16 message, u32 chatType }
   - PlayerKilled (0x019E)  { string16 deathMessage, u32 victimGuid, u32 killerGuid }
   Shared StringReader.cs has the CP1252 String16L primitive.

3. WorldSession dispatch. ProcessDatagram adds branches for the four
   new top-level opcodes + fires session-level events (EmoteHeard,
   SoulEmoteHeard, ServerMessageReceived, PlayerKilledReceived).
   0x0295 SetTurbineChatChannels stubbed with TODO for parallel I.6.

4. GameEventWiring routes WeenieError + WeenieErrorWithString
   (parsers existed but were unrouted) -> chat.OnWeenieError.

5. ChatLog adapters: Emote / SoulEmote ChatKind values, OnEmote,
   OnSoulEmote, OnPlayerKilled, OnWeenieError. OnLocalSpeech now
   substitutes empty sender -> "You" per holtburger client/messages.rs.
   ChatVM.FormatEntry handles new kinds (asterisk + sender + text).

22 new tests covering parser round-trips + reject-bad-opcode +
ChatLog adapter coverage + Win-1252 round-trip with non-ASCII chars.
Solution total: 881 green (210->225 in Core.Net.Tests, 606->613 in Core.Tests).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:06:01 +02:00
Erik
3fd9f264b7 feat(net): 9 more GameEvent payload parsers
Extends GameEvents.cs with parsers for commonly-observed events that
had been routed-but-unparsed by the F.1 dispatcher:

- UseDone (0x01C7): u32 weenieError — Use/UseWithTarget completion.
- InventoryPutObjectIn3D (0x019A): u32 itemGuid — server dropped item.
- InventoryServerSaveFailed (0x00A0): u32 itemGuid — rollback signal.
- CloseGroundContainer (0x0052): u32 containerGuid.
- TradeFailure (0x0207): u32 errorCode.
- AddToTrade (0x0200): (itemGuid, slotIndex).
- AcceptTrade (0x0202): u32 initiatorGuid.
- QueryItemManaResponse (0x0264): (itemGuid, f32 manaPercent).
- CharacterConfirmationRequest (0x0274): (type, contextId, otherGuid,
  string16L message) — server-driven modal confirmations.

All defensive: return null on truncated payloads rather than throwing,
matching the existing style. Caller can keep the dispatcher alive even
on malformed events.

Build green, tests unchanged (no new tests — these are simple passthroughs).

Ref: r08 §4 payloads for each opcode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:29:56 +02:00
Erik
c95aedcd4a feat(spells): Phase E.5 CastSpellRequest + Spellbook/enchantment state
Completes the client-side spell loop on top of Phase F.1. Player can
send cast requests; spellbook + active-enchantment state mirrors what
the server broadcasts.

Wire layer:
- CastSpellRequest (C→S, inside 0xF7B1 GameAction):
  - BuildUntargeted (0x0048): self-buffs, recalls, heal-self — 16 bytes.
  - BuildTargeted (0x004A): projectile attacks, target buffs/debuffs — 20 bytes.
- GameEvents parsers added:
  - 0x02C1 MagicUpdateSpell: spell-id → spellbook.
  - 0x01A8 MagicRemoveSpell.
  - 0x02C2 MagicUpdateEnchantment: spellId + layerId + duration + casterGuid
    (summary head; full stat-mod body deferred).
  - 0x02C3 MagicRemoveEnchantment: (layerId, spellId).
  - 0x02C7 MagicDispelEnchantment: same shape.

Core layer:
- Spellbook: learned-spell set + active-enchantment-by-layer dict
  with events (SpellLearned, SpellForgotten, EnchantmentAdded,
  EnchantmentRemoved). Duplicate learn is idempotent. Same-layer
  add refreshes duration. Purge fires per-record remove for UI
  cleanup.
- ActiveEnchantmentRecord: (SpellId, LayerId, Duration, CasterGuid).

Tests (10 new):
- CastSpellRequest untargeted (16 bytes) + targeted (20 bytes) wire encoding.
- GameEvents: MagicUpdateSpell, MagicUpdateEnchantment,
  MagicRemoveEnchantment round-trip.
- Spellbook: learn idempotent, forget, add/refresh enchantment,
  remove fires event, purge-all clears + fires per-record.

Build green, 555 tests pass (up from 544).

Ref: r01 §2 (wire casts), §3 (cast state machine), §5 (stacking rules).
Ref: r08 §3 opcodes 0x0048/0x004A, §4 opcodes 0x01A8/0x02C1-0x02C8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 17:00:32 +02:00
Erik
2e3f9d7a04 feat(combat): Phase E.4 AttackTargetRequest + combat notification pipeline
Completes the client-side combat loop: send attacks, receive server's
damage broadcasts, maintain per-entity health state for HP bars +
damage floaters. All atop Phase F.1's GameEvent dispatcher.

Wire layer:
- AttackTargetRequest (0x0008 C→S, inside 0xF7B1): targetGuid +
  powerLevel + accuracyLevel + attackHeight. 28-byte body.
- GameEvents parsers for all combat notifications from r08 §4:
  - VictimNotification (0x01AC) — you got hit, full details
  - KillerNotification (0x01AD) — you killed X
  - AttackerNotification (0x01B1) — you hit X for Y (damage%)
  - DefenderNotification (0x01B2) — X hit you
  - EvasionAttackerNotification (0x01B3) — X evaded
  - EvasionDefenderNotification (0x01B4) — you evaded X
  - AttackDone (0x01A7) — attack sequence completed

Core layer:
- CombatState: per-entity health-percent cache + typed events
  (HealthChanged, DamageTaken, DamageDealtAccepted, EvadedIncoming,
  MissedOutgoing, AttackDone). Each event carries enough detail for
  the UI to render damage floaters, HP bars, and a combat log panel.
  Server is authoritative; client only mirrors state.

The server computes damage (armor, resist, crit, hit-chance); the
client only displays results. Predictive UI like "estimated damage
at 0.75 power" still works via the existing CombatMath helper class
that was in the scaffold (r02 §5 formulas).

Tests (13 new):
- AttackTargetRequest byte-exact wire encoding
- VictimNotification / AttackerNotification / EvasionAttacker /
  AttackDone round-trip parse.
- CombatState: UpdateHealth caches + fires, Victim fires DamageTaken,
  Attacker fires DamageDealt, Evasion routes to right event, AttackDone
  carries sequence+error, Clear resets cache.

Build green, 544 tests pass (up from 532).

Ref: r02 §7 (wire formats), r08 §4 (event payloads), ACE
GameEvent*Notification.cs families.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:58:14 +02:00
Erik
2561f5599f feat(items): Phase F.2 ItemRepository + AppraiseRequest round-trip
Implements the item-state mirror + appraise round-trip infrastructure
on top of Phase F.1's GameEvent dispatcher.

Core layer (AcDream.Core/Items):
- ItemRepository: ConcurrentDictionary-backed live item state keyed by
  server ObjectId. Events: ItemAdded, ItemMoved, ItemRemoved,
  ItemPropertiesUpdated. MoveItem handles container / slot / equip
  location updates atomically and fires ItemMoved with old+new container
  ids. UpdateProperties merges a PropertyBundle patch (for appraise
  results) without clobbering existing untouched keys.

Wire layer (AcDream.Core.Net/Messages):
- AppraiseRequest (0x00C8 C→S, inside 0xF7B1 GameAction envelope):
  Build(sequence, targetGuid) → 16-byte body ready for SendGameAction.
- GameEvents.ParseIdentifyResponseHeader for 0x00C9 S→C — extracts
  (guid, appraiseFlags, success). Full PropertyBundle deserialization
  (the 10-flag bitfield-indexed tables) is a future pass; header alone
  is enough to route into the repository + surface "appraise complete"
  to UI.
- GameEvents.ParseWieldObject (0x0023) — server-driven equip.
- GameEvents.ParsePutObjInContainer (0x0022) — server-driven inventory
  move (item, container, placement).

Tests (11 new):
- ItemRepository: add/update fires correct event, move updates fields,
  missing-id returns false, remove, properties merge, clear.
- Wire: AppraiseRequest byte-exact encoding, IdentifyResponse header
  round-trip, WieldObject round-trip, PutObjInContainer round-trip.

Build green, 532 tests pass (up from 521).

Phase F.2 unblocks the Paperdoll + Inventory UI panels and the
"appraise on right-click" UX. Next pieces: PropertyBundle full
deserializer (AppraiseInfo 10-flag bitfield), outbound move/drop/
pickup actions.

Ref: r06 §1 (ItemType), §2 (EquipMask), §5 (appraise wire), §7 (pack
depth rules).
Ref: ACE GameEventIdentifyObjectResponse.cs for AppraiseInfo format.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:55:36 +02:00
Erik
d86fd08011 feat(net): Phase F.1 GameEvent (0xF7B0) envelope dispatcher
Implements the inbound GameEvent routing layer — the single biggest
network-protocol gap per r08 (94 sub-opcodes, zero handled before).
WorldSession now detects 0xF7B0, parses the 16-byte header (guid +
gameEventSequence + eventType), and forwards to a pluggable
GameEventDispatcher.

Added:
- GameEventEnvelope record + TryParse with layout from
  ACE GameEventMessage.cs.
- GameEventType enum: all 94 S→C sub-opcodes from
  ACE.Server.Network.GameEvent.GameEventType, named per ACE conventions.
- GameEventDispatcher: handler registry + unhandled-counts bag for
  diagnostics ("which server events are firing that we don't parse?").
  Handlers invoked synchronously on the decode thread; thrown exceptions
  are swallowed + logged to stderr so one bad handler can't take down
  the packet loop.
- GameEvents parsers: ChannelBroadcast, Tell, TransientMessage,
  PopupString, WeenieError (+ WithString), UpdateHealth, PingResponse,
  MagicUpdateSpell. Each returns a typed record or null on malformed
  payload. String16L helper matches the existing CharacterList pattern
  (u16 length + ASCII bytes + 4-byte pad).
- WorldSession.GameEvents property exposing the dispatcher so
  GameWindow / UI / chat can register handlers at startup.

Wired into WorldSession.ProcessDatagram: new `else if (op ==
GameEventEnvelope.Opcode)` branch with TryParse + Dispatch.

Tests (13 new):
- Envelope: valid round-trip, wrong outer opcode, too-short body.
- Dispatcher: handler invoked, unhandled count, exception isolation,
  unregister + rollover to unhandled.
- Event parsers: ChannelBroadcast, Tell, UpdateHealth, WeenieError,
  Transient, MagicUpdateSpell.

Total: 521 tests pass (up from 508).

With this dispatcher in place, Phase F.2 (items + appraise), F.3 (combat
+ damage), F.4 (spell cast state machine), chat UI, allegiance, quest
tracker — all of which depend on GameEvent handling — are unblocked.

Ref: r08 §4 (GameEvent sub-opcode table), §2 (envelope wire shape).
Ref: ACE GameEventMessage.cs / GameEventType.cs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:52:46 +02:00