fix(shard): answer with the instance name when the shard is unnamed
ServUO ships Server.cfg with `Name=My Shard`. An operator who never edited it publishes that verbatim, so the rules page read "My Shard" under a header carrying the real name. That value is the shard saying *unnamed* rather than naming anything, so the site now answers with its own. `settings.getInstanceName()` resolves `site_title || BRAND_NAME` — the same resolution `getPublic().brand.name` already uses, so an install that set only the site title can never show two different names on two pages. Bare `brand.name` would have been wrong for exactly that case. Substituted at INGEST rather than on read: world.ruleset is also broadcast live, and the same object is handed to the SSE fan-out, so a read-time fix would be undone by the next reconnect's frame. Matched case- and padding-insensitively but only as a whole value, so a shard genuinely called "My Shard Reborn" keeps its name. Fixes a second ruleset writer found on the way: uoLinkSocket.backfill() called shardState.setRuleset directly instead of going through the dispatcher as ingestEach does, so the boot/reconnect snapshot silently skipped this normalization. The two arrival orders have to produce the same stored frame. Also renders a placeholder row on an unscored leaderboard — the instance name with an em dash where a score goes, deliberately not shaped like an entry (no medal, no bar) because a placeholder that looked like a real standing would be a fabricated one. Presentation only; the API still sends an empty `top`. Verified live against the shard + sidecar: rules page and leaderboards on web and Android both correct. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U7CBg11prhLimL9iHSX1bP
This commit is contained in:
@@ -17,6 +17,7 @@ const shardStateModel = require('../model/shardState/shardState.model')
|
||||
const shardLinksModel = require('../model/shardLinks/shardLinks.model')
|
||||
const shardMarketModel = require('../model/shardMarket/shardMarket.model')
|
||||
const uoLinkConfigModel = require('../model/uoLinkConfig/uoLinkConfig.model')
|
||||
const settingsModel = require('../model/settings/settings.model')
|
||||
const broadcaster = require('./shardBroadcast')
|
||||
const pushDispatch = require('./pushDispatch')
|
||||
const defaultLog = require('./logger')('shard-ingest')
|
||||
@@ -63,6 +64,31 @@ function shouldLog(event) {
|
||||
return LOGGED_KINDS.has(event.kind)
|
||||
}
|
||||
|
||||
// ServUO's stock Server.cfg name. An operator who never set one publishes this
|
||||
// verbatim, so it carries no more information than a blank — matched
|
||||
// case-insensitively and trim-tolerantly, but ONLY as an exact whole value: a
|
||||
// shard genuinely called "My Shard Reborn" keeps its name.
|
||||
const STOCK_SHARD_NAME = 'my shard'
|
||||
|
||||
/**
|
||||
* The name to publish for the shard: its own, or this instance's when it has
|
||||
* effectively not given one.
|
||||
*
|
||||
* Deliberately not a general "blank means brand" rule applied across the wire —
|
||||
* it is scoped to this one field, where the two names denote the same thing.
|
||||
*/
|
||||
async function resolveShardName(shard, deps) {
|
||||
const given = String(shard ?? '').trim()
|
||||
if (given !== '' && given.toLowerCase() !== STOCK_SHARD_NAME) return given
|
||||
try {
|
||||
return (await deps.settings.getInstanceName()) || given
|
||||
} catch {
|
||||
// A ruleset that publishes the stock name is still better than one that
|
||||
// fails to store because the settings read hiccuped.
|
||||
return given
|
||||
}
|
||||
}
|
||||
|
||||
// Apply the state-change side effect for a kind (if any). Returns a promise.
|
||||
async function applyStateChange(event, deps) {
|
||||
const { shardState, uoLinkConfig, log } = deps
|
||||
@@ -180,6 +206,15 @@ async function applyStateChange(event, deps) {
|
||||
// would put a duplicate row in the event log on every reconnect, and
|
||||
// server.hello already marks each of those.
|
||||
case 'world.ruleset':
|
||||
// A shard whose operator never edited Server.cfg publishes ServUO's stock
|
||||
// "My Shard". That is the shard saying *unnamed*, not a name, so the site
|
||||
// answers with its own — the rules page reading "My Shard" under a header
|
||||
// reading UOMysticmoon is the shard failing to introduce itself.
|
||||
//
|
||||
// Normalized HERE rather than on read because the ruleset is also live: the
|
||||
// same `event` object is handed to the SSE broadcast a few lines below, and
|
||||
// a read-time fix would be undone by the next reconnect's frame.
|
||||
event.shard = await resolveShardName(event.shard, deps)
|
||||
await shardState.setRuleset(event)
|
||||
return
|
||||
// Board state, like guild.update — the newest frame for a system replaces the
|
||||
@@ -225,6 +260,7 @@ function resolveDeps(deps) {
|
||||
shardLinks: deps.shardLinks || shardLinksModel,
|
||||
shardMarket: deps.shardMarket || shardMarketModel,
|
||||
uoLinkConfig: deps.uoLinkConfig || uoLinkConfigModel,
|
||||
settings: deps.settings || settingsModel,
|
||||
broadcast: deps.broadcast || broadcaster.broadcast,
|
||||
pushDispatch: deps.pushDispatch || pushDispatch.fromShardEvent,
|
||||
log: deps.log || defaultLog,
|
||||
|
||||
Reference in New Issue
Block a user