Wire the host's new readings into the frames

vitals.vitae is the penalty percent computed exactly as the original's client
library did from the vitae multiplier; character_stats carries the titles (by
name, server order), the current title id and the allegiance (monarch,
patron, rank, followers); telemetry.vt_state reads the bot's published meta
state; dungeon_map reads the landblock's indoor cells; the main window's
Tools buttons reopen the Mossy Tracker and Metas windows even after the
player closed them.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-25 13:00:03 +02:00
parent 50a173ef25
commit 9b5371c2e4
6 changed files with 223 additions and 4 deletions

View file

@ -44,6 +44,8 @@ public sealed class FakeAutomation : IAutomationSurface
public FakeDungeonMap DungeonMap { get; } = new();
public FakeAllegiance Allegiance { get; } = new();
ICharacterInfo IAutomationSurface.Character => Character;
ISpellCatalog IAutomationSurface.Spells => Spells;
IMagicCommands IAutomationSurface.Magic => Magic;
@ -61,6 +63,7 @@ public sealed class FakeAutomation : IAutomationSurface
ILoginAutomation IAutomationSurface.Login => Login;
INetworkAutomation IAutomationSurface.Network => Network;
IDungeonMapAutomation IAutomationSurface.DungeonMap => DungeonMap;
IAllegianceAutomation IAutomationSurface.Allegiance => Allegiance;
}
/// <summary>A settable <see cref="ITradeAutomation"/>: a test drives <see cref="Raise*"/> and inspects <see cref="Calls"/>.</summary>
@ -310,6 +313,9 @@ public sealed class FakeDialogAutomation : IDialogAutomation
public sealed class FakeCharacter : ICharacterInfo
{
public float VitaeMultiplier { get; set; } = 1f;
public uint CurrentTitleId { get; set; }
public IReadOnlyList<PluginCharacterTitle> Titles { get; set; } = [];
public bool IsInWorld { get; set; }
public int Level { get; set; }
public IReadOnlyList<PluginVitalInfo> Vitals { get; set; } = [];

View file

@ -34,6 +34,10 @@ public sealed class FakeHost : IPluginHost
public FakeHostWindow Window { get; } = new();
public FakeStatusBoard StatusBoard { get; } = new();
IPluginStatusBoard IPluginHost.StatusBoard => StatusBoard;
IEvents IPluginHost.Events => Events;
ISelectionService IPluginHost.Selection => Selection;

View file

@ -52,3 +52,31 @@ public sealed class FakeDungeonMap : IDungeonMapAutomation
public bool IsSealedDungeon(uint cellId) =>
(cellId & 0xFFFFu) >= 0x100u && SealedLandblocks.Contains(cellId & 0xFFFF0000u);
}
/// <summary>An allegiance surface a test fills in.</summary>
public sealed class FakeAllegiance : IAllegianceAutomation
{
public bool IsAvailable { get; set; } = true;
public PluginAllegianceSnapshot Snapshot { get; set; }
}
/// <summary>A status board whose lines a test sets.</summary>
public sealed class FakeStatusBoard : IPluginStatusBoard
{
public Dictionary<(string PluginId, string Key), string> Lines { get; } = [];
public bool IsAvailable => true;
public bool Publish(string key, string? value)
{
if (value is null)
Lines.Remove(("openac.mosswartmassacre", key));
else
Lines[("openac.mosswartmassacre", key)] = value;
return true;
}
public bool TryRead(string pluginId, string key, out string value) =>
Lines.TryGetValue((pluginId, key), out value!);
}

View file

@ -0,0 +1,73 @@
using AcDream.Plugin.Abstractions;
namespace OpenAC.MosswartMassacre.Tests;
public sealed class HostReadingsTests
{
[Theory]
// The original's (long)((1.00 - (v + 0.0000001f)) * 100 + 0.5f).
[InlineData(1f, 0)]
[InlineData(0.95f, 5)]
[InlineData(0.9f, 10)]
[InlineData(0.6f, 40)]
// 0.995 plus the nudge is just over 0.995 as a float: the rounding lands on 0.
[InlineData(0.995f, 0)]
public void Vitae_is_the_penalty_in_percent_as_the_original_rounded_it(float multiplier, int expected) =>
Assert.Equal(expected, HostReadings.VitaePercent(multiplier));
[Fact]
public void The_vitals_frame_carries_the_vitae_penalty()
{
using var harness = new PluginHarness("Mossy");
harness.Host.Automation.Character.VitaeMultiplier = 0.95f;
harness.Host.Automation.Character.MaxHealth = 10;
harness.Login();
harness.Frames.Clear();
harness.Feature<OpenAC.MosswartMassacre.Streams.VitalsStream>().SendNow();
Assert.EndsWith(",\"vitae\":5}", Assert.Single(harness.Frames), StringComparison.Ordinal);
}
[Fact]
public void Titles_are_null_until_listed_then_names_in_order_and_the_current_id()
{
var character = new Fakes.FakeCharacter();
Assert.Null(HostReadings.Titles(character));
Assert.Null(HostReadings.CurrentTitle(character));
character.Titles = [new PluginCharacterTitle(7, "Adventurer"), new PluginCharacterTitle(3, "Sawato Resident")];
character.CurrentTitleId = 3;
Assert.Equal(["Adventurer", "Sawato Resident"], HostReadings.Titles(character));
Assert.Equal(3, HostReadings.CurrentTitle(character));
}
[Fact]
public void Allegiance_maps_monarch_patron_rank_and_followers()
{
var automation = new Fakes.FakeAutomation();
Assert.Null(HostReadings.Allegiance(automation));
automation.Allegiance.Snapshot = new PluginAllegianceSnapshot(1, true, "Mossy Club", 3u, 40u, 17u, 5u)
{
Monarch = new PluginAllegianceMember(5u, "King", 8u, 200u, 1, 2, true),
};
var summary = HostReadings.Allegiance(automation)!.Value;
Assert.Equal("Mossy Club", summary.Name);
Assert.Equal(new OpenAC.MosswartMassacre.Streams.AllegianceMember("King", 1, 8, 2), summary.Monarch);
Assert.Null(summary.Patron);
Assert.Equal(3, summary.Rank);
Assert.Equal(17, summary.Followers);
}
[Fact]
public void Meta_state_comes_from_the_bots_status_line()
{
var host = new Fakes.FakeHost();
Assert.Equal(string.Empty, HostReadings.MetaState(host));
host.StatusBoard.Lines[("acdream.mosstank", "metaState")] = "Combat";
Assert.Equal("Combat", HostReadings.MetaState(host));
}
}