feat(quest): QT6 — plugins can read the contract tracker
The last piece of QT6's own scope: r10-quest-dialogs.md §11.6's contract half. IGameState.Contracts exposes what the client structurally knows about quests, which — per that same research — is the tracker and nothing else. The rest of §11.6 (chat stream, tells, give, use, confirmations) is other features and stays out of this campaign. A pull-through source rather than a pushed mirror. Contracts change rarely and are already owned canonically, so a second copy would only be a thing to keep in step; reading through means a plugin cannot observe a stale list. Both hosts implement it. The headless one carries contract id, stage and progress but no names — a bot has no dat access — because losing the TEXT is expected while losing the QUEST would leave a bot silently unable to see what it is on. Same rule covers a contract the installed dat has never heard of: it still projects, with empty text and a correct status, rather than vanishing. The interface member is defaulted so a host predating this campaign still satisfies IGameState. Two lazy catalog loads exist (the panel's and this one) rather than one shared instance. That is deliberate: threading a shared ContractCatalog through three composition records to avoid reading a 322-row immutable table at most twice per session would be plumbing for no correctness or performance gain, and the comment at the call site says so. Campaign QT is complete; the connected user gate is owed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
56beeb720d
commit
fe1e68e5fe
8 changed files with 305 additions and 0 deletions
59
src/AcDream.Runtime/Gameplay/ContractPluginProjection.cs
Normal file
59
src/AcDream.Runtime/Gameplay/ContractPluginProjection.cs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Quests;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
|
||||
namespace AcDream.Runtime.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Projects the canonical contract tracker into the plugin-facing
|
||||
/// <see cref="ContractSnapshot"/> shape.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Lives here rather than in either host because both of them need it and
|
||||
/// neither owns the tracker. The authored catalog is optional: a headless bot
|
||||
/// has no dat access, and a contract the installed dat has never heard of still
|
||||
/// has to appear — a plugin must not silently miss a live quest because the
|
||||
/// text for it is unavailable.
|
||||
/// </remarks>
|
||||
public static class ContractPluginProjection
|
||||
{
|
||||
public static IReadOnlyList<ContractSnapshot> Project(
|
||||
IRuntimeContractView contracts,
|
||||
ContractCatalog? catalog,
|
||||
DateTime now)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(contracts);
|
||||
|
||||
IReadOnlyList<ContractTracker> tracked = contracts.GetContracts();
|
||||
if (tracked.Count == 0)
|
||||
return [];
|
||||
|
||||
uint displayed = contracts.Snapshot.DisplayContractId;
|
||||
var result = new ContractSnapshot[tracked.Count];
|
||||
for (int i = 0; i < tracked.Count; i++)
|
||||
{
|
||||
ContractTracker tracker = tracked[i];
|
||||
ContractEntry? entry = catalog?.Lookup(tracker.ContractId);
|
||||
|
||||
result[i] = new ContractSnapshot(
|
||||
tracker.ContractId,
|
||||
(uint)tracker.Stage,
|
||||
tracker.Progress,
|
||||
tracker.ContractId == displayed,
|
||||
entry?.ContractName ?? string.Empty,
|
||||
entry?.Description ?? string.Empty,
|
||||
entry is null
|
||||
? string.Empty
|
||||
: ContractProgressText.Build(
|
||||
(uint)tracker.Stage,
|
||||
tracker.TimeWhenRepeats,
|
||||
tracker.ReceivedAt,
|
||||
entry,
|
||||
now));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue