feat(bridge): protocol 12 — BetterChat titles and styles, popups (phase 17) #12

Merged
whitlocktech merged 2 commits from feat/phase-17-integrations into edge 2026-09-25 18:31:04 -05:00
Showing only changes of commit 5ff88610a5 - Show all commits

View File

@@ -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
/// <summary>
/// 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 (<c>titles.set</c>); nothing here is persisted, because the site sends
/// it again on every boot it has not sent to.
/// the whole set (<c>titles.set</c>).
///
/// <para>
/// <b>Kept on disk as well (D144).</b> The site re-sends on a new boot id or wipe id, and
/// a hot reload of this plugin — an update, an <c>oxide.reload</c> — 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.
/// </para>
///
/// <para>
/// <b>Swapped, never edited.</b> BetterChat reads this through <see cref="TitleFor"/> 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);
}
/// <summary>What <c>titles.json</c> holds: the set, and the wipe it was pushed on.</summary>
private sealed class TitleFile
{
public string WipeId;
public Dictionary<string, string> Titles = new Dictionary<string, string>();
}
private string TitlesPath()
{
return Path.Combine(Interface.Oxide.DataDirectory, Name, "titles.json");
}
/// <summary>Main thread. Written on every set the site pushes — a set arrives only when it changed.</summary>
private void SaveTitles()
{
var file = new TitleFile { WipeId = _wipeId };
foreach (KeyValuePair<ulong, string> kv in _titles)
file.Titles[kv.Key.ToString()] = kv.Value;
WriteDataFile(TitlesPath(), file, "titles");
}
/// <summary>
/// Main thread, from <c>OnServerInitialized</c> — 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.
/// </summary>
private void LoadTitles()
{
try
{
string path = TitlesPath();
if (!File.Exists(path))
return;
var file = JsonConvert.DeserializeObject<TitleFile>(File.ReadAllText(path));
if (file == null || file.Titles == null || string.IsNullOrEmpty(_wipeId) || file.WipeId != _wipeId)
return;
var loaded = new Dictionary<ulong, string>();
foreach (KeyValuePair<string, string> 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<string, object> frame = Frame("titles.error", "reply");