diff --git a/overlay/oxide/plugins/RunicGateway.cs b/overlay/oxide/plugins/RunicGateway.cs index d9c8f63..3ffa50b 100644 --- a/overlay/oxide/plugins/RunicGateway.cs +++ b/overlay/oxide/plugins/RunicGateway.cs @@ -1372,8 +1372,10 @@ namespace Oxide.Plugins // The map's sets are seeded here for the same reason: a hotload hears no spawns. SeedMapSets(); - // Hand BetterChat the title getter, if it is here. When it loads later, or reloads and - // forgets us, OnPluginLoaded does this again (§33.1). + // The titles this plugin last held, if they are this wipe's (D144), and then the getter + // handed to BetterChat, if it is here. When it loads later, or reloads and forgets us, + // OnPluginLoaded does this again (§33.1). + LoadTitles(); RegisterTitles("load"); // Started here rather than in Init: this hook fires both at the end of server startup @@ -8459,8 +8461,15 @@ namespace Oxide.Plugins /// /// The chat titles players have earned on this server, by Steam id, already in /// BetterChat's markup. The website composes them from the wipe's standings and pushes - /// the whole set (titles.set); nothing here is persisted, because the site sends - /// it again on every boot it has not sent to. + /// the whole set (titles.set). + /// + /// + /// Kept on disk as well (D144). The site re-sends on a new boot id or wipe id, and + /// a hot reload of this plugin — an update, an oxide.reload — is neither: the boot + /// id names the server process. Without the file a reload empties every title until the + /// standings next move. A file from another wipe is not loaded, because titles rank the + /// wipe they were earned on. + /// /// /// /// Swapped, never edited. BetterChat reads this through on @@ -8598,6 +8607,7 @@ namespace Oxide.Plugins } _titles = next; + SaveTitles(); // A BetterChat that arrived without us hearing it load is picked up here, which also // makes a restart of either plugin heal on the site's next push. @@ -8622,6 +8632,65 @@ namespace Oxide.Plugins EmitFrame(reply); } + /// What titles.json holds: the set, and the wipe it was pushed on. + private sealed class TitleFile + { + public string WipeId; + public Dictionary Titles = new Dictionary(); + } + + private string TitlesPath() + { + return Path.Combine(Interface.Oxide.DataDirectory, Name, "titles.json"); + } + + /// Main thread. Written on every set the site pushes — a set arrives only when it changed. + private void SaveTitles() + { + var file = new TitleFile { WipeId = _wipeId }; + + foreach (KeyValuePair kv in _titles) + file.Titles[kv.Key.ToString()] = kv.Value; + + WriteDataFile(TitlesPath(), file, "titles"); + } + + /// + /// Main thread, from OnServerInitialized — the first moment the wipe id is real. + /// A file from another wipe, or with no wipe, is left unread: the site sends this wipe's. + /// + private void LoadTitles() + { + try + { + string path = TitlesPath(); + + if (!File.Exists(path)) + return; + + var file = JsonConvert.DeserializeObject(File.ReadAllText(path)); + + if (file == null || file.Titles == null || string.IsNullOrEmpty(_wipeId) || file.WipeId != _wipeId) + return; + + var loaded = new Dictionary(); + + foreach (KeyValuePair kv in file.Titles) + { + ulong id; + if (ulong.TryParse(kv.Key, out id) && !string.IsNullOrEmpty(kv.Value)) + loaded[id] = kv.Value; + } + + _titles = loaded; + _lastTitlesSummary = $"{loaded.Count} titles from disk, at={DateTime.UtcNow:HH:mm:ss}Z"; + } + catch (Exception ex) + { + Puts($"could not read the titles: {Flatten(ex.Message)}"); + } + } + private void EmitTitlesError(string reqId, string reason, string message) { Dictionary frame = Frame("titles.error", "reply");