import { useMemo } from 'react' import PublicLayout from '../../components/PublicLayout.jsx' import PageHeader from '../../components/PageHeader.jsx' import { Loading, ErrorState } from '../../components/PageState.jsx' import { useAsync } from '../../lib/useAsync.js' import { useShardFeed } from '../../lib/useShardFeed.js' import { api } from '../../api/client.js' // The shard ruleset. Loaded from /public/shard/ruleset, replaced wholesale by any // world.ruleset frame on the live feed (the shard re-emits the entire ruleset, so // there is nothing to merge — latest wins). // // Everything on this page is published BY THE SHARD from its own Config/*.cfg, so // it cannot drift the way a hand-written rules page does. That is the whole point // of the feature, and the page says so. const RULESET_KINDS = new Set(['world.ruleset']) // Skill and stat caps arrive in tenths, the way ServUO stores them: 1000 is 100.0 // skill. Showing the raw number would be actively misleading. const tenths = (v) => (Number.isFinite(v) ? (v / 10).toFixed(1) : null) const num = (v) => (Number.isFinite(v) ? v.toLocaleString() : null) const pct = (v) => (Number.isFinite(v) ? `${v}%` : null) // The systems block is a flat bag of booleans; these are their display names, and // the order here is the order they render. A key the shard sends that we don't // know about still renders, humanised, rather than being silently dropped — a new // plugin must not go invisible against an older client. const SYSTEM_LABELS = { cityLoyalty: 'City Loyalty (governors)', vvv: 'Vice vs Virtue', factions: 'Factions', siege: 'Siege ruleset', chat: 'In-game chat', store: 'Ultima Store', dailyRares: 'Daily rares', honesty: 'Honesty virtue', shadowguard: 'Shadowguard', treasureMaps: 'Treasure maps', vetRewards: 'Veteran rewards', testCenter: 'Test Center', } const humanise = (key) => key.replace(/([A-Z])/g, ' $1').replace(/^./, (c) => c.toUpperCase()) function Panel({ title, children }) { return (

{title}

{children}
) } // A label/value row. Rows whose value is null are dropped by the caller, so a // block never renders a dangling label for something the shard didn't publish. function Row({ label, value }) { return (
{label} {value}
) } function Rows({ items }) { const rows = items.filter(([, value]) => value !== null && value !== undefined) if (rows.length === 0) return null return (
{rows.map(([label, value]) => ( ))}
) } function SystemPill({ label, on }) { const color = on ? '#8fdcae' : 'var(--muted)' return ( ) } function Systems({ systems }) { // Known keys first in their declared order, then anything the shard added that // this build doesn't know about. const known = Object.keys(SYSTEM_LABELS).filter((k) => k in systems) const extra = Object.keys(systems).filter((k) => !(k in SYSTEM_LABELS)) const keys = [...known, ...extra] if (keys.length === 0) return null return (
{keys.map((k) => ( ))}
) } function Caps({ caps }) { return ( ) } function AccountsAndHousing({ accounts, housing, vetRewards }) { const items = [] if (accounts) { items.push(['Accounts per IP', num(accounts.perIp)]) items.push(['Character slots', num(accounts.charSlots)]) items.push([ 'In-game account creation', accounts.autoCreate === undefined ? null : accounts.autoCreate ? 'Enabled' : 'Website only', ]) } if (housing) items.push(['Houses per account', num(housing.accountHouseLimit)]) if (vetRewards?.enabled) { items.push(['Veteran reward interval', vetRewards.rewardIntervalDays ? `${vetRewards.rewardIntervalDays} days` : null]) } if (items.length === 0) return null return ( ) } function Champions({ champions }) { const t = champions.rankThresholds return ( 0 ? t.join(' · ') : null, ], ]} /> ) } function Felucca({ loot }) { return ( ) } function Vendors({ vendors }) { return ( ) } function Pvp({ vvv }) { return ( ) } function Schedule({ schedule }) { const items = [] if (schedule.autoSaveEnabled && schedule.autoSaveFrequencyMinutes) { items.push(['World save', `every ${schedule.autoSaveFrequencyMinutes} min`]) } else if (schedule.autoSaveEnabled === false) { items.push(['World save', 'Disabled']) } if (schedule.autoRestartEnabled) { const h = String(schedule.autoRestartHour ?? 0).padStart(2, '0') const m = String(schedule.autoRestartMinute ?? 0).padStart(2, '0') items.push(['Automatic restart', `${h}:${m} server time`]) if (schedule.autoRestartFrequencyHours) { items.push(['Restart interval', `every ${schedule.autoRestartFrequencyHours}h`]) } } if (items.length === 0) return null return ( ) } export default function Rules() { const { loading, error, data } = useAsync(() => api.shard.ruleset()) const { events, connected } = useShardFeed({ filter: RULESET_KINDS, max: 4 }) // The newest world.ruleset on the feed wins outright over the fetched copy — // the frame is a complete ruleset, not a delta. const ruleset = useMemo(() => events[0] || data || null, [data, events]) return (
{connected ? 'Live' : 'Offline'}
{loading && } {error && } {!loading && !error && !ruleset && (

The shard has not published its ruleset yet.

)} {!loading && !error && ruleset && (
{ruleset.systems && } {ruleset.caps && } {ruleset.champions && } {ruleset.loot && } {ruleset.vendors && } {ruleset.vvv?.enabled && } {ruleset.schedule && }
)}
) }