docs: move docs to RunicGateway/docs, repoint all references

Extracted docs/ (ADMIN_CONTROLS, INTEGRATION, PLAN, PROTOCOL_2, RESEARCH,
SHARD_PREREQS) into the central RunicGateway/docs repo under link/, with
full commit history preserved via git filter-repo.

The source cites these design docs by section throughout, so every in-repo
reference (C# + Rust comments, Bridge.cfg, and the READMEs) is repointed at
the new docs-repo URL. README references are rendered as markdown links; a
Documentation pointer section is added to the top-level README.

Docs repo: https://gitea.whitlocktech.com/RunicGateway/docs
This commit is contained in:
2026-07-18 00:08:34 -05:00
parent 5cade1aa9d
commit 0fd6b91f22
16 changed files with 26 additions and 26 deletions

View File

@@ -14,7 +14,7 @@ Port=7788
QueueCap=10000 QueueCap=10000
# Sweep intervals, seconds. Measured on a 150-character shard: a vitals sweep costs # Sweep intervals, seconds. Measured on a 150-character shard: a vitals sweep costs
# 0.0015 ms/char, so 1000 online players is ~1.5 ms per sweep. See docs/PLAN.md §1. # 0.0015 ms/char, so 1000 online players is ~1.5 ms per sweep. See https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §1.
StatSweepSeconds=30 StatSweepSeconds=30
DecaySweepSeconds=60 DecaySweepSeconds=60
EconomySweepSeconds=300 EconomySweepSeconds=300
@@ -29,7 +29,7 @@ ChampSweepSeconds=10
# support queue; the full open queue is also available on demand via pages.snapshot. # support queue; the full open queue is also available on demand via pages.snapshot.
PageSweepSeconds=5 PageSweepSeconds=5
# Guild roster poll (docs/PROTOCOL_2.md Part B). Guilds expose only EventSink.JoinGuild, so # Guild roster poll (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part B). Guilds expose only EventSink.JoinGuild, so
# create/disband/leave/leader/alliance changes are found by diffing BaseGuild.List on this # create/disband/leave/leader/alliance changes are found by diffing BaseGuild.List on this
# interval (emit guild.update / guild.remove). Guild membership moves slowly; 60s is ample. # interval (emit guild.update / guild.remove). Guild membership moves slowly; 60s is ample.
GuildSweepSeconds=60 GuildSweepSeconds=60
@@ -85,7 +85,7 @@ AdminReasonMaxLength=400
# Clamp on a timed ban's duration, seconds. A ban with no/zero duration is indefinite. # Clamp on a timed ban's duration, seconds. A ban with no/zero duration is indefinite.
AdminBanMaxDurationSec=31536000 AdminBanMaxDurationSec=31536000
# Account provisioning (docs/PROTOCOL_2.md Part A). Which side may mint game accounts: # Account provisioning (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part A). Which side may mint game accounts:
# website — the website is the authority; pair with Accounts.AutoCreateAccounts=false # website — the website is the authority; pair with Accounts.AutoCreateAccounts=false
# (else an in-game login of any new name still mints an account). # (else an in-game login of any new name still mints an account).
# game — the game server is the authority; website account.create is refused. # game — the game server is the authority; website account.create is refused.

View File

@@ -18,7 +18,7 @@ namespace Server.Custom.Bridge
/// and replies link.ok. The tag persists to accounts.xml across restarts. /// and replies link.ok. The tag persists to accounts.xml across restarts.
/// ///
/// The code table and the account write both live on the Core thread. The websiteUserId in /// The code table and the account write both live on the Core thread. The websiteUserId in
/// link.confirm is trusted only because the socket is loopback-only (docs/PLAN.md §2); if the /// link.confirm is trusted only because the socket is loopback-only (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §2); if the
/// sidecar ever moves off-host, gate it behind a shared secret. /// sidecar ever moves off-host, gate it behind a shared secret.
/// </summary> /// </summary>
public static class BridgeAccountLink public static class BridgeAccountLink

View File

@@ -8,7 +8,7 @@ using Server.Misc;
namespace Server.Custom.Bridge namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// The account provisioning plane (docs/PROTOCOL_2.md Part A): website-driven account /// The account provisioning plane (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part A): website-driven account
/// creation and unlinking. Companion to BridgeAccountLink (the in-game [link flow), which /// creation and unlinking. Companion to BridgeAccountLink (the in-game [link flow), which
/// is unchanged. /// is unchanged.
/// ///
@@ -18,7 +18,7 @@ namespace Server.Custom.Bridge
/// Both handlers run on the Core thread (BridgeBoot marshals inbound lines through /// Both handlers run on the Core thread (BridgeBoot marshals inbound lines through
/// Timer.DelayCall first), so they touch accounts freely. /// Timer.DelayCall first), so they touch accounts freely.
/// ///
/// Trust model matches the admin plane (docs/ADMIN_CONTROLS.md §5): authorization lives on /// Trust model matches the admin plane (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/ADMIN_CONTROLS.md §5): authorization lives on
/// the website; the shard trusts the loopback + token socket and a required "actor" field. /// the website; the shard trusts the loopback + token socket and a required "actor" field.
/// The one shard-side floor on unlink is BridgeAdmin.Protected — a protected staff account is /// The one shard-side floor on unlink is BridgeAdmin.Protected — a protected staff account is
/// never unlinkable from the web. The whole create plane is opt-in via SignupMode / /// never unlinkable from the web. The whole create plane is opt-in via SignupMode /

View File

@@ -13,7 +13,7 @@ namespace Server.Custom.Bridge
/// Every handler runs on the Core thread (BridgeBoot marshals inbound lines through /// Every handler runs on the Core thread (BridgeBoot marshals inbound lines through
/// Timer.DelayCall first), so they may touch accounts, mobiles, and the network freely. /// Timer.DelayCall first), so they may touch accounts, mobiles, and the network freely.
/// ///
/// Trust model (docs/ADMIN_CONTROLS.md §5): authorization is enforced on the *website* — /// Trust model (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/ADMIN_CONTROLS.md §5): authorization is enforced on the *website* —
/// these commands are gated there behind admin/moderator roles. The shard trusts the /// these commands are gated there behind admin/moderator roles. The shard trusts the
/// loopback socket exactly as town-crier does, and applies inbound commands with an implicit /// loopback socket exactly as town-crier does, and applies inbound commands with an implicit
/// CoOwner authority. Its one hard floor is <see cref="Protected"/>: a command refuses any /// CoOwner authority. Its one hard floor is <see cref="Protected"/>: a command refuses any
@@ -248,7 +248,7 @@ namespace Server.Custom.Bridge
/// Opens an admin.audit frame (origin=web) with the common fields. Broadcast to every /// Opens an admin.audit frame (origin=web) with the common fields. Broadcast to every
/// connected dashboard so the website's moderation log stays complete regardless of which /// connected dashboard so the website's moderation log stays complete regardless of which
/// client issued the action. The in-game counterpart (origin=in-game) is emitted from /// client issued the action. The in-game counterpart (origin=in-game) is emitted from
/// BridgeEvents; see docs/ADMIN_CONTROLS.md §5.5. /// BridgeEvents; see https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/ADMIN_CONTROLS.md §5.5.
/// </summary> /// </summary>
private static System.Text.StringBuilder AuditBegin(string action, string actor, string target) private static System.Text.StringBuilder AuditBegin(string action, string actor, string target)
{ {

View File

@@ -5,7 +5,7 @@ namespace Server.Custom.Bridge
/// <summary> /// <summary>
/// Which side may mint game accounts. Governs the bridge's inbound account.create verb; /// Which side may mint game accounts. Governs the bridge's inbound account.create verb;
/// the in-game first-login auto-create is a separate core setting (Accounts.AutoCreateAccounts) /// the in-game first-login auto-create is a separate core setting (Accounts.AutoCreateAccounts)
/// the operator pairs with this (docs/PROTOCOL_2.md §2). /// the operator pairs with this (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §2).
/// </summary> /// </summary>
public enum SignupMode public enum SignupMode
{ {
@@ -43,7 +43,7 @@ namespace Server.Custom.Bridge
public static int TownCrierMaxActive { get; private set; } public static int TownCrierMaxActive { get; private set; }
public static int TownCrierMaxDurationSec { get; private set; } public static int TownCrierMaxDurationSec { get; private set; }
// Town Cryer news gump (docs/PROTOCOL_2.md §16). // Town Cryer news gump (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §16).
public static int NewsMaxTitleLength { get; private set; } public static int NewsMaxTitleLength { get; private set; }
public static int NewsMaxBodyLength { get; private set; } public static int NewsMaxBodyLength { get; private set; }
public static int NewsMaxExternal { get; private set; } public static int NewsMaxExternal { get; private set; }
@@ -55,7 +55,7 @@ namespace Server.Custom.Bridge
public static int AdminReasonMaxLength { get; private set; } public static int AdminReasonMaxLength { get; private set; }
public static int AdminBanMaxDurationSec { get; private set; } public static int AdminBanMaxDurationSec { get; private set; }
// ---- account provisioning (docs/PROTOCOL_2.md Part A) ---- // ---- account provisioning (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part A) ----
public static SignupMode Signup { get; private set; } public static SignupMode Signup { get; private set; }
public static bool AccountCreateEnabled { get; private set; } public static bool AccountCreateEnabled { get; private set; }
public static bool RequireIpForCreate { get; private set; } public static bool RequireIpForCreate { get; private set; }
@@ -89,7 +89,7 @@ namespace Server.Custom.Bridge
if (ChampSweepSeconds < 1) if (ChampSweepSeconds < 1)
ChampSweepSeconds = 1; ChampSweepSeconds = 1;
// Social/political sweeps (docs/PROTOCOL_2.md Part B). Both change slowly, so the // Social/political sweeps (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md Part B). Both change slowly, so the
// defaults are unhurried; the pass is a handful of field reads over a small set. // defaults are unhurried; the pass is a handful of field reads over a small set.
GuildSweepSeconds = Config.Get("Bridge.GuildSweepSeconds", 60); GuildSweepSeconds = Config.Get("Bridge.GuildSweepSeconds", 60);
if (GuildSweepSeconds < 1) if (GuildSweepSeconds < 1)

View File

@@ -6,7 +6,7 @@ using Server.Engines.CityLoyalty;
namespace Server.Custom.Bridge namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// The town-governor stream (docs/PROTOCOL_2.md §10.2). In modern ServUO the "mayor of a /// The town-governor stream (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §10.2). In modern ServUO the "mayor of a
/// town" is the Governor in the City Loyalty System (King Blackthorn's governance): each of /// town" is the Governor in the City Loyalty System (King Blackthorn's governance): each of
/// the governed cities has a Governor, a GovernorElect, and an Election. None of these raises /// the governed cities has a Governor, a GovernorElect, and an Election. None of these raises
/// an EventSink, so — like <see cref="BridgeChamps"/> and <see cref="BridgeSocial"/> — the set /// an EventSink, so — like <see cref="BridgeChamps"/> and <see cref="BridgeSocial"/> — the set

View File

@@ -7,7 +7,7 @@ using Server.Multis;
namespace Server.Custom.Bridge namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// The housing registry (docs/PROTOCOL_2.md §11 #9). BridgeSweeps already emits house.decay /// The housing registry (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §11 #9). BridgeSweeps already emits house.decay
/// *transitions*; this is the complementary *board*: one row per house with owner, location, /// *transitions*; this is the complementary *board*: one row per house with owner, location,
/// region, co-owners, value, and current decay level, so the website can render an owner→houses /// region, co-owners, value, and current decay level, so the website can render an owner→houses
/// map. Like the other Part B boards it is a diff sweep over BaseHouse.AllHouses — emit /// map. Like the other Part B boards it is a diff sweep over BaseHouse.AllHouses — emit

View File

@@ -8,7 +8,7 @@ namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// Outbound JSON is written by hand into a StringBuilder. It runs on the Core thread for /// Outbound JSON is written by hand into a StringBuilder. It runs on the Core thread for
/// every emitted event, and the measured budget in docs/PLAN.md assumes this cost, not a /// every emitted event, and the measured budget in https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md assumes this cost, not a
/// reflection serializer's. /// reflection serializer's.
/// ///
/// Inbound JSON is parsed with JavaScriptSerializer. Commands arrive at human rates, so /// Inbound JSON is parsed with JavaScriptSerializer. Commands arrive at human rates, so

View File

@@ -8,7 +8,7 @@ namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// Website news articles pushed into the modern Town Cryer News gump /// Website news articles pushed into the modern Town Cryer News gump
/// (docs/PROTOCOL_2.md §16). Distinct from BridgeTownCrier, which drives the scrolling-crier /// (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §16). Distinct from BridgeTownCrier, which drives the scrolling-crier
/// announcement lines (GlobalTownCrierEntryList). Here the full article — title, body (HTML), /// announcement lines (GlobalTownCrierEntryList). Here the full article — title, body (HTML),
/// image, and a "more info" URL — becomes a TownCryerNewsEntry in TownCryerSystem.NewsEntries, /// image, and a "more info" URL — becomes a TownCryerNewsEntry in TownCryerSystem.NewsEntries,
/// which the stock news gumps already render (they branch on TextDefinition.Number, so string /// which the stock news gumps already render (they branch on TextDefinition.Number, so string

View File

@@ -6,7 +6,7 @@ using Server.Mobiles;
namespace Server.Custom.Bridge namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// The presence stream (docs/PROTOCOL_2.md §11 #1/#2): who is online and where. Two parts: /// The presence stream (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §11 #1/#2): who is online and where. Two parts:
/// ///
/// presence.online - a periodic population snapshot (total, per-facet, per-region), emitted /// presence.online - a periodic population snapshot (total, per-facet, per-region), emitted
/// on a sweep but only when it changes, so the site has a live "N online" /// on a sweep but only when it changes, so the site has a live "N online"

View File

@@ -15,7 +15,7 @@ namespace Server.Custom.Bridge
/// ///
/// A profile is the single most expensive read in the bridge (~0.07 ms + ~2.4 KB at the /// A profile is the single most expensive read in the bridge (~0.07 ms + ~2.4 KB at the
/// seeded scale, more for a fully-kitted character), so it is built on demand only, never in /// seeded scale, more for a fully-kitted character), so it is built on demand only, never in
/// a sweep. See docs/PLAN.md §1. /// a sweep. See https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §1.
/// </summary> /// </summary>
public static class BridgeProfile public static class BridgeProfile
{ {
@@ -83,7 +83,7 @@ namespace Server.Custom.Bridge
} }
sb.Append(']'); sb.Append(']');
// worn equipment only — not the backpack/bank (see docs/PLAN.md §IV.4) // worn equipment only — not the backpack/bank (see https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §IV.4)
sb.Append(",\"equipment\":["); sb.Append(",\"equipment\":[");
first = true; first = true;
foreach (var item in m.Items) foreach (var item in m.Items)
@@ -104,7 +104,7 @@ namespace Server.Custom.Bridge
} }
/// <summary> /// <summary>
/// The titles a character holds (docs/PROTOCOL_2.md §10.3). `selected` is the index into /// The titles a character holds (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §10.3). `selected` is the index into
/// `reward` currently displayed (-1 if none). `fameKarma` and `skill` are the computed /// `reward` currently displayed (-1 if none). `fameKarma` and `skill` are the computed
/// display titles (may be absent). `reward` is the raw reward-title list — an entry may be /// display titles (may be absent). `reward` is the raw reward-title list — an entry may be
/// a cliloc number (as a string) or a literal string; resolve clilocs website-side. /// a cliloc number (as a string) or a literal string; resolve clilocs website-side.

View File

@@ -7,7 +7,7 @@ using Server.Guilds;
namespace Server.Custom.Bridge namespace Server.Custom.Bridge
{ {
/// <summary> /// <summary>
/// The guild stream (docs/PROTOCOL_2.md §10.1). Guilds have almost no useful EventSink: /// The guild stream (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PROTOCOL_2.md §10.1). Guilds have almost no useful EventSink:
/// EventSink.CreateGuild is only the load-time deserialization factory (Server/World.cs), and /// EventSink.CreateGuild is only the load-time deserialization factory (Server/World.cs), and
/// leave/disband/leader/alliance changes raise nothing. Only EventSink.JoinGuild is real. So, /// leave/disband/leader/alliance changes raise nothing. Only EventSink.JoinGuild is real. So,
/// exactly like <see cref="BridgeChamps"/>, the roster is polled: enumerate BaseGuild.List each /// exactly like <see cref="BridgeChamps"/>, the roster is polled: enumerate BaseGuild.List each

View File

@@ -11,7 +11,7 @@ namespace Server.Custom.Bridge
/// <summary> /// <summary>
/// The three polled streams, for state that has no EventSink: player vitals, house decay, /// The three polled streams, for state that has no EventSink: player vitals, house decay,
/// and money supply. All three run on the Core thread via repeating Timers, and the /// and money supply. All three run on the Core thread via repeating Timers, and the
/// measured cost (docs/PLAN.md §1) is why they can: at the seeded scale a full pass of all /// measured cost (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §1) is why they can: at the seeded scale a full pass of all
/// three is well under a millisecond. /// three is well under a millisecond.
/// ///
/// Timers do not fire during a world save (Timer.cs:322), so a sweep that would have landed /// Timers do not fire during a world save (Timer.cs:322), so a sweep that would have landed

View File

@@ -9,7 +9,7 @@ namespace Server.Custom.Bridge
/// <summary> /// <summary>
/// Forwards IN-GAME uses of the write-plane verbs to the website as admin.audit /// Forwards IN-GAME uses of the write-plane verbs to the website as admin.audit
/// (origin=in-game), so the site's moderation log is complete regardless of whether an action /// (origin=in-game), so the site's moderation log is complete regardless of whether an action
/// came from the website or a staff member in the game client. See docs/ADMIN_CONTROLS.md §5.5. /// came from the website or a staff member in the game client. See https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/ADMIN_CONTROLS.md §5.5.
/// ///
/// Two sources, mirroring how the shard records each: /// Two sources, mirroring how the shard records each:
/// - ban / kick: resolved with their target inside the stock generic command, which logs a /// - ban / kick: resolved with their target inside the stock generic command, which logs a

View File

@@ -11,7 +11,7 @@ git apply patches/<name>.patch
## Phase 7 — player-vendor sale (a coupled unit) ## Phase 7 — player-vendor sale (a coupled unit)
Player-vendor purchases raise **no** EventSink. `ValidVendorPurchase` / `ValidVendorSell` cover NPC vendors only. The commit point is `PlayerVendorBuyGump.OnResponse`, the only place where buyer, vendor **owner**, price, and commission are all in scope — exactly what cheat detection needs. See `docs/PLAN.md` §6. Player-vendor purchases raise **no** EventSink. `ValidVendorPurchase` / `ValidVendorSell` cover NPC vendors only. The commit point is `PlayerVendorBuyGump.OnResponse`, the only place where buyer, vendor **owner**, price, and commission are all in scope — exactly what cheat detection needs. See [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §6.
This is the one non-drop-in piece. Apply all three together: This is the one non-drop-in piece. Apply all three together:
@@ -56,4 +56,4 @@ Phase 0 modifies an existing file but ships as a whole-file overlay (`overlay/Sc
## Note on shard repairs ## Note on shard repairs
The deletions and edits described in `docs/SHARD_PREREQS.md` are one-time repairs to a specific broken install, not part of the bridge. They are not shipped here. The deletions and edits described in [SHARD_PREREQS.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/SHARD_PREREQS.md) are one-time repairs to a specific broken install, not part of the bridge. They are not shipped here.

View File

@@ -2,7 +2,7 @@
**Not part of the bridge. Never deployed.** `deploy.ps1` only copies `overlay/`, so nothing here reaches a server unless you put it there by hand. **Not part of the bridge. Never deployed.** `deploy.ps1` only copies `overlay/`, so nothing here reaches a server unless you put it there by hand.
These two scripts produced the measured budget in `docs/PLAN.md` §1. They are kept because those numbers should be reproducible, and because re-running the probe is the only honest way to check whether a change to the plugin's read path got more expensive. These two scripts produced the measured budget in [PLAN.md](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md) §1. They are kept because those numbers should be reproducible, and because re-running the probe is the only honest way to check whether a change to the plugin's read path got more expensive.
| File | Server path when testing | What | | File | Server path when testing | What |
|------|--------------------------|------| |------|--------------------------|------|