module-uo's half of ENGAGEMENT.md Phase 11: every trigger DECLARATION, the
wire-kind mapping that fires them, and the three registered audiences. No rule
and no template is seeded here -- that is 11b -- so nothing this adds sends
anybody anything until an operator writes a rule.
server/config/shardTriggers.js declares the 24, grouped by the audience kind
each family exercises, and every variable carries the `example` the template
editor previews and test-sends with. Ceilings: 10 `owner`, 2 `members`, 7
`authenticated`, 2 `staff`, 3 `admin` (the value core adds in the same window).
`uo.cheat.detected` at `staff` is the declaration the lattice exists for.
server/utils/shardEngagement.js maps the wire to those ids, hung off
shardIngest.ingest beside the SSE broadcast and the push tickle, and reads like
shardPush.js on purpose -- owner resolution is why neither can be a pure mapper.
Three things live here because a rule cannot express them:
* Transitions. champ.update and city.update are full-state upserts, so without
a per-process tracker a sidecar reconnect reads as twenty spawns starting.
A FIRST sighting is never a transition.
* Thresholds. conditions.js compares a declared variable against a LITERAL, so
"within 24 hours of dismissal" is not expressible; and vendor.listing is a
sweep frame re-emitted on any price change, so per-frame would flood. The
crossing is tracked here and `hoursRemaining` is declared so an operator can
still narrow with `is at most`.
* The members audience. "The members of THIS guild" differs every firing, so
it travels on the envelope as recipientUserIds (Phase 6 decision 2).
**The fan-out runs BEFORE the state write, and that ordering is load-bearing.**
account.unlinked drops the shard_account_links row that names the one person who
needs to be told; house.remove drops the house whose stored ownerAcct is the only
place a collapsed house's owner appears; guild.leave/remove need the roster and
board mirrors to name who left. Resolving afterwards finds nobody, every time.
Four rows of 8.6 deliberately do not ship, each with its reason recorded in
docs (docs#194): uo.market.item_listed (a saved search, no per-user query store),
uo.guild.joined (core's team.member.joined already fires for it -- a UO guild IS
a Team and this module is the provider), uo.link.requested (no addressable
recipient by construction, ~5-minute TTL), and uo.points.rank_changed's personal
half (top[] names a serial, links are keyed by account).
coreApi -> ^1.8.0: the module now calls registerEventTriggers and declares
`ceiling: 'admin'`, so a 1.7.0 core would refuse the ceiling and a 1.6.0 one
would not have the method at all.
39 new tests; 509/509 pass. check:imports, check:bundle and check:swagger clean.
Co-Authored-By: Claude <noreply@anthropic.com>
446 lines
19 KiB
JavaScript
446 lines
19 KiB
JavaScript
const { query } = require('../../core')
|
|
|
|
// Shared upsert builder for the shard-state tables. Each is keyed on a single
|
|
// primary column (`pkCol` = pk); `fields` carries only the columns the model
|
|
// wants to write, so a partial refresh touches nothing else. `coalesce` keeps
|
|
// the prior column value when the incoming one is NULL (used by shard_online so a
|
|
// vitals frame that omits acct/name doesn't blank what mob.login set); otherwise
|
|
// the incoming value wins (VALUES()).
|
|
function upsertRow(table, pkCol, pk, fields, { coalesce = false } = {}) {
|
|
const cols = Object.keys(fields)
|
|
const allCols = [pkCol, ...cols]
|
|
const insertCols = allCols.map((c) => `\`${c}\``).join(', ')
|
|
const placeholders = allCols.map(() => '?').join(', ')
|
|
const rhs = coalesce
|
|
? (c) => `\`${c}\` = COALESCE(VALUES(\`${c}\`), \`${c}\`)`
|
|
: (c) => `\`${c}\` = VALUES(\`${c}\`)`
|
|
const updates = cols.map(rhs).join(', ')
|
|
return query(
|
|
`INSERT INTO ${table} (${insertCols}) VALUES (${placeholders})
|
|
ON DUPLICATE KEY UPDATE ${updates}`,
|
|
[pk, ...cols.map((c) => fields[c])],
|
|
)
|
|
}
|
|
|
|
// ── Online players ─────────────────────────────────────────────────────────
|
|
const ONLINE_COLS =
|
|
'serial, name, acct, web_id, map, x, y, z, hits, hits_max, mana, mana_max, stam, stam_max, str, dex, `int`, updated_at'
|
|
|
|
// Upsert one online player. `fields` already prepared by the model (only the
|
|
// columns it wants to write); serial is required and is the primary key.
|
|
// COALESCE variant: a char.vitals frame that omits acct/name must not blank what
|
|
// mob.login set, so an incoming NULL keeps the prior column value.
|
|
const upsertOnline = (serial, fields) =>
|
|
upsertRow('shard_online', 'serial', serial, fields, { coalesce: true })
|
|
|
|
const removeOnline = (serial) => query('DELETE FROM shard_online WHERE serial = ?', [serial])
|
|
const clearOnline = () => query('DELETE FROM shard_online')
|
|
|
|
async function countOnline() {
|
|
const rows = await query('SELECT COUNT(*) AS n FROM shard_online')
|
|
return rows[0] ? Number(rows[0].n) : 0
|
|
}
|
|
|
|
const listOnline = () =>
|
|
query(`SELECT ${ONLINE_COLS} FROM shard_online ORDER BY name ASC`)
|
|
|
|
// Online players on any of the given game accounts (admin: a user's linked
|
|
// accounts). Empty list short-circuits so we never emit `IN ()`.
|
|
const listOnlineByAccounts = (accounts) =>
|
|
accounts.length === 0
|
|
? Promise.resolve([])
|
|
: query(
|
|
`SELECT ${ONLINE_COLS} FROM shard_online
|
|
WHERE acct IN (${accounts.map(() => '?').join(', ')})
|
|
ORDER BY name ASC`,
|
|
accounts,
|
|
)
|
|
|
|
// Staff roles whose online presence is shown on the public Shard page. Players
|
|
// who link an account are NOT surfaced publicly — only staff opt into visibility
|
|
// by virtue of being staff.
|
|
const PUBLIC_ONLINE_ROLES = ['admin', 'editor', 'moderator']
|
|
|
|
// Online players whose game account is linked to a STAFF website user. Joined
|
|
// against shard_account_links (not the sidecar-supplied web_id) so a link takes
|
|
// effect immediately, regardless of whether the player has re-logged since
|
|
// linking, then through to users so only staff roles are surfaced publicly.
|
|
const listOnlineLinked = () => {
|
|
const cols = ONLINE_COLS.split(', ')
|
|
.map((c) => `o.${c}`)
|
|
.join(', ')
|
|
return query(
|
|
`SELECT ${cols}
|
|
FROM shard_online o
|
|
JOIN shard_account_links l ON l.account = o.acct
|
|
JOIN users u ON u.id = l.user_id
|
|
WHERE u.role IN (${PUBLIC_ONLINE_ROLES.map(() => '?').join(', ')})
|
|
ORDER BY o.name ASC`,
|
|
PUBLIC_ONLINE_ROLES,
|
|
)
|
|
}
|
|
|
|
// ── Economy supply series ────────────────────────────────────────────────
|
|
const insertEconomy = ({ accounts, gold, t }) =>
|
|
query('INSERT INTO shard_economy (accounts, gold, t) VALUES (?, ?, ?)', [
|
|
accounts ?? null,
|
|
gold ?? null,
|
|
t,
|
|
])
|
|
|
|
const listEconomy = (limit) =>
|
|
query('SELECT accounts, gold, t FROM shard_economy ORDER BY t DESC LIMIT ?', [limit])
|
|
|
|
async function latestEconomy() {
|
|
const rows = await query('SELECT accounts, gold, t FROM shard_economy ORDER BY t DESC LIMIT 1')
|
|
return rows[0] || null
|
|
}
|
|
|
|
// ── Houses / IDOC ────────────────────────────────────────────────────────
|
|
const HOUSE_COLS =
|
|
'serial, stage, map, x, y, z, region, name, owner_serial, owner_acct, built_on, last_refreshed, is_idoc, updated_at' +
|
|
// Protocol 5's decay schedule. Added to the BASE column list rather than to
|
|
// HOUSE_REG_COLS because it arrives on house.decay, so a decay-only row -- one the
|
|
// registry sweep has never seen -- carries it too, and the public IDOC page reads
|
|
// exactly those rows.
|
|
', next_stage, estimated_collapse, decay_period_sec, dynamic_decay'
|
|
|
|
const upsertHouse = (serial, fields) => upsertRow('shard_houses', 'serial', serial, fields)
|
|
|
|
const listIdocHouses = () =>
|
|
query(`SELECT ${HOUSE_COLS} FROM shard_houses WHERE is_idoc = 1 ORDER BY updated_at DESC`)
|
|
|
|
// Houses owned by any of the given game accounts (admin: a user's linked
|
|
// accounts). IDOC houses first, then newest-refreshed. Empty list short-circuits.
|
|
const listHousesByAccounts = (accounts) =>
|
|
accounts.length === 0
|
|
? Promise.resolve([])
|
|
: query(
|
|
`SELECT ${HOUSE_REG_COLS} FROM shard_houses
|
|
WHERE owner_acct IN (${accounts.map(() => '?').join(', ')})
|
|
ORDER BY is_idoc DESC, updated_at DESC`,
|
|
accounts,
|
|
)
|
|
|
|
// ── House registry (Protocol 2.0 house.update / house.remove) ──────────────
|
|
// The registry columns extend HOUSE_COLS; a registry row is one we've seen via
|
|
// house.update (in_registry = 1), as opposed to a decay-only transition row.
|
|
const HOUSE_REG_COLS = `${HOUSE_COLS}, owner_name, co_owners, friends, price, decay, in_registry`
|
|
|
|
const removeHouse = (serial) => query('DELETE FROM shard_houses WHERE serial = ?', [serial])
|
|
|
|
// The full registered-house browser: every row we've seen via house.update.
|
|
const listRegistryHouses = () =>
|
|
query(`SELECT ${HOUSE_REG_COLS} FROM shard_houses WHERE in_registry = 1 ORDER BY name ASC`)
|
|
|
|
// ── Champion spawns ────────────────────────────────────────────────────────
|
|
const CHAMP_COLS =
|
|
'serial, category, type, name, status, active, map, x, y, z, boss_up, payload, t, updated_at'
|
|
|
|
const upsertChamp = (serial, fields) => upsertRow('shard_champs', 'serial', serial, fields)
|
|
|
|
const removeChamp = (serial) => query('DELETE FROM shard_champs WHERE serial = ?', [serial])
|
|
const clearChamps = () => query('DELETE FROM shard_champs')
|
|
// Ordered by name (matches the sidecar's /champs ordering).
|
|
const listChamps = () => query(`SELECT ${CHAMP_COLS} FROM shard_champs ORDER BY name ASC`)
|
|
|
|
// ── Help-page (support) queue ──────────────────────────────────────────────
|
|
const PAGE_COLS =
|
|
'page_id, type, sender_name, sender_acct, web_id, message, map, x, y, z, sent_ms, handled, handler, payload, updated_at'
|
|
|
|
async function upsertPage(pageId, fields) {
|
|
const cols = Object.keys(fields)
|
|
const allCols = ['page_id', ...cols]
|
|
const insertCols = allCols.map((c) => `\`${c}\``).join(', ')
|
|
const placeholders = allCols.map(() => '?').join(', ')
|
|
const updates = cols.map((c) => `\`${c}\` = VALUES(\`${c}\`)`).join(', ')
|
|
await query(
|
|
`INSERT INTO shard_pages (${insertCols}) VALUES (${placeholders})
|
|
ON DUPLICATE KEY UPDATE ${updates}`,
|
|
[pageId, ...cols.map((c) => fields[c])],
|
|
)
|
|
}
|
|
|
|
const removePage = (pageId) => query('DELETE FROM shard_pages WHERE page_id = ?', [pageId])
|
|
const clearPages = () => query('DELETE FROM shard_pages')
|
|
// Oldest-open first so the queue reads like a work list.
|
|
const listPages = () => query(`SELECT ${PAGE_COLS} FROM shard_pages ORDER BY sent_ms ASC`)
|
|
|
|
// ── Guild board (Protocol 2.0) ─────────────────────────────────────────────
|
|
const GUILD_COLS =
|
|
'id, name, abbr, members, online, alliance, leader_serial, leader_name, leader_acct, leader_web_id, payload, t, updated_at'
|
|
|
|
const upsertGuild = (id, fields) => upsertRow('shard_guilds', 'id', id, fields)
|
|
|
|
const removeGuild = (id) => query('DELETE FROM shard_guilds WHERE id = ?', [id])
|
|
const clearGuilds = () => query('DELETE FROM shard_guilds')
|
|
const listGuilds = () => query(`SELECT ${GUILD_COLS} FROM shard_guilds ORDER BY name ASC`)
|
|
|
|
// ── Guild membership (Protocol 4) ──────────────────────────────────────────
|
|
// `rank` is backticked wherever it is written, like `int` on shard_online: it is a
|
|
// reserved word in MySQL 8 and merely a keyword in MariaDB, so it parses bare here
|
|
// and must not be relied on to.
|
|
const MEMBER_COLS = 'guild_id, serial, name, acct, web_id, is_player, `rank`, rank_cliloc, rank_name, t'
|
|
|
|
// Upsert rather than plain insert: a roster frame can be redelivered (the /history
|
|
// backfill replays stored frames on every reconnect), and a redelivery must be a
|
|
// no-op rather than a duplicate-key error.
|
|
//
|
|
// The rank columns are assigned unconditionally, NULL included. A member whose rank
|
|
// the shard withheld — a staff account, whose GuildRank getter reports Leader
|
|
// regardless of the truth — must go back to "not known" rather than keeping a rank
|
|
// from before they were promoted.
|
|
const upsertGuildMembers = (rows) => {
|
|
if (!rows.length) return Promise.resolve()
|
|
const values = rows.map(() => '(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)').join(', ')
|
|
const params = rows.flatMap((r) => [
|
|
r.guild_id, r.serial, r.name, r.acct, r.web_id, r.is_player,
|
|
r.rank, r.rank_cliloc, r.rank_name, r.t,
|
|
])
|
|
return query(
|
|
`INSERT INTO shard_guild_members (${MEMBER_COLS}) VALUES ${values}
|
|
ON DUPLICATE KEY UPDATE name = VALUES(name), acct = VALUES(acct),
|
|
web_id = VALUES(web_id), is_player = VALUES(is_player),
|
|
\`rank\` = VALUES(\`rank\`), rank_cliloc = VALUES(rank_cliloc),
|
|
rank_name = VALUES(rank_name), t = VALUES(t)`,
|
|
params,
|
|
)
|
|
}
|
|
|
|
const clearGuildMembers = (guildId) =>
|
|
query('DELETE FROM shard_guild_members WHERE guild_id = ?', [guildId])
|
|
|
|
const removeGuildMember = (guildId, serial) =>
|
|
query('DELETE FROM shard_guild_members WHERE guild_id = ? AND serial = ?', [guildId, serial])
|
|
|
|
const clearAllGuildMembers = () => query('DELETE FROM shard_guild_members')
|
|
|
|
const listGuildMembers = (guildId) =>
|
|
query(`SELECT ${MEMBER_COLS} FROM shard_guild_members WHERE guild_id = ? ORDER BY name ASC`, [
|
|
guildId,
|
|
])
|
|
|
|
|
|
// **The game accounts on one guild's roster** — the input to
|
|
// `shardLinks.userIdsForAccounts`, and therefore to the `members` audience a
|
|
// guild event carries (Phase 11). Accounts rather than `web_id`, deliberately:
|
|
// `web_id` is a value MIRRORED off the wire actor, and `shard_account_links` is
|
|
// the authoritative map. A mirror that has drifted would mail the wrong person,
|
|
// and a mirror that is behind would mail nobody, so the query that decides who
|
|
// is told reads the table whose job that is.
|
|
const listGuildMemberAccounts = (guildId) =>
|
|
query(
|
|
'SELECT DISTINCT acct FROM shard_guild_members WHERE guild_id = ? AND acct IS NOT NULL',
|
|
[guildId],
|
|
)
|
|
|
|
// The accounts of every sitting governor — the `uo.governors` audience.
|
|
// `governor_acct` is NULL on a city with no governor and on one whose governor's
|
|
// mobile has no account, and both are simply nobody.
|
|
const listGovernorAccounts = () =>
|
|
query('SELECT DISTINCT governor_acct FROM shard_governors WHERE governor_acct IS NOT NULL')
|
|
|
|
// The guild an actor LEADS — matched on the current board (leader_serial or the
|
|
// linked leader_acct), so it reflects live state. Guild MEMBERSHIP for non-leaders
|
|
// is not modelled (the board carries only counts + leader), so we don't guess it.
|
|
const findGuildLedByActor = (serial, acct) =>
|
|
query(
|
|
`SELECT id, name, abbr, alliance, leader_name FROM shard_guilds
|
|
WHERE leader_serial = ? OR (leader_acct IS NOT NULL AND leader_acct = ?)
|
|
LIMIT 1`,
|
|
[serial ?? null, acct ?? null],
|
|
)
|
|
|
|
// Guilds led by any of the given game accounts (admin: a user's linked accounts).
|
|
const listGuildsLedByAccounts = (accounts) =>
|
|
accounts.length === 0
|
|
? Promise.resolve([])
|
|
: query(
|
|
`SELECT id, name, abbr, alliance, leader_name FROM shard_guilds
|
|
WHERE leader_acct IN (${accounts.map(() => '?').join(', ')})
|
|
ORDER BY name ASC`,
|
|
accounts,
|
|
)
|
|
|
|
// ── Governor board + term history (Protocol 2.0) ───────────────────────────
|
|
const GOV_COLS =
|
|
'city, governor_serial, governor_name, governor_acct, governor_web_id, elect_serial, elect_name, elect_acct, election_phase, candidates, auto_pick_at, payload, t, updated_at'
|
|
|
|
const upsertGovernor = (city, fields) => upsertRow('shard_governors', 'city', city, fields)
|
|
|
|
const listGovernors = () => query(`SELECT ${GOV_COLS} FROM shard_governors ORDER BY city ASC`)
|
|
|
|
// Cities whose current governor is one of the given game accounts (cross-link:
|
|
// does this user hold a governorship?). Empty list short-circuits.
|
|
const listGovernorshipsByAccounts = (accounts) =>
|
|
accounts.length === 0
|
|
? Promise.resolve([])
|
|
: query(
|
|
`SELECT ${GOV_COLS} FROM shard_governors
|
|
WHERE governor_acct IN (${accounts.map(() => '?').join(', ')})
|
|
ORDER BY city ASC`,
|
|
accounts,
|
|
)
|
|
|
|
// The single open term (ended_at IS NULL) for a city, if any.
|
|
async function currentGovernorTerm(city) {
|
|
const rows = await query(
|
|
'SELECT id, city, governor_serial, governor_name, governor_acct, governor_web_id, started_at, ended_at, votes FROM shard_governor_terms WHERE city = ? AND ended_at IS NULL ORDER BY started_at DESC LIMIT 1',
|
|
[city],
|
|
)
|
|
return rows[0] || null
|
|
}
|
|
|
|
const closeGovernorTerm = (id, endedAt) =>
|
|
query('UPDATE shard_governor_terms SET ended_at = ? WHERE id = ?', [endedAt, id])
|
|
|
|
const openGovernorTerm = ({ city, serial, name, acct, webId, startedAt }) =>
|
|
query(
|
|
`INSERT INTO shard_governor_terms
|
|
(city, governor_serial, governor_name, governor_acct, governor_web_id, started_at)
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
[city, serial ?? null, name ?? null, acct ?? null, webId ?? null, startedAt],
|
|
)
|
|
|
|
const listGovernorTerms = (city, limit) =>
|
|
query(
|
|
'SELECT id, city, governor_serial, governor_name, governor_acct, governor_web_id, started_at, ended_at, votes FROM shard_governor_terms WHERE city = ? ORDER BY started_at DESC LIMIT ?',
|
|
[city, limit],
|
|
)
|
|
|
|
// ── Online-population snapshot (Protocol 2.0 presence.online) ───────────────
|
|
async function setPresence({ count, byFacet, byRegion, t }) {
|
|
await query(
|
|
`INSERT INTO shard_presence (id, count, by_facet, by_region, t) VALUES (1, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE count = VALUES(count), by_facet = VALUES(by_facet),
|
|
by_region = VALUES(by_region), t = VALUES(t)`,
|
|
[
|
|
Number.isFinite(count) ? count : 0,
|
|
byFacet ? JSON.stringify(byFacet) : null,
|
|
byRegion ? JSON.stringify(byRegion) : null,
|
|
Number.isFinite(t) ? t : null,
|
|
],
|
|
)
|
|
}
|
|
|
|
async function latestPresence() {
|
|
const rows = await query('SELECT count, by_facet, by_region, t FROM shard_presence WHERE id = 1')
|
|
return rows[0] || null
|
|
}
|
|
|
|
// ── Shard ruleset (Protocol 3.0 world.ruleset) ─────────────────────────────
|
|
// Singleton, same shape as shard_presence: the shard re-emits the whole frame on
|
|
// every connect, so there is nothing to merge — the latest one wins outright.
|
|
async function setRuleset({ rev, expansion, payload, t }) {
|
|
await query(
|
|
`INSERT INTO shard_ruleset (id, rev, expansion, payload, t) VALUES (1, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE rev = VALUES(rev), expansion = VALUES(expansion),
|
|
payload = VALUES(payload), t = VALUES(t)`,
|
|
[rev ?? null, expansion ?? null, payload, Number.isFinite(t) ? t : null],
|
|
)
|
|
}
|
|
|
|
async function getRuleset() {
|
|
const rows = await query(
|
|
'SELECT rev, expansion, payload, t, updated_at FROM shard_ruleset WHERE id = 1',
|
|
)
|
|
return rows[0] || null
|
|
}
|
|
|
|
// ── Points/loyalty boards (Protocol 3.0 points.board) ──────────────────────
|
|
// One row per point system. The shard only emits a system whose top N actually
|
|
// moved, so this is a sparse stream of overwrites; there is no delete, because
|
|
// the shard's set of systems is fixed at startup.
|
|
async function upsertPointsBoard({ system, name, nameCliloc, maxPoints, players, showOnGump, payload, t }) {
|
|
await query(
|
|
`INSERT INTO shard_points_boards
|
|
(system, name, name_cliloc, max_points, players, show_on_gump, payload, t)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE name = VALUES(name), name_cliloc = VALUES(name_cliloc),
|
|
max_points = VALUES(max_points), players = VALUES(players),
|
|
show_on_gump = VALUES(show_on_gump), payload = VALUES(payload), t = VALUES(t)`,
|
|
[
|
|
system,
|
|
name ?? null,
|
|
Number.isFinite(nameCliloc) ? nameCliloc : null,
|
|
Number.isFinite(maxPoints) ? maxPoints : null,
|
|
Number.isFinite(players) ? players : null,
|
|
showOnGump ? 1 : 0,
|
|
payload,
|
|
Number.isFinite(t) ? t : null,
|
|
],
|
|
)
|
|
}
|
|
|
|
// Ordered by display name, falling back to the system key for a board whose name
|
|
// arrived as a bare cliloc — otherwise every unresolved board would sort together
|
|
// under NULL.
|
|
async function listPointsBoards() {
|
|
return query(
|
|
`SELECT system, name, name_cliloc, max_points, players, show_on_gump, payload, t, updated_at
|
|
FROM shard_points_boards ORDER BY COALESCE(name, system), system`,
|
|
)
|
|
}
|
|
|
|
async function getPointsBoard(system) {
|
|
const rows = await query(
|
|
`SELECT system, name, name_cliloc, max_points, players, show_on_gump, payload, t, updated_at
|
|
FROM shard_points_boards WHERE system = ?`,
|
|
[system],
|
|
)
|
|
return rows[0] || null
|
|
}
|
|
|
|
module.exports = {
|
|
upsertOnline,
|
|
removeOnline,
|
|
clearOnline,
|
|
countOnline,
|
|
listOnline,
|
|
listOnlineLinked,
|
|
listOnlineByAccounts,
|
|
insertEconomy,
|
|
listEconomy,
|
|
latestEconomy,
|
|
upsertHouse,
|
|
listIdocHouses,
|
|
listHousesByAccounts,
|
|
removeHouse,
|
|
listRegistryHouses,
|
|
upsertGuild,
|
|
removeGuild,
|
|
clearGuilds,
|
|
listGuilds,
|
|
upsertGuildMembers,
|
|
clearGuildMembers,
|
|
removeGuildMember,
|
|
clearAllGuildMembers,
|
|
listGuildMembers,
|
|
listGuildMemberAccounts,
|
|
listGovernorAccounts,
|
|
findGuildLedByActor,
|
|
listGuildsLedByAccounts,
|
|
upsertGovernor,
|
|
listGovernors,
|
|
listGovernorshipsByAccounts,
|
|
currentGovernorTerm,
|
|
closeGovernorTerm,
|
|
openGovernorTerm,
|
|
listGovernorTerms,
|
|
setPresence,
|
|
latestPresence,
|
|
setRuleset,
|
|
getRuleset,
|
|
upsertPointsBoard,
|
|
listPointsBoards,
|
|
getPointsBoard,
|
|
upsertChamp,
|
|
removeChamp,
|
|
clearChamps,
|
|
listChamps,
|
|
upsertPage,
|
|
removePage,
|
|
clearPages,
|
|
listPages,
|
|
}
|