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
|
|
@ -0,0 +1,131 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Quests;
|
||||
using AcDream.Plugin.Abstractions;
|
||||
using AcDream.Runtime.Gameplay;
|
||||
|
||||
namespace AcDream.Runtime.Tests.Gameplay;
|
||||
|
||||
/// <summary>
|
||||
/// Campaign QT slice QT6: what a plugin sees of the contract tracker.
|
||||
/// </summary>
|
||||
public sealed class ContractPluginProjectionTests
|
||||
{
|
||||
private static readonly DateTime Now = new(2026, 8, 21, 12, 0, 0, DateTimeKind.Utc);
|
||||
|
||||
private static void Track(
|
||||
RuntimeContractState state,
|
||||
uint contractId,
|
||||
uint stage,
|
||||
bool setAsDisplay = false)
|
||||
=> state.ApplyUpdate(new ContractTrackerUpdate(
|
||||
new ContractTracker(1u, contractId, (ContractStage)stage, 0d, 0d, Now),
|
||||
Delete: false,
|
||||
SetAsDisplay: setAsDisplay));
|
||||
|
||||
private static ContractCatalog Catalog(uint id, string name, string progressFormat = "")
|
||||
=> new(new Dictionary<uint, ContractEntry>
|
||||
{
|
||||
[id] = ContractEntry.Unknown with
|
||||
{
|
||||
ContractId = id,
|
||||
ContractName = name,
|
||||
Description = "Do the thing.",
|
||||
DescriptionProgress = progressFormat,
|
||||
},
|
||||
});
|
||||
|
||||
[Fact]
|
||||
public void AnEmptyTrackerProjectsToNothing()
|
||||
{
|
||||
using var state = new RuntimeContractState();
|
||||
|
||||
Assert.Empty(ContractPluginProjection.Project(state.View, ContractCatalog.Empty, Now));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheProjectionCarriesTheAuthoredTextAndTheRetailStatus()
|
||||
{
|
||||
using var state = new RuntimeContractState();
|
||||
Track(state, 0x10u, stage: 9u); // ProgressCounter + 5
|
||||
|
||||
ContractSnapshot snapshot = Assert.Single(ContractPluginProjection.Project(
|
||||
state.View, Catalog(0x10u, "Tusker Hunt", "%d/20 Tuskers"), Now));
|
||||
|
||||
Assert.Equal(0x10u, snapshot.ContractId);
|
||||
Assert.Equal(9u, snapshot.Stage);
|
||||
Assert.Equal(5u, snapshot.Progress);
|
||||
Assert.Equal("Tusker Hunt", snapshot.Name);
|
||||
Assert.Equal("Do the thing.", snapshot.Description);
|
||||
Assert.Equal("5/20 Tuskers", snapshot.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheDisplayContractIsFlagged()
|
||||
{
|
||||
using var state = new RuntimeContractState();
|
||||
Track(state, 0x10u, stage: 2u);
|
||||
Track(state, 0x20u, stage: 2u, setAsDisplay: true);
|
||||
|
||||
IReadOnlyList<ContractSnapshot> projected = ContractPluginProjection.Project(
|
||||
state.View, ContractCatalog.Empty, Now);
|
||||
|
||||
Assert.False(projected.Single(c => c.ContractId == 0x10u).IsDisplayed);
|
||||
Assert.True(projected.Single(c => c.ContractId == 0x20u).IsDisplayed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithNoCatalogTheNumbersStillProject()
|
||||
{
|
||||
// A headless bot has no dat access. Losing the text is expected;
|
||||
// losing the QUEST would mean a bot silently unable to see what it is
|
||||
// on, which is the failure this rules out.
|
||||
using var state = new RuntimeContractState();
|
||||
Track(state, 0x10u, stage: 6u);
|
||||
|
||||
ContractSnapshot snapshot = Assert.Single(
|
||||
ContractPluginProjection.Project(state.View, catalog: null, Now));
|
||||
|
||||
Assert.Equal(0x10u, snapshot.ContractId);
|
||||
Assert.Equal(6u, snapshot.Stage);
|
||||
Assert.Equal(2u, snapshot.Progress);
|
||||
Assert.Equal(string.Empty, snapshot.Name);
|
||||
Assert.Equal(string.Empty, snapshot.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AContractTheCatalogDoesNotKnowStillProjects()
|
||||
{
|
||||
// Same rule as the panel: the server can track a contract this dat
|
||||
// build has never heard of, and a plugin must not miss it.
|
||||
using var state = new RuntimeContractState();
|
||||
Track(state, 0xDEADu, stage: 2u);
|
||||
|
||||
ContractSnapshot snapshot = Assert.Single(ContractPluginProjection.Project(
|
||||
state.View, Catalog(0x10u, "Something Else"), Now));
|
||||
|
||||
Assert.Equal(0xDEADu, snapshot.ContractId);
|
||||
Assert.Equal(string.Empty, snapshot.Name);
|
||||
// ContractEntry.Unknown still runs the progress arms, so an in-progress
|
||||
// contract reads correctly even with no authored text.
|
||||
Assert.Equal("In Progress", snapshot.Status);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TheProjectionOrderMatchesTheTrackersOwn()
|
||||
{
|
||||
using var state = new RuntimeContractState();
|
||||
Track(state, 0x30u, stage: 2u);
|
||||
Track(state, 0x10u, stage: 2u);
|
||||
Track(state, 0x20u, stage: 2u);
|
||||
|
||||
IReadOnlyList<ContractSnapshot> projected = ContractPluginProjection.Project(
|
||||
state.View, ContractCatalog.Empty, Now);
|
||||
|
||||
Assert.Equal(
|
||||
new uint[] { 0x10u, 0x20u, 0x30u },
|
||||
projected.Select(c => c.ContractId).ToArray());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue