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>
69 lines
2.2 KiB
C#
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();
|
|
}
|
|
}
|