using System;
using System.Collections.Generic;
using System.Text;
using Server.Accounting;
using Server.Engines.Help;
namespace Server.Custom.Bridge
{
///
/// The in-game help-page (support ticket) queue, surfaced to the website.
///
/// A player who uses the Help button creates a — sender, message,
/// type, location, and (once a staffer claims it) a handler. The queue lives in memory with
/// no EventSink, so — like the sweeps in — it is polled and diffed:
/// a page appearing emits page.new, one leaving emits page.closed, and a
/// handled-state change emits page.updated. The whole open queue is also available on
/// demand via the pages.snapshot request (the backfill a dashboard uses on connect).
///
/// A page is keyed by its sender's serial: the queue enforces one page per sender
/// (PageQueue.Contains), so the sender serial is a stable page id.
///
/// Inbound page.respond delivers a message to the player exactly as an in-game staff
/// response does (online: a gump now; offline: queued for next login), optionally closing the
/// page; page.close just removes it. Both run on the Core thread.
///
public static class BridgePages
{
private static readonly DateTime Epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
private static Timer _timer;
private static long _sweeps, _new, _closed, _updated;
private struct Seen
{
public long SentMs;
public bool Handled;
}
// sender serial -> last-seen page identity. Core-thread only.
private static readonly Dictionary _seen = new Dictionary();
public static void Initialize()
{
if (!BridgeConfig.Enabled)
return;
BridgeBoot.RegisterHandler("pages.snapshot", OnSnapshot);
BridgeBoot.RegisterHandler("page.respond", OnRespond);
BridgeBoot.RegisterHandler("page.close", OnClose);
EventSink.ServerStarted += OnServerStarted;
}
private static void OnServerStarted()
{
Baseline();
Rearm();
}
/// Stops and recreates the poll timer from current config. Called by `[bridge reload`.
public static void Rearm()
{
if (_timer != null)
{
_timer.Stop();
_timer = null;
}
var iv = TimeSpan.FromSeconds(BridgeConfig.PageSweepSeconds);
_timer = Timer.DelayCall(iv, iv, Sweep);
}
public static string Status()
{
return String.Format(
"pages(sweeps={0} new={1} closed={2} updated={3} open={4})",
_sweeps, _new, _closed, _updated, _seen.Count);
}
/// Seeds _seen from the current queue without emitting, so a restart/reload does not
/// re-announce pages already open.
private static void Baseline()
{
_seen.Clear();
foreach (PageEntry e in PageQueue.List)
{
if (e == null || e.Sender == null)
continue;
_seen[e.Sender.Serial.Value] = new Seen { SentMs = ToMs(e.Sent), Handled = e.Handler != null };
}
}
// ---- poll ----
private static void Sweep()
{
try
{
_sweeps++;
var cur = new Dictionary();
foreach (PageEntry e in PageQueue.List)
{
if (e == null || e.Sender == null)
continue;
cur[e.Sender.Serial.Value] = e;
}
// Closed: keys in _seen no longer present.
if (_seen.Count > 0)
{
List gone = null;
foreach (var kv in _seen)
{
if (!cur.ContainsKey(kv.Key))
{
if (gone == null)
gone = new List();
gone.Add(kv.Key);
}
}
if (gone != null)
{
foreach (var id in gone)
{
EmitClosed(id);
_seen.Remove(id);
}
}
}
// New / replaced / handled-state changed.
foreach (var kv in cur)
{
var e = kv.Value;
long sentMs = ToMs(e.Sent);
bool handled = e.Handler != null;
Seen prev;
if (!_seen.TryGetValue(kv.Key, out prev))
{
EmitNew(e);
}
else if (prev.SentMs != sentMs)
{
// Same sender, different page (they cancelled and re-paged within a tick).
EmitClosed(kv.Key);
EmitNew(e);
}
else if (prev.Handled != handled)
{
EmitUpdated(e);
}
_seen[kv.Key] = new Seen { SentMs = sentMs, Handled = handled };
}
}
catch (Exception ex)
{
Console.WriteLine("[Bridge] page sweep threw: {0}", ex.Message);
}
}
// ---- outbound ----
private static void EmitNew(PageEntry e)
{
_new++;
var sb = BridgeJson.Begin("page.new").Str("pageId", PageId(e));
AppendPageTail(sb, e);
BridgeLink.Emit(sb.End());
}
private static void EmitUpdated(PageEntry e)
{
_updated++;
var sb = BridgeJson.Begin("page.updated").Str("pageId", PageId(e));
AppendPageTail(sb, e);
BridgeLink.Emit(sb.End());
}
private static void EmitClosed(int serial)
{
_closed++;
BridgeLink.Emit(BridgeJson.Begin("page.closed")
.Str("pageId", "0x" + serial.ToString("X"))
.End());
}
private static void OnSnapshot(Dictionary o)
{
var reqId = BridgeJson.GetString(o, "reqId");
var sb = BridgeJson.Begin("pages.list");
if (reqId != null)
sb.Str("reqId", reqId);
sb.Append(",\"pages\":[");
bool first = true;
foreach (PageEntry e in PageQueue.List)
{
if (e == null || e.Sender == null)
continue;
if (!first)
sb.Append(',');
first = false;
sb.Append("{\"pageId\":\"0x").Append(e.Sender.Serial.Value.ToString("X")).Append('"');
AppendPageTail(sb, e);
sb.Append('}');
}
sb.Append(']');
BridgeLink.Emit(sb.End());
}
/// Appends every page field except the opening pageId, each comma-prefixed, so it
/// works both after Begin(...) (events) and after a manual `{"pageId":..` (snapshot array).
private static void AppendPageTail(StringBuilder sb, PageEntry e)
{
sb.Append(",\"sender\":");
WriteSender(sb, e.Sender);
sb.Str("type", e.Type.ToString());
sb.Str("message", e.Message ?? "");
sb.Str("map", e.PageMap == null ? null : e.PageMap.Name);
sb.Num("x", e.PageLocation.X);
sb.Num("y", e.PageLocation.Y);
sb.Num("z", e.PageLocation.Z);
sb.Num("sentMs", ToMs(e.Sent));
sb.Bool("handled", e.Handler != null);
if (e.Handler != null)
sb.Str("handler", e.Handler.Name);
}
private static void WriteSender(StringBuilder sb, Mobile m)
{
if (m == null)
{
sb.Append("null");
return;
}
sb.Append("{\"serial\":\"0x").Append(m.Serial.Value.ToString("X")).Append('"');
sb.Append(",\"name\":");
BridgeJson.Escape(sb, m.Name ?? "");
var acct = m.Account as Account;
if (acct != null)
{
sb.Append(",\"acct\":");
BridgeJson.Escape(sb, acct.Username);
var webId = BridgeAccountLink.WebIdFor(acct);
if (webId != null)
{
sb.Append(",\"webId\":");
BridgeJson.Escape(sb, webId);
}
}
sb.Append('}');
}
// ---- inbound ----
/// page.respond {reqId, pageId, message, close?}. Delivers a staff response to the
/// player and optionally closes the page.
private static void OnRespond(Dictionary o)
{
var reqId = BridgeJson.GetString(o, "reqId");
var pageId = BridgeJson.GetString(o, "pageId");
var message = BridgeJson.GetString(o, "message");
bool close = GetBool(o, "close");
if (String.IsNullOrEmpty(message))
{
Err(reqId, "respond", pageId, "missing message");
return;
}
var e = Find(pageId);
if (e == null)
{
Err(reqId, "respond", pageId, "unknown page");
return;
}
try
{
// Same delivery as an in-game staff response: a null handler shows as "Staff".
// ResponseEntry queues for an offline sender; SendGump delivers now if online.
var re = new ResponseEntry(e.Sender, null, message);
re.SendGump();
if (close)
PageQueue.Remove(e);
Ok(reqId, "respond", pageId, close);
}
catch (Exception ex)
{
Console.WriteLine("[Bridge] page.respond threw: {0}", ex.Message);
Err(reqId, "respond", pageId, "internal error");
}
}
/// page.close {reqId, pageId}. Removes the page from the queue.
private static void OnClose(Dictionary o)
{
var reqId = BridgeJson.GetString(o, "reqId");
var pageId = BridgeJson.GetString(o, "pageId");
var e = Find(pageId);
if (e == null)
{
Err(reqId, "close", pageId, "unknown page");
return;
}
try
{
PageQueue.Remove(e);
Ok(reqId, "close", pageId, true);
}
catch (Exception ex)
{
Console.WriteLine("[Bridge] page.close threw: {0}", ex.Message);
Err(reqId, "close", pageId, "internal error");
}
}
private static void Ok(string reqId, string action, string pageId, bool closed)
{
var sb = BridgeJson.Begin("page.ok");
if (reqId != null) sb.Str("reqId", reqId);
sb.Str("action", action);
if (pageId != null) sb.Str("pageId", pageId);
sb.Bool("closed", closed);
BridgeLink.Emit(sb.End());
}
private static void Err(string reqId, string action, string pageId, string reason)
{
var sb = BridgeJson.Begin("page.error");
if (reqId != null) sb.Str("reqId", reqId);
sb.Str("action", action);
if (pageId != null) sb.Str("pageId", pageId);
sb.Str("reason", reason);
BridgeLink.Emit(sb.End());
}
// ---- helpers ----
private static PageEntry Find(string pageId)
{
int serial;
if (!TryParseSerial(pageId, out serial))
return null;
foreach (PageEntry e in PageQueue.List)
{
if (e != null && e.Sender != null && e.Sender.Serial.Value == serial)
return e;
}
return null;
}
private static string PageId(PageEntry e)
{
return "0x" + e.Sender.Serial.Value.ToString("X");
}
private static long ToMs(DateTime dt)
{
return (long)(dt.ToUniversalTime() - Epoch).TotalMilliseconds;
}
private static bool GetBool(Dictionary o, string key)
{
object v;
if (o != null && o.TryGetValue(key, out v) && v is bool)
return (bool)v;
return false;
}
private static bool TryParseSerial(string s, out int value)
{
value = 0;
if (String.IsNullOrEmpty(s))
return false;
try
{
s = s.Trim();
if (s.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
value = Convert.ToInt32(s.Substring(2), 16);
else
value = Convert.ToInt32(s, 10);
return true;
}
catch
{
return false;
}
}
}
}