feat(bridge)!: protocol 4 — guild rosters and per-member leaves (Teams cutover 1/6) #14
@@ -99,9 +99,15 @@ namespace Server.Custom.Bridge
|
|||||||
/// (Bridge.GuildRosterMembersPerLine). A null entry in the sequence is skipped rather
|
/// (Bridge.GuildRosterMembersPerLine). A null entry in the sequence is skipped rather
|
||||||
/// than written as null, so the array is always a list of real members and a caller can
|
/// than written as null, so the array is always a list of real members and a caller can
|
||||||
/// trust its length.
|
/// trust its length.
|
||||||
|
///
|
||||||
|
/// `withGuildRank` adds each member's guild rank to their object. It is a parameter
|
||||||
|
/// rather than always-on because rank is a property of a mobile's membership of THIS
|
||||||
|
/// guild, not of the mobile — every other actor this bridge writes is a bystander,
|
||||||
|
/// a killer, a governor, and guild rank is meaningless on all of them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static StringBuilder Actors(
|
public static StringBuilder Actors(
|
||||||
this StringBuilder sb, string name, IList<Mobile> mobiles, int start, int count)
|
this StringBuilder sb, string name, IList<Mobile> mobiles, int start, int count,
|
||||||
|
bool withGuildRank = false)
|
||||||
{
|
{
|
||||||
sb.Append(",\"").Append(name).Append("\":[");
|
sb.Append(",\"").Append(name).Append("\":[");
|
||||||
|
|
||||||
@@ -120,7 +126,11 @@ namespace Server.Custom.Bridge
|
|||||||
if (!first)
|
if (!first)
|
||||||
sb.Append(',');
|
sb.Append(',');
|
||||||
|
|
||||||
|
if (withGuildRank)
|
||||||
|
WriteGuildMember(sb, m);
|
||||||
|
else
|
||||||
WriteActor(sb, m);
|
WriteActor(sb, m);
|
||||||
|
|
||||||
first = false;
|
first = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,6 +139,66 @@ namespace Server.Custom.Bridge
|
|||||||
return sb;
|
return sb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A roster member: the standard actor object plus the member's rank in their guild.
|
||||||
|
///
|
||||||
|
/// **Only the raw rank is emitted, never a resolved label.** ServUO names the five
|
||||||
|
/// standard ranks with cliloc ids (1062959–1062963) and ships no text for them, so the
|
||||||
|
/// shard cannot produce "Warlord" without a client-file table it does not have. The
|
||||||
|
/// website module does have one, and resolving a game term is its job in any case.
|
||||||
|
///
|
||||||
|
/// `rank` is the numeric rank, 0–4, with 4 being Leader (`RankDefinition.Ranks`). A
|
||||||
|
/// custom rank definition may carry a literal string instead of a cliloc, so `rankName`
|
||||||
|
/// is written when there is one and `rankCliloc` when there is not; a shard that has
|
||||||
|
/// replaced the rank table therefore keeps its own naming rather than being flattened
|
||||||
|
/// into the stock five.
|
||||||
|
///
|
||||||
|
/// A member with no readable rank — a mobile that is not a PlayerMobile, or one whose
|
||||||
|
/// GuildRank is null — is written with no rank fields at all rather than a fabricated
|
||||||
|
/// default. Absent means "not known", and a consumer that treated a missing rank as 0
|
||||||
|
/// would silently demote them.
|
||||||
|
///
|
||||||
|
/// **Staff are deliberately written with no rank, and this is not a rounding error.**
|
||||||
|
/// `PlayerMobile.GuildRank` returns `RankDefinition.Leader` for anyone at GameMaster or
|
||||||
|
/// above, whatever their actual rank — a gameplay convenience so staff can operate a
|
||||||
|
/// guild stone, and emphatically not a claim about who leads the guild. The true value
|
||||||
|
/// is in a private field with no accessor, so the only honest options are "Leader" and
|
||||||
|
/// "not known", and publishing a staff member as a guild leader on a public roster is
|
||||||
|
/// the worse of the two by a wide margin. A staff account that genuinely leads its guild
|
||||||
|
/// shows as an unranked member, which is a visible gap rather than a false claim.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteGuildMember(StringBuilder sb, Mobile m)
|
||||||
|
{
|
||||||
|
if (m == null)
|
||||||
|
{
|
||||||
|
sb.Append("null");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('{');
|
||||||
|
WriteActorFields(sb, m);
|
||||||
|
|
||||||
|
var pm = m as Server.Mobiles.PlayerMobile;
|
||||||
|
var rank = pm == null || pm.AccessLevel >= AccessLevel.GameMaster ? null : pm.GuildRank;
|
||||||
|
|
||||||
|
if (rank != null)
|
||||||
|
{
|
||||||
|
sb.Append(",\"rank\":").Append(rank.Rank);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(rank.Name.String))
|
||||||
|
{
|
||||||
|
sb.Append(",\"rankName\":");
|
||||||
|
Escape(sb, rank.Name.String);
|
||||||
|
}
|
||||||
|
else if (rank.Name.Number > 0)
|
||||||
|
{
|
||||||
|
sb.Append(",\"rankCliloc\":").Append(rank.Name.Number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// One bare actor object, with no leading field name: serial, name, account (when there
|
/// One bare actor object, with no leading field name: serial, name, account (when there
|
||||||
/// is one), the linked webId (when the account is linked), and the player flag. A `null`
|
/// is one), the linked webId (when the account is linked), and the player flag. A `null`
|
||||||
@@ -147,7 +217,21 @@ namespace Server.Custom.Bridge
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Append("{\"serial\":\"0x").Append(m.Serial.Value.ToString("X")).Append('"');
|
sb.Append('{');
|
||||||
|
WriteActorFields(sb, m);
|
||||||
|
sb.Append('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The actor fields, with no braces, so a caller can add its own.
|
||||||
|
///
|
||||||
|
/// Split out for <see cref="WriteGuildMember"/>, which is the same object plus guild
|
||||||
|
/// rank. Note the first field is written WITHOUT a leading comma and every later one
|
||||||
|
/// with, so this must be the first thing inside its object.
|
||||||
|
/// </summary>
|
||||||
|
private static void WriteActorFields(StringBuilder sb, Mobile m)
|
||||||
|
{
|
||||||
|
sb.Append("\"serial\":\"0x").Append(m.Serial.Value.ToString("X")).Append('"');
|
||||||
|
|
||||||
sb.Append(",\"name\":");
|
sb.Append(",\"name\":");
|
||||||
Escape(sb, m.Name ?? "");
|
Escape(sb, m.Name ?? "");
|
||||||
@@ -167,7 +251,6 @@ namespace Server.Custom.Bridge
|
|||||||
}
|
}
|
||||||
|
|
||||||
sb.Append(",\"player\":").Append(m.Player ? "true" : "false");
|
sb.Append(",\"player\":").Append(m.Player ? "true" : "false");
|
||||||
sb.Append('}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Closes the object. The trailing newline is the frame delimiter.</summary>
|
/// <summary>Closes the object. The trailing newline is the frame delimiter.</summary>
|
||||||
|
|||||||
@@ -325,7 +325,11 @@ namespace Server.Custom.Bridge
|
|||||||
.Num("seq", seq)
|
.Num("seq", seq)
|
||||||
.Bool("more", more);
|
.Bool("more", more);
|
||||||
|
|
||||||
sb.Actors("members", members, start, perLine);
|
// `withGuildRank` — the roster is the one place a member's rank in THIS guild is
|
||||||
|
// meaningful, and the only frame that carries it. Leadership is rank 4
|
||||||
|
// (RankDefinition.Ranks), and a guild can have several members at it, which is why
|
||||||
|
// the board's single `leader` field was never enough to answer "who leads this".
|
||||||
|
sb.Actors("members", members, start, perLine, withGuildRank: true);
|
||||||
|
|
||||||
BridgeLink.Emit(sb.End());
|
BridgeLink.Emit(sb.End());
|
||||||
_rosters++;
|
_rosters++;
|
||||||
|
|||||||
Reference in New Issue
Block a user