Five verbs an author sees -- creatures, an enhanced "boss", an oracle NPC, a temporary gate, decoration -- and ONE command family underneath them, because every one of them ends in the same sentence: an object exists, and this run owns it. `world.spawn` / `world.despawn` / `world.owned` carry a `what` discriminator, and the per-verb differences are fields rather than kinds. The ownership registry is PERSISTED, and that is forced rather than chosen. A spawned creature is in the world save, so it survives the restart that proves a town-crier line gone -- which already rules out reconcile-by-boot-stamp. But the record of which run owns which serial has nowhere else to live: in memory it is lost in the restart the creatures survive, and only in the website's ledger it is not held here at all, so `world.despawn` would delete whatever serial it was handed and "never touches a creature it did not create" would have no mechanism behind it. So the Bridge gains its second persisted file beside `Participation.bin` -- written by the same world save as the objects it describes, so the two cannot get out of step. The oracle is ours rather than `XmlSpawner2.XmlDialog`'s, and that engine is the reason for both halves of the decision. Its `SpeechEntry` is the evidence the shape is right -- `Text` plus comma-separated `Keywords`, a keyword-less entry as the greeting, a proximity range, a conversation lock. It is also why not to build on it: `SpeechEntry` carries an `Action` string, XmlSpawner's command-scripting language, which would leave an arbitrary-command field one step from an event author. `Mobile.OnMovement` (delivered to every mobile in range -- the `HandlesOnMovement` filter applies only to Items) and `Mobile.HandlesOnSpeech`/`OnSpeech` are native virtuals and are all it needs. Every `Bridge.EventsMax*` REFUSES rather than clamps, on `LeaseMaxDurationSec`'s argument from 11b: the shard's bound exists for the case where the website is wrong. `Bridge.EventsEnabled` gates all of it -- spawning is the same consent 11b introduced that switch for, not a third one. Decoration carries an `itemId`, because `Static` accounts for 5031 of the tree's decoration placements under 1992 different graphics: for that class the graphic IS the identity. Never applied to a `BaseAddon`, whose own ItemID is not what a player sees. Containers are refused outright -- teardown would delete whatever a player had left inside. `tools/scaffolding` gains `worldgone <serial>`, which deletes an object behind the registry's back. It is the one outcome the rig cannot reach by asking the bridge -- every bridge verb that removes an object also drops its row -- and it is what a player's sword does every time they kill an event creature. Verified on a real ServUO 57.4 world (206k items, 42k mobiles) against the release sidecar: all five verbs place; every ceiling refuses; a container and an unknown type refuse; one run cannot despawn another's object; the registry and its objects both survive a save and a clean restart (`pruned: 0`); a creature deleted behind the registry's back comes back `gone` rather than `removed`; and a five-second gate is collected by the shard's own deadline with `world.expired` on the wire. Refs: docs/link/v7.md, docs/website/EVENTS_PLAN.md Phase 12a Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016wDDVXWMDz82WqE1i969r4
289 lines
15 KiB
INI
289 lines
15 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 event plane (docs/link/v6.md 8) ──────────────────────────────────────
|
|
#
|
|
# Leases and the participation ledger: the website holding a live config value for a bounded
|
|
# time, and this shard counting who took part in a run. Both are driven on a SCHEDULE, by an
|
|
# event the website starts unattended.
|
|
#
|
|
# This is deliberately NOT AdminWriteEnabled. Turning the admin plane on is consenting to
|
|
# staff moderation driven from a screen a human is looking at; turning this on is consenting
|
|
# to the website changing and watching your world at four in the morning. One switch could
|
|
# not honestly express both.
|
|
#
|
|
# A lease always carries its own deadline and this shard restores the baseline when it
|
|
# passes, whether or not the website is ever heard from again -- and a lease is never written
|
|
# to disk, so a restart puts every leased value back too.
|
|
EventsEnabled=false
|
|
|
|
# The longest this shard will hold a lease, whatever the website asks for. Thirty days.
|
|
# A longer request is REFUSED rather than shortened: a silently-clamped lease would leave the
|
|
# two halves disagreeing about when the world comes back.
|
|
LeaseMaxDurationSec=2592000
|
|
|
|
# How long a finished lease stays listed after its deadline restored it, so a teardown that
|
|
# arrives late still gets a definite verdict instead of finding nothing.
|
|
LeaseGraceSec=86400
|
|
|
|
# How often the participation sweep credits everyone standing in a run's area, and what one
|
|
# kill inside it is worth against one minute of being there.
|
|
ParticipationSweepSeconds=30
|
|
ParticipationKillWeight=5.0
|
|
|
|
# Bounds. Runs counted at once, members per run, and the widest area an event may declare.
|
|
ParticipationMaxRuns=8
|
|
ParticipationMaxMembers=2000
|
|
ParticipationMaxRadius=300
|
|
|
|
# How long a closed run's tally stays readable before this shard forgets it, and how many
|
|
# members one snapshot resolves before yielding the Core thread.
|
|
ParticipationGraceSec=86400
|
|
ParticipationSnapshotChunk=100
|
|
|
|
# ---- The world verbs (protocol 7) ----------------------------------------------------
|
|
# What an event may PLACE in the world, all of it owned by the run that placed it and
|
|
# deleted when the run tears down. Every ceiling here REFUSES rather than clamps: this
|
|
# shard's bound exists for the case where the website is wrong, and a quiet clamp would
|
|
# leave the two halves disagreeing about what was actually placed.
|
|
#
|
|
# The defaults are the EM Program's published quotas, because they are the only numbers
|
|
# anyone has defended in public.
|
|
|
|
# Per CALL: creatures, enhanced "boss" variants, oracle NPCs and decoration items.
|
|
EventsMaxCreatures=30
|
|
EventsMaxBosses=4
|
|
EventsMaxNpcs=5
|
|
EventsMaxDecor=60
|
|
|
|
# The longest a temporary gate may stand. The shard closes it on its own when the time
|
|
# passes, whether or not the website is ever heard from again.
|
|
EventsMaxGateMinutes=240
|
|
|
|
# Per RUN, across every verb above. The per-call ceilings bound one request; this bounds
|
|
# a run that calls a verb in a loop, which is the shape a runaway schedule takes.
|
|
EventsMaxOwnedPerRun=200
|
|
|
|
# How far from the chosen spot things may be scattered.
|
|
EventsMaxSpread=40
|
|
|
|
# How much harder than normal a "boss" may be made. EVENTS.md calls it an enhanced
|
|
# regular mob, so this is low enough that the result is still the creature that was
|
|
# picked.
|
|
EventsMaxBossMultiplier=10.0
|
|
|
|
# The oracle NPC: how many keyword lines it answers to, how close a player must be to be
|
|
# greeted and to be heard, and how often it will speak to the same player.
|
|
EventsOracleMaxLines=5
|
|
EventsOracleGreetRange=4
|
|
EventsOracleSpeechRange=8
|
|
EventsOracleGreetCooldownSec=60
|
|
EventsOracleAnswerCooldownSec=5
|
|
|
|
# How often expired gates are collected and rows for objects the world has already lost
|
|
# are pruned.
|
|
EventsSweepSeconds=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.
|