feat(bridge): protocol 6 — an idempotency key, and champ.boss.killed (Phase 11a)
A command carrying an `idempotencyKey` is now executed at most once: a repeat is answered with the original reply rather than re-run. That is the precondition every world verb in Phase 12 is waiting on, and it is what let `uo.broadcast` stop being un-retryable. The gate sits in BridgeBoot's inbound dispatch, not in each handler, so it covers every kind including ones a later protocol adds. A command with no key behaves exactly as it did before, which leaves the admin screens unchanged. Four rules, each a decision rather than an implementation detail: reserve on receipt (so a handler that defers is covered, answering `bridge.busy` to a repeat in flight); a key that has begun is never released, not even when the handler throws; a replay is stamped with the REPEAT's correlation id, because the sidecar's reqId is fresh per call and replaying the original would hang the retry; and the bound is loud, because an evicted key is the guarantee's one hole. `champ.boss.killed` rides along because a bump costs a release, a bundle and an operator update on every shard. It fires from EventSink.CreatureDeath, detected by type so a boss that popped and died inside one sweep is still reported, and it carries the damage table that exists at the death and nowhere else. overlay.toml protocol = 6, in this commit rather than a later one. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,53 @@ namespace Server.Custom.Bridge
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A named array of actor objects each carrying a damage total — a boss kill's damage
|
||||
/// table (Protocol 6), and the first actor array whose entries are ranked rather than
|
||||
/// merely listed.
|
||||
///
|
||||
/// The pairs are written in the order given, so the CALLER owns the sort. That is
|
||||
/// deliberate: "the top damagers" is a judgement about a fight, and the shard's job is
|
||||
/// to report the numbers it holds rather than to decide what counts as a contribution.
|
||||
///
|
||||
/// Each entry is the standard actor object plus `damage`, which means it carries `acct`
|
||||
/// and `webId` and is therefore governed by the website's locked-field rule exactly as
|
||||
/// every other actor is. A shard that considers the whole table too revealing hides it
|
||||
/// with one field rule rather than by dropping the kind.
|
||||
/// </summary>
|
||||
public static StringBuilder Damagers(
|
||||
this StringBuilder sb, string name, IList<KeyValuePair<Mobile, int>> pairs, int count)
|
||||
{
|
||||
sb.Append(",\"").Append(name).Append("\":[");
|
||||
|
||||
if (pairs != null)
|
||||
{
|
||||
var end = Math.Min(count, pairs.Count);
|
||||
bool first = true;
|
||||
|
||||
for (int i = 0; i < end; i++)
|
||||
{
|
||||
var m = pairs[i].Key;
|
||||
|
||||
if (m == null)
|
||||
continue;
|
||||
|
||||
if (!first)
|
||||
sb.Append(',');
|
||||
|
||||
sb.Append('{');
|
||||
WriteActorFields(sb, m);
|
||||
sb.Append(",\"damage\":").Append(pairs[i].Value);
|
||||
sb.Append('}');
|
||||
|
||||
first = false;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append(']');
|
||||
return sb;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A roster member: the standard actor object plus the member's rank in their guild.
|
||||
///
|
||||
@@ -289,6 +336,75 @@ namespace Server.Custom.Bridge
|
||||
sb.Append('"');
|
||||
}
|
||||
|
||||
// ---- rewriting an already-built line (protocol 6) ----
|
||||
//
|
||||
// BridgeIdempotency replays a stored reply under the REPEAT's correlation id. It could
|
||||
// parse the line, edit the dictionary and re-serialize, but a round trip through
|
||||
// JavaScriptSerializer would silently renormalise every number and string in a reply this
|
||||
// file went to the trouble of writing by hand. These two edit the text instead, so a
|
||||
// replayed reply is byte-for-byte the original apart from the field that had to change.
|
||||
|
||||
/// <summary>
|
||||
/// Replaces the value of a top-level STRING field, honouring backslash escapes when
|
||||
/// finding the value's end. Returns null if the field is not present as a string —
|
||||
/// never a half-rewritten line.
|
||||
/// </summary>
|
||||
public static string RewriteStringField(string line, string name, string value)
|
||||
{
|
||||
if (line == null || name == null || value == null)
|
||||
return null;
|
||||
|
||||
// The leading comma is part of the needle: every top-level field is written by Str()
|
||||
// after Begin() has already emitted `t` and `kind`, so a real one always has one. It
|
||||
// is the cheapest thing that stops the search matching the same text inside a value.
|
||||
var needle = ",\"" + name + "\":\"";
|
||||
int at = line.IndexOf(needle, StringComparison.Ordinal);
|
||||
|
||||
if (at < 0)
|
||||
return null;
|
||||
|
||||
int valueStart = at + needle.Length;
|
||||
int i = valueStart;
|
||||
|
||||
while (i < line.Length)
|
||||
{
|
||||
char c = line[i];
|
||||
|
||||
if (c == '\\')
|
||||
{
|
||||
i += 2; // an escape consumes the next character, whatever it is
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
break;
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i >= line.Length)
|
||||
return null; // unterminated: refuse rather than guess
|
||||
|
||||
var sb = new StringBuilder(line.Length + value.Length);
|
||||
sb.Append(line, 0, valueStart - 1); // up to and excluding the opening quote
|
||||
Escape(sb, value);
|
||||
sb.Append(line, i + 1, line.Length - i - 1);
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends `"name":true` to an already-closed object. Returns the line unchanged if it
|
||||
/// is not one, so a malformed reply is passed through rather than corrupted further.
|
||||
/// </summary>
|
||||
public static string WithTrueFlag(string line, string name)
|
||||
{
|
||||
if (String.IsNullOrEmpty(line) || line[line.Length - 1] != '}')
|
||||
return line;
|
||||
|
||||
return line.Substring(0, line.Length - 1) + ",\"" + name + "\":true}";
|
||||
}
|
||||
|
||||
// ---- inbound ----
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user