namespace Aeshnidae.SkillMastery; /// /// The HUD feed contract, shared between Aeshnidae.Hud and every mod that provides a panel. /// /// A mod cannot reference another mod's types (each loads in its own context, so a /// type shared across the boundary is not the same type), but every mod can read /// ACE's own. So the one piece of state the feed needs - "this session asked for /// panels" - lives on the Player as an ephemeral PropertyBool, and everything else /// is a constant. Providers keep a verbatim copy of this file as HudFeed.cs in /// their own namespace; keep them identical. /// /// The wire contract: one chat message per panel on chat type 0x21, carrying JSON. /// /// { "v":1, "id":"bank", "title":"...", "sub":"...", /// "cols":[{"n":"Currency","w":140}, ...], /// "rows":[{"k":"p","c":["Pyreals","1,234,567","20,000"],"col":"#9BE39B"}, ...], /// "flds":[{"k":"amount","l":"Amount","w":90}, ...], /// "acts":[{"l":"Deposit","c":"/b d {key} {amount}","row":true}, ...] } /// /// Cells are strings, already formatted; the client lays them out and does nothing /// clever with them. A field is a text box the player types into. An action is a /// button; its command is sent to the server as if typed, with {key} replaced by the /// selected row's key when the action needs a row and {} by what was typed. /// So a panel is a table with text boxes and buttons, which is enough for skills and /// the bank alike, and every one of them ships with no client change. The client side /// is Mods\Content\tools\AeshHud. /// /// Why chat type 0x21: it is above every type the stock client's windows know, and /// OpenAC's window filters hide it by default, so the line reaches the plugin without /// being painted into the transcript. Nothing is sent to a session that has not asked /// with /hud on, so a stock client never sees a line of it. /// /// Why an ephemeral property: SetProperty on a PropertyBool listed in /// EphemeralProperties.PropertiesBool goes to an in-memory dictionary on the object, /// never to the biota and never to the shard database, and dies with the Player. That /// is exactly the lifetime the flag wants - a stock client logging in tomorrow must /// not inherit it - and the id is well clear of anything ACE (up to 9010) uses. /// public static class HudFeed { public const int ChatType = 0x21; /// Set on the Player while its session wants panels. Ephemeral - see above. public const PropertyBool Listening = (PropertyBool)9501; /// /// Commands the HUD mod invokes with ["sync"] when a session turns the feed on or /// asks for a refresh: every registered command whose name starts with this. /// A provider registers "hud-<panel>" (Player, RequiresWorld) and sends its /// panel from it if the session is listening. /// public const string ProviderPrefix = "hud-"; // Fully qualified on purpose: this file is copied into mods with different usings. private static readonly System.Text.Json.JsonSerializerOptions Json = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping, DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, }; /// /// Make the flag ephemeral. Must run before the first SetProperty of it, or that /// write lands in the biota and is saved. Idempotent; every mod that touches the /// flag calls this from Initialize so the order mods load in does not matter. /// public static void RegisterProperty() => EphemeralProperties.PropertiesBool.Add(Listening); public static bool IsOn(Player? player) => player?.GetProperty(Listening) == true; public static void Send(Session session, object panel) { var text = System.Text.Json.JsonSerializer.Serialize(panel, Json); session.Network.EnqueueSend(new GameMessageSystemChat(text, (ChatMessageType)ChatType)); } }