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

@ -31,7 +31,15 @@ internal static class FeatureCatalog
features.Add(new VitalsStream(context));
features.Add(new SpawnPortalStream(context));
features.Add(new BotControl(context));
features.Add(new CharacterStatsStream(context));
var characterStats = new CharacterStatsStream(context)
{
CurrentTitle = () => HostReadings.CurrentTitle(context.Automation.Character),
Titles = () => HostReadings.Titles(context.Automation.Character),
Allegiance = () => HostReadings.Allegiance(context.Automation),
};
features.Add(characterStats);
context.Vitae = () => HostReadings.VitaePercent(context.Automation.Character.VitaeMultiplier);
context.MetaState = () => HostReadings.MetaState(context.Host);
var mainWindow = new Views.MainWindow(context, stats);
features.Add(mainWindow);
session.AddConnectionListener(telemetry);
@ -44,7 +52,7 @@ internal static class FeatureCatalog
features.Add(new Sharing.VitalSharingOverlay(context, sharing));
// The radar the backend turns on and off.
var radar = new Radar.RadarStream(context, cells: null);
var radar = new Radar.RadarStream(context, new HostDungeonCells(context.Automation));
features.Add(radar);
session.RadarToggle = on =>
{
@ -62,9 +70,21 @@ internal static class FeatureCatalog
var metas = new MetaSyncFeature(context);
features.Add(metas);
if (flagTracker.Panel is { } trackerPanel)
mainWindow.FlagTrackerOpener = () => trackerPanel.IsVisible = true;
{
mainWindow.FlagTrackerOpener = () =>
{
trackerPanel.IsVisible = true;
context.Host.Ui.ShowPanel("flagtracker");
};
}
if (metas.Panel is { } metasPanel)
mainWindow.MetasOpener = () => metasPanel.IsVisible = true;
{
mainWindow.MetasOpener = () =>
{
metasPanel.IsVisible = true;
context.Host.Ui.ShowPanel("metas");
};
}
// Inventory: full_inventory, inventory_delta, equipment_cantrip_state,
// and the taper count telemetry reads (InventoryFeature.PrismaticTaperCount).

View file

@ -0,0 +1,88 @@
using AcDream.Plugin.Abstractions;
using OpenAC.MosswartMassacre.Radar;
using OpenAC.MosswartMassacre.Streams;
namespace OpenAC.MosswartMassacre;
/// <summary>
/// The values the original read from its client library that the host now
/// reports, turned into the numbers the original put on the wire.
/// </summary>
internal static class HostReadings
{
/// <summary>The bot plugin whose meta state the telemetry frame carries.</summary>
public const string BotPluginId = "acdream.mosstank";
/// <summary>The status line the bot publishes its current meta state under.</summary>
public const string MetaStateKey = "metaState";
/// <summary>
/// The vitae penalty in percent as the original's client library computed
/// it: the vitae multiplier as a float, nudged up by a ten-millionth
/// against float error, then <c>(long)((1.00 - v) * 100 + 0.5f)</c>. With
/// no vitae the multiplier is exactly 1 and the penalty 0.
/// </summary>
public static int VitaePercent(float multiplier)
{
if (!float.IsFinite(multiplier) || multiplier >= 1f)
return 0;
float vitae = multiplier + 0.0000001f;
return (int)(long)(((1.00 - vitae) * 100) + 0.5f);
}
/// <summary>The bot's current meta state, or empty when no bot publishes one.</summary>
public static string MetaState(IPluginHost host) =>
host.StatusBoard.TryRead(BotPluginId, MetaStateKey, out string value) ? value : string.Empty;
/// <summary>The allegiance part of the character-stats frame, once the server has described it.</summary>
public static AllegianceSummary? Allegiance(IAutomationSurface automation)
{
if (!automation.Allegiance.IsAvailable)
return null;
PluginAllegianceSnapshot snapshot = automation.Allegiance.Snapshot;
if (!snapshot.IsKnown)
return null;
return new AllegianceSummary(
snapshot.Name ?? string.Empty,
Member(snapshot.Monarch),
Member(snapshot.Patron),
(int)snapshot.Rank,
(int)snapshot.VassalCount);
}
private static AllegianceMember? Member(PluginAllegianceMember? member) =>
member is { } m && m.Name is not null
? new AllegianceMember(m.Name, m.HeritageGroup, (int)m.Rank, m.Gender)
: null;
/// <summary>The character's titles by name, in the server's order; null before the server has listed them.</summary>
public static IReadOnlyList<string>? Titles(ICharacterInfo character)
{
IReadOnlyList<PluginCharacterTitle> titles = character.Titles;
return titles.Count == 0 ? null : [.. titles.Select(static title => title.Name)];
}
/// <summary>The current title id once the server has listed titles; null before.</summary>
public static int? CurrentTitle(ICharacterInfo character) =>
character.Titles.Count == 0 ? null : unchecked((int)character.CurrentTitleId);
}
/// <summary>A landblock's indoor cells from the host's game data.</summary>
internal sealed class HostDungeonCells(IAutomationSurface automation) : IDungeonCellSource
{
public IReadOnlyList<DungeonCellRecord>? ReadCells(uint landblock)
{
IReadOnlyList<PluginIndoorCell> cells = automation.DungeonMap.CaptureIndoorCells(landblock);
if (cells.Count == 0)
return null;
return
[
.. cells.Select(static cell => new DungeonCellRecord(
(ushort)cell.EnvironmentId,
cell.Origin.X,
cell.Origin.Y,
cell.Origin.Z,
cell.Orientation.W)),
];
}
}