feat(quest): QT3 — the contract tracker becomes state, and the events get routed
Fourth sibling J-owner, built to the shape the other three established. It borrows nothing, because the retail client stores no quest state of its own — everything here is a projection of what the server pushed. Clearing at generation reset is safe for the same reason: a fresh session opens with a full 0x0314 replacement, so the reset cannot lose anything the next login will not immediately restate, while NOT clearing would show a previous character's quests. Three readings of the wire that would each lose contracts silently, one test apiece: a 0x0314 REPLACES rather than merges (merging resurrects contracts the server dropped); an empty 0x0314 clears rather than being ignored (it is how the server says "you have none", and ignoring it strands the last quest on screen); and a delete carries a full tracker struct, so it looks exactly like an add apart from one flag. Adding a teardown stage exposed a genuine trap: TeardownStageCount bounds the drain loop while GameRuntimeTeardownStage.Complete defines what the ledger demands, and nothing tied them together. Leave the constant behind and the new owner is never disposed at all, while the ledger goes on waiting for its flag — the runtime hangs in teardown rather than failing anywhere near the edit. The stage-ledger test now reads the constant by reflection and asserts it against the flag list, so the next owner fails at the edit instead. Campaign QT slice 3 of 6. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ab3934e21d
commit
f629ce7f3d
9 changed files with 564 additions and 19 deletions
|
|
@ -131,7 +131,14 @@ public static class GameEventWiring
|
|||
Action<GameEvents.HouseData>? onHouseData = null,
|
||||
Action<uint /*weenieError*/>? onHouseStatus = null,
|
||||
Action<uint /*rentTime*/>? onHouseUpdateRentTime = null,
|
||||
Action<IReadOnlyList<GameEvents.HousePayment>>? onHouseUpdateRentPayment = null)
|
||||
Action<IReadOnlyList<GameEvents.HousePayment>>? onHouseUpdateRentPayment = null,
|
||||
// Campaign QT (2026-08-21): the contract tracker's two events. Same
|
||||
// Runtime-owned delegate-hole shape as house/trade above --
|
||||
// RuntimeContractState is the consumer. Both opcodes have been named
|
||||
// in GameEventType since the wire catalog with nothing behind them,
|
||||
// so until this wiring the bytes arrived and were dropped.
|
||||
Action<IReadOnlyDictionary<uint, ContractTracker>>? onContractTable = null,
|
||||
Action<ContractTrackerUpdate>? onContractUpdate = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(dispatcher);
|
||||
ArgumentNullException.ThrowIfNull(items);
|
||||
|
|
@ -446,6 +453,27 @@ public static class GameEventWiring
|
|||
});
|
||||
}
|
||||
|
||||
// Campaign QT (2026-08-21). Arrival is stamped HERE rather than
|
||||
// inside the parser's caller, because FillProgressString counts a
|
||||
// repeat timer down from the moment the state arrived and the server
|
||||
// never sends that moment.
|
||||
if (onContractTable is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.SendClientContractTrackerTable, e =>
|
||||
{
|
||||
var p = ContractTrackerMessages.ParseTable(e.Payload.Span, DateTime.UtcNow);
|
||||
if (p is not null) onContractTable(p);
|
||||
});
|
||||
}
|
||||
if (onContractUpdate is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.SendClientContractTracker, e =>
|
||||
{
|
||||
var p = ContractTrackerMessages.ParseUpdate(e.Payload.Span, DateTime.UtcNow);
|
||||
if (p is not null) onContractUpdate(p.Value);
|
||||
});
|
||||
}
|
||||
|
||||
if (onConfirmationRequest is not null)
|
||||
{
|
||||
registrar.Register(GameEventType.CharacterConfirmationRequest, e =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue