acdream/src/AcDream.Core/Plugins/WorldGameState.cs
Erik fe1e68e5fe 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>
2026-08-21 15:22:22 +02:00

69 lines
2.2 KiB
C#

// src/AcDream.Core/Plugins/WorldGameState.cs
using AcDream.Plugin.Abstractions;
namespace AcDream.Core.Plugins;
public sealed class WorldGameState : IGameState
{
private readonly List<WorldEntitySnapshot> _entities = new();
private readonly Dictionary<uint, int> _indexById = new();
public IReadOnlyList<WorldEntitySnapshot> Entities => _entities;
/// <summary>
/// Where <see cref="Contracts"/> reads from. Set once by the host.
/// </summary>
/// <remarks>
/// A pull-through source rather than a pushed list: contracts change rarely
/// and are already owned canonically elsewhere, so mirroring them here
/// would add a second copy to keep in step for no gain.
/// </remarks>
public Func<IReadOnlyList<ContractSnapshot>>? ContractsSource { get; set; }
public IReadOnlyList<ContractSnapshot> Contracts =>
ContractsSource?.Invoke() ?? [];
/// <summary>
/// Publish the current projection for an entity. Re-hydration replaces the
/// prior snapshot instead of turning the current-state API into history.
/// </summary>
public void Add(WorldEntitySnapshot snapshot)
{
if (_indexById.TryGetValue(snapshot.Id, out int index))
{
_entities[index] = snapshot;
return;
}
_indexById.Add(snapshot.Id, _entities.Count);
_entities.Add(snapshot);
}
/// <summary>
/// Remove any snapshot with the given local <c>Id</c>. Used when the
/// server re-sends <c>CreateObject</c> for an entity already known to
/// acdream — the host deletes the old snapshot before adding the new
/// one so plugins don't see stale duplicates.
/// </summary>
public bool RemoveById(uint id)
{
if (!_indexById.Remove(id, out int index))
return false;
int lastIndex = _entities.Count - 1;
if (index != lastIndex)
{
WorldEntitySnapshot moved = _entities[lastIndex];
_entities[index] = moved;
_indexById[moved.Id] = index;
}
_entities.RemoveAt(lastIndex);
return true;
}
public void Clear()
{
_entities.Clear();
_indexById.Clear();
}
}