namespace Aeshnidae.Mule; /// /// /mule-data [page] - the vault as the Aelrynth plugins read it (RynthInventory's Global /// Inventory shows it with icons). The same contents /mule list prints, one chat line per /// page in the CompanionFeed shape, so a player without the plugin loses nothing and the /// plugin can do nothing a player could not type. /// /// ~ael1 {"t":"mule","page":1,"pages":3,"count":57,"max":1000,"items":[...]} /// /// An item is {"o":objectId,"w":wcid,"n":name,"i":icon,"u":underlay,"v":overlay, /// "s":stack,"m":maxStack,"y":itemType,"e":uiEffects}. Read-only: nothing here moves or /// loads anything, and a vault still loading answers {"t":"mule","ready":false}, never an /// empty list (the same rule every /mule verb follows). /// public static class MuleData { // A chat line must stay well inside one network message; ~130 bytes an item. public const int PerPage = 25; [CommandHandler("mule-data", AccessLevel.Player, CommandHandlerFlag.RequiresWorld, -1, "The vault as data, one page a line, for the Aelrynth plugins. /mule list is the human version.", "/mule-data [page]")] public static void Handle(Session session, params string[] parameters) { var player = session?.Player; if (player is null) return; if (!Mod.Settings.Enabled || !MuleDb.Ready) { CompanionFeed.Send(session, new { t = "mule", ready = false, why = "closed" }); return; } try { var accountId = player.Account?.AccountId ?? 0; var handle = Vault.Get(accountId, out _); if (handle is null || !handle.Ready || handle.Fault.Length > 0) { CompanionFeed.Send(session, new { t = "mule", ready = false, why = handle?.Fault.Length > 0 ? "fault" : "loading" }); return; } var page = parameters is { Length: > 0 } && int.TryParse(parameters[0], out var p) && p > 0 ? p : 1; // The order /mule list uses: newest first (later links, then later placement). List all; lock (Vault.Gate(accountId)) { all = handle.Links .Select((link, seq) => (link, seq)) .SelectMany(x => x.link.Inventory.Values.Select(i => (item: i, x.seq, pos: i.PlacementPosition ?? 0))) .OrderByDescending(x => x.seq) .ThenByDescending(x => x.pos) .Select(x => x.item) .ToList(); } var pages = Math.Max(1, (all.Count + PerPage - 1) / PerPage); page = Math.Clamp(page, 1, pages); var items = all.Skip((page - 1) * PerPage).Take(PerPage).Select(i => new { o = i.Guid.Full, w = i.WeenieClassId, n = i.Name, i = i.IconId, u = i.IconUnderlayId == 0 ? (uint?)null : i.IconUnderlayId, v = i.IconOverlayId == 0 ? (uint?)null : i.IconOverlayId, s = i.StackSize is > 1 ? i.StackSize : null, m = i.MaxStackSize is > 1 ? i.MaxStackSize : null, y = (uint)(i.ItemType), e = i.UiEffects is { } fx && (uint)fx != 0 ? (uint?)fx : null, }).ToList(); CompanionFeed.Send(session, new { t = "mule", ready = true, page, pages, count = all.Count, max = Mod.Settings.MaxItems, items }); } catch (Exception ex) { ModManager.Log($"[{Mod.Name}] /mule-data failed for {player.Name}: {ex}", ModManager.LogLevel.Error); CompanionFeed.Send(session, new { t = "mule", ready = false, why = "error" }); } } }