Protocol 2 could say how many members a guild had, not who they were, and there is no EventSink for leaving a guild — so PROTOCOL_2.md §10.1 deferred the whole membership half. This closes it. The sweep now holds each guild's member serial **set** instead of folding it into the signature as a sum. That buys two things. A set comparison cannot collide, where a sum could: one member joining and another leaving between two passes offset each other and the guild looked unchanged. And a set can be *differenced*, which is what makes a per-member `guild.leave` possible without a core tap — departures are simply the prior set minus the current one. A changed set also re-emits `guild.roster`, the full member list. That is what lets the departure events stay advisory: a consumer building a "so-and-so left" feed wants them, but a consumer holding a membership table only needs the roster, so nothing downstream has to replay deltas to stay correct. On a guild's first sweep there is no prior set, so nothing is reported as leaving — an unknown roster becoming known is not 155 people leaving at once. A roster is the only fat frame this plugin emits — measured at roughly 69 bytes per member against a real 155-member guild — and the sidecar reads a line with no length bound. So members per frame are capped (default 500, about 35 KB), and a guild over the cap is split into frames carrying `seq`, `more` and `total`. Every realistic guild emits exactly one frame with `seq` 0 and `more` false, which is the same shape as if chunking did not exist. Verified against the real sidecar with the cap forced down to 50, which produced 50/50/50/5 across four frames. The reconnect baseline is spread rather than fired in one pass. `OnConnected` clears the diff caches, so every guild looks changed at once, and building hundreds of fat frames in a single Core-thread tick is exactly the stall this bridge exists to avoid. At most GuildRosterGuildsPerTick guilds emit a roster per sweep; a guild over budget keeps its old member set, so it still reads as changed next pass. The sweep re-arms itself after 2s while a baseline is draining, so catch-up takes seconds rather than one full sweep interval per batch. BridgeJson gained the array writer it never had — there was no way to express a list of objects at all. Every field helper emits a leading `,"name":`, so Actor is split into a bare-object writer that both the single and array forms use. overlay.toml protocol -> 4, in this commit rather than a later one: CI folds it into the release manifest and the installer refuses to pair an overlay and a sidecar that disagree, so a bump landing separately from the emitters would silently fail to compose into a bundle. Verified on a live ServUO shard against the real Rust sidecar (not a stub): 155 members seeded from real PlayerMobiles, four roster frames reassembled to 153 entries on the board after two members were removed, two guild.leave frames with the correct serials, and the departed serials absent from the re-emitted roster. Refs: docs/website/TEAMS.md Part 12 Phase 1 Co-Authored-By: Claude <noreply@anthropic.com>
206 lines
11 KiB
INI
206 lines
11 KiB
INI
|
|
# uo-link bridge settings.
|
|
#
|
|
# Key scope is the filename: Bridge.cfg + StatSweepSeconds => "Bridge.StatSweepSeconds".
|
|
# Read in Configure(), which runs before World.Load.
|
|
|
|
# Loopback only. The socket being local is the trust boundary for inbound commands;
|
|
# if the sidecar ever moves off-host, add a shared secret first.
|
|
Host=127.0.0.1
|
|
Port=7788
|
|
|
|
# Outbound queue cap. On overflow the plugin drops oldest and counts the drops,
|
|
# because a stalled sidecar must never OOM the shard.
|
|
QueueCap=10000
|
|
|
|
# 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 https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/PLAN.md §1.
|
|
StatSweepSeconds=30
|
|
DecaySweepSeconds=60
|
|
EconomySweepSeconds=300
|
|
|
|
# Champion-spawn board poll. ChampionSpawn has no EventSink, so every spawn is diffed on
|
|
# this interval to emit champ.update on any status/level/kills/boss change. The world holds
|
|
# only a handful of spawns, so the pass is trivial; 5-10s is well within site tolerance.
|
|
ChampSweepSeconds=10
|
|
|
|
# Help-page queue poll. The in-game page queue has no EventSink, so it is diffed on this
|
|
# interval to emit page.new / page.closed / page.updated. A few seconds is fine for a
|
|
# support queue; the full open queue is also available on demand via pages.snapshot.
|
|
PageSweepSeconds=5
|
|
|
|
# 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
|
|
# interval (emit guild.update / guild.remove). Guild membership moves slowly; 60s is ample.
|
|
GuildSweepSeconds=60
|
|
|
|
# Members per guild.roster frame (Protocol 4). A roster is the only fat frame the bridge emits
|
|
# (~69 bytes per member) and the sidecar reads a line with no length bound, so this caps it; a
|
|
# guild over the cap is split across continuation frames carrying seq/more. 500 members is ~35 KB,
|
|
# past any realistic guild, so the split path is an edge case rather than the norm.
|
|
GuildRosterMembersPerLine=500
|
|
|
|
# Guilds that may emit a roster in one sweep. Every guild looks changed right after a sidecar
|
|
# reconnect, and building hundreds of fat frames in a single Core-thread pass is exactly the stall
|
|
# the bridge exists to avoid. The sweep re-arms itself every 2s while a baseline is draining, so
|
|
# lowering this slows the catch-up without making the site wait a full sweep interval per batch.
|
|
GuildRosterGuildsPerTick=25
|
|
|
|
# Town-governor poll. Each city's Governor / election is diffed on this interval to emit
|
|
# city.update on change. Governors turn over on the order of weeks, so a slow sweep is fine.
|
|
# Idle (emits nothing) unless the City Loyalty system is enabled (CityLoyalty.Enabled).
|
|
CitySweepSeconds=300
|
|
|
|
# Presence poll. Online population (total, per-facet, per-region) is snapshotted on this
|
|
# interval and emitted as presence.online only when it changes. Region transitions come
|
|
# through separately in real time as region.enter (EventSink.OnEnterRegion).
|
|
PresenceSweepSeconds=30
|
|
|
|
# Housing registry poll. Every house is diffed on this interval to emit house.update /
|
|
# house.remove (owner, region, location, decay). Houses change slowly; a few minutes is fine.
|
|
HousingSweepSeconds=300
|
|
|
|
# Points / loyalty leaderboards (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §7). ServUO carries ~25 point
|
|
# currencies (Queen's Loyalty, Void Pool, Casino, Clean Up Britannia, the nine city loyalties,
|
|
# the Doom/Khaldun/Kotl treasure systems, …). Each is diffed on this interval and emitted as
|
|
# one points.board frame per system when its top N moves.
|
|
#
|
|
# Slow on purpose: these are month-scale standings, and ten of the systems keep a row for
|
|
# every character ever created, so the pass is the widest read in the bridge. It is still
|
|
# cheap — a single bounded pass, never a sort — but there is nothing to gain by hurrying it.
|
|
PointsSweepSeconds=300
|
|
|
|
# Master switch for the boards. Off leaves char.profile points alone (see below).
|
|
PointsLeaderboardEnabled=true
|
|
|
|
# How many players per board. Clamped to 1..100 — the frame is emitted PER SYSTEM, so a big
|
|
# N is multiplied by ~25.
|
|
PointsTopN=10
|
|
|
|
# Which systems to publish, as a comma-separated list of PointsType names, e.g.
|
|
# PointsSystems=QueensLoyalty,CleanUpBritannia,VoidPool
|
|
# Blank (the default) publishes whatever the shard itself shows on the in-game loyalty gump
|
|
# (ShowOnLoyaltyGump), so a subsystem you add later gets a board without an edit here.
|
|
# An unrecognized name is logged and ignored, never silently dropped.
|
|
PointsSystems=
|
|
|
|
# Include a per-character "points" block in char.profile (the website character sheet). This
|
|
# is a lookup across every published system's table, so it is the dominant cost of building a
|
|
# profile; turn it off on a very large shard that does not want the sheet paying for it.
|
|
PointsProfileEnabled=true
|
|
|
|
# Also compute each system's rank in that block. OFF by default and worth leaving off: a
|
|
# points lookup stops at the character's own row, but a rank must count every row that beats
|
|
# them, in every system, on every profile build. The website already derives rank from the
|
|
# board for anyone in the top N.
|
|
PointsProfileRank=false
|
|
|
|
# Player-vendor market index (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §8). Every player vendor's shop name,
|
|
# owner, location and priced inventory, published as one vendor.listing frame per vendor so the
|
|
# website can offer the search the in-game Vendor Search gump offers. Honours each player's own
|
|
# in-game opt-out (the vendor's VendorSearch flag) — hide your vendor in game and it is hidden
|
|
# on the site too.
|
|
MarketEnabled=true
|
|
|
|
# Sweep interval. UNLIKE every other sweep here, a tick does NOT walk the whole world: it
|
|
# inventories at most MarketSweepBatch vendors and a persistent cursor round-robins through the
|
|
# rest, so the per-tick cost is bounded by the batch rather than by how many vendors exist. Full
|
|
# coverage takes ceil(vendors / batch) x MarketSweepSeconds — 500 vendors at the defaults is one
|
|
# complete pass every 20 minutes, and the site labels the data with how stale it may be.
|
|
#
|
|
# Lower this (or raise the batch) for faster coverage; both trade directly against per-tick cost,
|
|
# and the expensive part is the item walk, which recurses into every container a vendor is selling.
|
|
MarketSweepSeconds=60
|
|
MarketSweepBatch=25
|
|
|
|
# Per-vendor listing cap, after which the frame carries "truncated": true. A commodity reseller
|
|
# with thousands of stacked resources is a real thing, and an uncapped frame for one is measured
|
|
# in megabytes. Clamped to 1..5000.
|
|
MarketMaxListings=250
|
|
|
|
# Shard ruleset (https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/link/v3.md §5). One world.ruleset frame — expansion, which
|
|
# systems are on, skill/stat caps, account and house limits, champion scroll rules —
|
|
# emitted on every sidecar connect (and on [bridge reload), so the website's rules page
|
|
# cannot drift from the server. Not a sweep: it changes only when you edit a .cfg.
|
|
#
|
|
# The frame is built from an explicit allowlist of keys in BridgeRuleset.cs. Server.cfg,
|
|
# Staff.cfg, Email.cfg, DataPath.cfg, Bridge.cfg, Compiler.cfg, Reports.cfg and Client.cfg
|
|
# are never read.
|
|
RulesetEnabled=true
|
|
|
|
# The one connection detail the bridge will publish, e.g. play.myshard.com,2593. Blank
|
|
# (the default) omits it entirely. Server.cfg's Address/Listen/Port are NEVER published —
|
|
# if you want a connect string on the site, put it here deliberately.
|
|
PublicConnectAddress=
|
|
|
|
# Include the save/restart schedule (AutoSave frequency, AutoRestart hour) in the frame.
|
|
# Turn off if you would rather not advertise a predictable restart window.
|
|
RulesetIncludeSchedule=true
|
|
|
|
# Shown to a player when they run [link. The website page where they enter the code.
|
|
LinkUrl=https://yoursite/link
|
|
|
|
# Town-crier news pushed from the website. Caps are defense in depth on top of the
|
|
# loopback trust boundary: a buggy or compromised sidecar still cannot flood the criers.
|
|
TownCrierMaxLines=6
|
|
TownCrierMaxLineLength=200
|
|
TownCrierMaxActive=20
|
|
TownCrierMaxDurationSec=86400
|
|
|
|
# Town Cryer news gump. Website articles (news.add) become entries in the modern Town
|
|
# Cryer News gump (TownCryerSystem.NewsEntries), separate from the scrolling-crier lines
|
|
# above. The article title is also proclaimed by the criers (announce defaults on). Caps
|
|
# are defense in depth on top of the loopback trust boundary.
|
|
NewsMaxTitleLength=100
|
|
NewsMaxBodyLength=2000
|
|
NewsMaxExternal=20
|
|
NewsAnnounceDurationSec=300
|
|
|
|
# Admin write plane (staff moderation from the website). OFF by default: the whole
|
|
# feature is opt-in per shard. When enabled, inbound admin.* commands (kick/ban/unban/
|
|
# broadcast) are honored. Authorization is enforced on the website; the shard trusts the
|
|
# loopback socket and applies a hard floor below.
|
|
AdminWriteEnabled=false
|
|
|
|
# The one shard-side safety floor. An admin.* command refuses any target whose AccessLevel
|
|
# is at or above this, so even a compromised sidecar can never touch the Owner. Values are
|
|
# AccessLevel names (Player, VIP, Counselor, Decorator, Spawner, GameMaster, Seer,
|
|
# Administrator, Developer, CoOwner, Owner). Default CoOwner => only Owner/CoOwners shielded.
|
|
AdminAccessFloor=CoOwner
|
|
|
|
# Defense-in-depth caps on admin.* payloads (mirroring the town-crier caps).
|
|
AdminBroadcastMaxLength=300
|
|
AdminReasonMaxLength=400
|
|
# Clamp on a timed ban's duration, seconds. A ban with no/zero duration is indefinite.
|
|
AdminBanMaxDurationSec=31536000
|
|
|
|
# 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
|
|
# (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.
|
|
# hybrid — either side may create (the default).
|
|
# The bridge governs only the account.create verb; the in-game first-login auto-create is
|
|
# the core Accounts.AutoCreateAccounts setting, which you pair with the mode above. On boot
|
|
# the bridge warns if the two contradict. An unrecognized value here falls back to 'game'
|
|
# (the safest — no website creation).
|
|
SignupMode=hybrid
|
|
|
|
# Master switch for the account.create verb. Absent, it follows the mode (on unless
|
|
# SignupMode=game). Set explicitly to force it on or off regardless of mode.
|
|
AccountCreateEnabled=true
|
|
|
|
# Fail closed if account.create omits a usable browser IP. The per-IP cap
|
|
# (Accounts.AccountsPerIp) only means something if a missing/loopback IP is refused rather
|
|
# than waved through. Turn off only for a deployment that deliberately does not cap website
|
|
# signups by IP (MaxAccountsPerIP still applies in-game either way).
|
|
RequireIpForCreate=true
|
|
|
|
# Length caps on a website-supplied username / password, checked before the account is made.
|
|
AccountNameMaxLength=16
|
|
AccountPasswordMaxLength=30
|
|
|
|
# The test scaffolding in tools/scaffolding/ reads its own flags from this file
|
|
# (SeedOnStart, CensusOnStart, ProbeOnStart). They are absent here on purpose:
|
|
# Config.Get returns the default of false when a key is missing, so a deployed
|
|
# server never runs the scaffolding even if its .cs files are present.
|