feat: web dashboard and playground work

The NexusDashboard-parity dashboard (dDashboardServer) and everything built on it on the experimental branch:
accounts, characters, properties and moderation tools, permissions shared with in-game slash commands, economy
reports, World 3D and property 3D views with client scenery, scheduled events (features, vanity changes, live
events, announcements, restarts), vanity files and events, the CDClient browser, the message inspector with saved
captures, chat filter tools, community challenges, live ops, the AI moderator helper, and the server-side changes
they need.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-09-26 17:52:10 -05:00
parent 854d02f509
commit e213a7aeff
681 changed files with 88310 additions and 1321 deletions

View File

@@ -0,0 +1,6 @@
-- Migration: Add login tracking columns to accounts table
-- Adds fields for tracking login attempts, lockouts, and last login
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS failed_attempts INT NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS lockout_time DATETIME NULL DEFAULT NULL;
ALTER TABLE accounts ADD COLUMN IF NOT EXISTS last_login DATETIME NULL DEFAULT NULL;

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS audit_log (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
account_id INT NOT NULL DEFAULT 0,
account_name VARCHAR(64) NOT NULL DEFAULT '',
action VARCHAR(128) NOT NULL,
description TEXT NOT NULL,
timestamp BIGINT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,6 @@
/* Columns used by the web dashboard. Names match NexusDashboard so databases it already migrated keep their data,
statements for columns that already exist fail individually and are skipped. */
ALTER TABLE play_keys ADD COLUMN notes TEXT NULL;
ALTER TABLE bug_reports ADD COLUMN resolved_time TIMESTAMP NULL DEFAULT NULL;
ALTER TABLE bug_reports ADD COLUMN resoleved_by_id INT NULL;
ALTER TABLE bug_reports ADD COLUMN resolution TEXT NULL;

View File

@@ -0,0 +1,14 @@
/* Account email for password resets. email/email_confirmed_at match NexusDashboard's columns so existing data is kept,
statements for columns that already exist fail individually and are skipped. */
ALTER TABLE accounts ADD COLUMN email VARCHAR(255) NULL DEFAULT '';
ALTER TABLE accounts ADD COLUMN email_confirmed_at DATETIME NULL DEFAULT NULL;
/* Sessions issued before this unix time are rejected, e.g. after a password reset */
ALTER TABLE accounts ADD COLUMN sessions_valid_after BIGINT NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS account_tokens (
token_hash CHAR(64) NOT NULL PRIMARY KEY,
account_id INT NOT NULL,
purpose VARCHAR(16) NOT NULL,
data VARCHAR(255) NOT NULL DEFAULT '',
expires_at BIGINT NOT NULL,
INDEX account_tokens_account (account_id, purpose)
);

View File

@@ -0,0 +1,63 @@
/* Economy ledger for dashboard reports. Flows are aggregated per day (days since the Unix epoch, UTC)
so storage grows with active players and item types, not with the number of events. */
CREATE TABLE IF NOT EXISTS economy_currency_daily (
day INT NOT NULL,
character_id BIGINT NOT NULL,
source INT NOT NULL,
gained BIGINT NOT NULL DEFAULT 0,
spent BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, character_id, source),
INDEX economy_currency_character (character_id, day)
);
CREATE TABLE IF NOT EXISTS economy_uscore_daily (
day INT NOT NULL,
character_id BIGINT NOT NULL,
source INT NOT NULL,
gained BIGINT NOT NULL DEFAULT 0,
lost BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, character_id, source)
);
CREATE TABLE IF NOT EXISTS economy_items_daily (
day INT NOT NULL,
lot INT NOT NULL,
source INT NOT NULL,
gm TINYINT NOT NULL DEFAULT 0,
created BIGINT NOT NULL DEFAULT 0,
destroyed BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, lot, source, gm),
INDEX economy_items_lot (lot, day)
);
/* Player-to-player item and coin transfers. item_id is the object that left the sender, new_item_id the item or
stack that received it (the same id when a unique item or unmerged stack kept its identity, otherwise the stack
it merged into or split off as). */
CREATE TABLE IF NOT EXISTS economy_transfers (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
time BIGINT NOT NULL,
method TINYINT NOT NULL,
item_id BIGINT NOT NULL DEFAULT 0,
new_item_id BIGINT NOT NULL DEFAULT 0,
lot INT NOT NULL DEFAULT 0,
count INT NOT NULL DEFAULT 0,
coins BIGINT NOT NULL DEFAULT 0,
from_character BIGINT NOT NULL DEFAULT 0,
to_character BIGINT NOT NULL DEFAULT 0,
zone INT NOT NULL DEFAULT 0,
INDEX economy_transfers_item (item_id),
INDEX economy_transfers_new_item (new_item_id),
INDEX economy_transfers_from (from_character, time),
INDEX economy_transfers_to (to_character, time)
);
/* Where things happen in the world, for heatmaps: enemy kills and loot drops per day, zone, LOT and 4x4 unit cell.
kind: 1 = enemy killed by a player, 2 = item dropped, 3 = coins dropped (lot 0, quantity = coins). */
CREATE TABLE IF NOT EXISTS map_events_daily (
day INT NOT NULL,
zone INT NOT NULL,
kind TINYINT NOT NULL,
lot INT NOT NULL,
cell_x INT NOT NULL,
cell_z INT NOT NULL,
events BIGINT NOT NULL DEFAULT 0,
quantity BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (zone, kind, day, lot, cell_x, cell_z)
);
ALTER TABLE mail ADD COLUMN attachment_config TEXT NULL;

View File

@@ -0,0 +1,48 @@
/* Outgoing webhooks, economy anomaly flags, small persistent dashboard state and two-factor login. */
CREATE TABLE IF NOT EXISTS dashboard_webhooks (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(64) NOT NULL,
url TEXT NOT NULL,
format VARCHAR(16) NOT NULL DEFAULT 'discord',
events TEXT NOT NULL,
secret VARCHAR(128) NOT NULL DEFAULT '',
enabled TINYINT NOT NULL DEFAULT 1,
created_at BIGINT NOT NULL,
last_sent_at BIGINT NOT NULL DEFAULT 0,
last_status INT NOT NULL DEFAULT 0,
last_error TEXT NULL
);
/* kind: 1 = unusual coin income, 2 = unusual item creation, 3 = duplicated object id (day 0, so it is flagged once). */
CREATE TABLE IF NOT EXISTS economy_flags (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
created_at BIGINT NOT NULL,
day INT NOT NULL,
kind TINYINT NOT NULL,
character_id BIGINT NOT NULL DEFAULT 0,
lot INT NOT NULL DEFAULT 0,
item_id BIGINT NOT NULL DEFAULT 0,
value BIGINT NOT NULL DEFAULT 0,
baseline BIGINT NOT NULL DEFAULT 0,
details TEXT NULL,
status TINYINT NOT NULL DEFAULT 0,
reviewed_by INT NOT NULL DEFAULT 0,
reviewed_at BIGINT NOT NULL DEFAULT 0,
note TEXT NULL,
UNIQUE KEY economy_flags_subject (kind, day, character_id, lot, item_id),
INDEX economy_flags_status (status, id)
);
CREATE TABLE IF NOT EXISTS dashboard_state (
name VARCHAR(64) NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
/* totp_secret is encrypted with the dashboard's own key. totp_enabled_at is 0 while two-factor login is off. */
ALTER TABLE accounts ADD COLUMN totp_secret TEXT NULL;
ALTER TABLE accounts ADD COLUMN totp_enabled_at BIGINT NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN totp_last_step BIGINT NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS account_recovery_codes (
id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
account_id INT NOT NULL,
code_hash VARCHAR(64) NOT NULL,
used_at BIGINT NOT NULL DEFAULT 0,
INDEX account_recovery_codes_account (account_id)
);

View File

@@ -0,0 +1,4 @@
/* Lets old command log rows be pruned by age. Rows written before this have time 0 and are kept. */
ALTER TABLE command_log ADD COLUMN time BIGINT NOT NULL DEFAULT 0;
CREATE INDEX command_log_time ON command_log (time);
CREATE INDEX activity_log_time ON activity_log (time);

View File

@@ -0,0 +1,17 @@
/* Server settings in the database, editable from the dashboard. Servers copy their ini and environment values here
(file_value, not for secrets) at startup. A web_value is used when the file and environment do not set the key,
or always when web_wins is set. */
CREATE TABLE IF NOT EXISTS server_config (
file VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
file_value TEXT NULL,
file_source VARCHAR(8) NOT NULL DEFAULT '',
web_value TEXT NULL,
web_wins TINYINT NOT NULL DEFAULT 0,
secret TINYINT NOT NULL DEFAULT 0,
description TEXT NULL,
seen_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT '',
PRIMARY KEY (file, name)
);

View File

@@ -0,0 +1,25 @@
/* Periodic dashboard tasks. scheduled_tasks only has rows for tasks whose schedule or switch was changed on the
dashboard, or that have run on a schedule (last_scheduled_at, used to catch up after downtime).
scheduled_task_runs keeps finished runs with their log, pruned after log_task_days. */
CREATE TABLE IF NOT EXISTS scheduled_tasks (
name VARCHAR(64) NOT NULL PRIMARY KEY,
schedule VARCHAR(128) NULL,
enabled TINYINT NOT NULL DEFAULT 1,
last_scheduled_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS scheduled_task_runs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
task VARCHAR(64) NOT NULL,
task_trigger VARCHAR(16) NOT NULL,
actor VARCHAR(64) NOT NULL DEFAULT '',
started_at BIGINT NOT NULL,
finished_at BIGINT NOT NULL,
status TINYINT NOT NULL,
summary TEXT NULL,
log MEDIUMTEXT NULL,
INDEX scheduled_task_runs_task (task, id),
INDEX scheduled_task_runs_finished (finished_at)
);

View File

@@ -0,0 +1,13 @@
/* Earlier versions of characters' XML (zlib-compressed), taken before dashboard edits and daily for changed characters. */
CREATE TABLE IF NOT EXISTS character_snapshots (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
character_id BIGINT NOT NULL,
taken_at BIGINT NOT NULL,
reason VARCHAR(100) NOT NULL DEFAULT '',
actor VARCHAR(64) NOT NULL DEFAULT '',
size INT UNSIGNED NOT NULL DEFAULT 0,
hash CHAR(64) NOT NULL DEFAULT '',
xml LONGBLOB NOT NULL,
INDEX character_snapshots_character (character_id, id),
INDEX character_snapshots_taken (taken_at)
);

View File

@@ -0,0 +1,12 @@
/* Moderation history per account, and temporary bans with a reason. */
ALTER TABLE accounts ADD COLUMN ban_expires BIGINT NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN ban_reason TEXT NULL;
CREATE TABLE IF NOT EXISTS account_notes (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
account_id INT NOT NULL,
kind VARCHAR(16) NOT NULL,
text TEXT NOT NULL,
actor VARCHAR(64) NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
INDEX account_notes_account (account_id, id)
);

View File

@@ -0,0 +1,9 @@
/* One row a minute from the dashboard: players online, running worlds, whether auth and chat were up, memory in use. */
CREATE TABLE IF NOT EXISTS server_health (
time BIGINT NOT NULL PRIMARY KEY,
players INT UNSIGNED NOT NULL DEFAULT 0,
worlds INT UNSIGNED NOT NULL DEFAULT 0,
auth_online TINYINT NOT NULL DEFAULT 0,
chat_online TINYINT NOT NULL DEFAULT 0,
memory_kb BIGINT UNSIGNED NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,12 @@
/* Moderators' decisions on name requests and pet names, with the reason, so players can see why a name was turned down.
subject_id is the character (kind 'name') or the pet (kind 'pet_name'); subject is the name that was asked for. */
CREATE TABLE IF NOT EXISTS moderation_decisions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
kind VARCHAR(16) NOT NULL,
subject_id BIGINT NOT NULL,
subject VARCHAR(64) NOT NULL DEFAULT '',
approved TINYINT NOT NULL DEFAULT 0,
reason VARCHAR(255) NOT NULL DEFAULT '',
decided_at BIGINT NOT NULL DEFAULT 0,
INDEX moderation_decisions_subject (kind, subject_id)
);

View File

@@ -0,0 +1,4 @@
/* Who owns each named pet, written by the game when the name is set. NULL: not looked up yet (the dashboard fills
those in once from the characters' inventories); 0: no character has the pet any more. */
ALTER TABLE pet_names ADD COLUMN owner_id BIGINT NULL DEFAULT NULL;
CREATE INDEX pet_names_owner ON pet_names (owner_id);

View File

@@ -0,0 +1,4 @@
/* What a dashboard action was about, so an account's history is a lookup instead of a text search. 0: nothing. */
ALTER TABLE audit_log ADD COLUMN target_account_id INT UNSIGNED NOT NULL DEFAULT 0;
ALTER TABLE audit_log ADD COLUMN target_character_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX audit_log_target_account ON audit_log (target_account_id);

View File

@@ -0,0 +1,20 @@
/* Chat, for moderation and for bots that bridge it to Discord and the like. channel: zone (said in a world),
whisper, team, web (sent from the dashboard or a bot). blocked: the chat filter stopped it, so nobody saw it. */
CREATE TABLE IF NOT EXISTS chat_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
time BIGINT NOT NULL,
channel VARCHAR(16) NOT NULL,
sender_id BIGINT NOT NULL DEFAULT 0,
sender_name VARCHAR(64) NOT NULL DEFAULT '',
account_id INT UNSIGNED NOT NULL DEFAULT 0,
recipient_id BIGINT NOT NULL DEFAULT 0,
recipient_name VARCHAR(64) NOT NULL DEFAULT '',
zone_id INT UNSIGNED NOT NULL DEFAULT 0,
instance_id INT UNSIGNED NOT NULL DEFAULT 0,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
message TEXT NOT NULL,
blocked TINYINT NOT NULL DEFAULT 0,
INDEX chat_log_time (time),
INDEX chat_log_sender (sender_id),
INDEX chat_log_account (account_id)
);

View File

@@ -0,0 +1,6 @@
/* Each account's dashboard view choices (show staff, terrain in the 3D viewer, ...), as a JSON object, so they follow
the account from browser to browser. Written only by the dashboard; keys are the pages' own. */
CREATE TABLE IF NOT EXISTS dashboard_preferences (
account_id INT UNSIGNED NOT NULL PRIMARY KEY,
prefs TEXT NOT NULL
);

View File

@@ -0,0 +1,18 @@
/* Strikes against accounts, given when staff reject a name, pet name or property or remove a leaderboard score and
decide it deserves one (or by hand). source: MANUAL, NAME, PET_NAME, PROPERTY, LEADERBOARD. A revoked strike
(revoked_at > 0) stays on record but no longer counts. */
CREATE TABLE IF NOT EXISTS account_strikes (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
account_id INT UNSIGNED NOT NULL,
character_id BIGINT NOT NULL DEFAULT 0,
source VARCHAR(16) NOT NULL,
subject VARCHAR(128) NOT NULL DEFAULT '',
reason TEXT NOT NULL,
given_by_id INT UNSIGNED NOT NULL DEFAULT 0,
given_by VARCHAR(64) NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
revoked_at BIGINT NOT NULL DEFAULT 0,
revoked_by VARCHAR(64) NOT NULL DEFAULT '',
revoke_reason TEXT NOT NULL,
INDEX account_strikes_account (account_id)
);

View File

@@ -0,0 +1,12 @@
/* Daily totals of the per-character statistics (StatisticID: smashables smashed, quickbuilds, missions, pets tamed,
times smashed, ...) summed over every player in a zone. gm = 1 for staff (GM 3+), so reports can leave them out.
The map event kinds in map_events_daily grow too: 4 = player death (lot = killer), 5 = coins a player dropped on
death, 6 = smashable smashed by a player, 7 = quickbuild completed. */
CREATE TABLE IF NOT EXISTS player_stats_daily (
day INT NOT NULL,
zone INT NOT NULL,
stat INT NOT NULL,
gm TINYINT NOT NULL DEFAULT 0,
amount BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, zone, stat, gm)
);

View File

@@ -0,0 +1,14 @@
/* The in-game slash commands as the world servers registered them, so the dashboard can list them and change who may
use them. The levels themselves are settings (command_level_<name> in server_config). aliases is comma separated. */
CREATE TABLE IF NOT EXISTS slash_commands (
name VARCHAR(64) NOT NULL PRIMARY KEY,
aliases TEXT NOT NULL,
help TEXT NOT NULL,
info TEXT NOT NULL,
default_level TINYINT NOT NULL,
min_level TINYINT NOT NULL DEFAULT 0,
fixed TINYINT NOT NULL DEFAULT 0,
client_handled TINYINT NOT NULL DEFAULT 0,
note TEXT NOT NULL,
dashboard_permission VARCHAR(64) NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,41 @@
/* Strike thresholds: the automatic step (eStrikeStep: WARN, MUTE, BAN) a strike set off and the number of strikes it was
for, so the same step isn't applied twice for the same count. Statements for columns that already exist fail
individually and are skipped. */
ALTER TABLE account_strikes ADD COLUMN step VARCHAR(16) NOT NULL DEFAULT '';
ALTER TABLE account_strikes ADD COLUMN step_count INT UNSIGNED NOT NULL DEFAULT 0;
/* Reports players send from the game's Report Abuse window. kind (ePlayerReportKind): PLAYER (ReportBug naming another
player), MODEL (ReportOffensiveModel), PROPERTY (ReportOffensiveProperty). object_id is what the player picked;
target_* is the player it is about (the reported player, or the owner of the model or property) when the world knew
it. status (ePlayerReportStatus): 0 open, 1 actioned, 2 dismissed. */
CREATE TABLE IF NOT EXISTS player_reports (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
created_at BIGINT NOT NULL,
kind VARCHAR(16) NOT NULL,
reporter_id BIGINT NOT NULL DEFAULT 0,
reporter_account_id INT UNSIGNED NOT NULL DEFAULT 0,
object_id BIGINT NOT NULL DEFAULT 0,
object_lot INT NOT NULL DEFAULT 0,
target_character_id BIGINT NOT NULL DEFAULT 0,
target_account_id INT UNSIGNED NOT NULL DEFAULT 0,
property_id BIGINT NOT NULL DEFAULT 0,
zone_id INT UNSIGNED NOT NULL DEFAULT 0,
instance_id INT UNSIGNED NOT NULL DEFAULT 0,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
body TEXT NOT NULL,
status TINYINT NOT NULL DEFAULT 0,
handled_by VARCHAR(64) NOT NULL DEFAULT '',
handled_at BIGINT NOT NULL DEFAULT 0,
resolution TEXT NOT NULL,
INDEX player_reports_status (status, id),
INDEX player_reports_target (target_account_id)
);
/* Words staff add to the chat filter on the dashboard, on top of the files (chatplus_en_us.txt, blocklist.dcf).
allowed = 1: accepted in whitelisted chat; 0: always stopped. Lower case. */
CREATE TABLE IF NOT EXISTS chat_filter_words (
word VARCHAR(64) NOT NULL PRIMARY KEY,
allowed TINYINT NOT NULL,
added_by VARCHAR(64) NOT NULL DEFAULT '',
added_at BIGINT NOT NULL
);

View File

@@ -0,0 +1,12 @@
/* The network addresses accounts log in to the game from, written by the auth server at each successful login while
log_login_addresses is on, so staff can see accounts that share a connection. One row per account and address;
rows not seen for log_login_address_days are deleted by the log pruning task. */
CREATE TABLE IF NOT EXISTS account_login_addresses (
account_id INT UNSIGNED NOT NULL,
address VARCHAR(64) NOT NULL,
first_seen BIGINT NOT NULL,
last_seen BIGINT NOT NULL,
logins INT UNSIGNED NOT NULL DEFAULT 1,
PRIMARY KEY (account_id, address),
INDEX account_login_addresses_address (address)
);

View File

@@ -0,0 +1,58 @@
/* Repeating in-game announcements scheduled on the dashboard. zones: comma separated zone IDs, '' for every world.
schedule: a cron expression or @every interval in UTC, as for scheduled tasks. starts_at/ends_at 0: no limit. */
CREATE TABLE IF NOT EXISTS scheduled_announcements (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL DEFAULT '',
message TEXT NOT NULL,
zones TEXT NOT NULL,
schedule VARCHAR(128) NOT NULL,
starts_at BIGINT NOT NULL DEFAULT 0,
ends_at BIGINT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
last_sent_at BIGINT NOT NULL DEFAULT 0,
sent_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);
/* Events: a feature name (gatingOnFeature in the levels, FeatureGating in the client database) put in a free
event_N setting from starts_at to ends_at. state: 0 scheduled, 1 active, 2 ended, 3 cancelled, 4 missed (the
dashboard was down for all of it). slot: the N it used. previous_value/previous_web_wins: the slot's
sharedconfig.ini web value before, put back at the end. */
CREATE TABLE IF NOT EXISTS scheduled_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
feature VARCHAR(128) NOT NULL,
note TEXT NOT NULL,
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
state TINYINT NOT NULL DEFAULT 0,
slot TINYINT NOT NULL DEFAULT 0,
previous_value TEXT NULL DEFAULT NULL,
previous_web_wins TINYINT NOT NULL DEFAULT 0,
status TEXT NOT NULL,
created_at BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);
/* Per-zone player caps and spare instances the master server applies to public instances of the zone. A NULL cap
uses the client's ZoneTable population_soft_cap / population_hard_cap. */
CREATE TABLE IF NOT EXISTS zone_limits (
zone_id INT UNSIGNED NOT NULL PRIMARY KEY,
soft_cap INT UNSIGNED NULL DEFAULT NULL,
hard_cap INT UNSIGNED NULL DEFAULT NULL,
spare_instances INT UNSIGNED NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);
/* Players in each world instance, sampled with server_health once a minute and kept as long (health_days). */
CREATE TABLE IF NOT EXISTS server_health_instances (
time BIGINT NOT NULL,
zone_id INT UNSIGNED NOT NULL,
instance_id INT UNSIGNED NOT NULL,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
players INT UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (time, zone_id, instance_id),
INDEX server_health_instances_zone (zone_id, time)
);

View File

@@ -0,0 +1,20 @@
/* Every change of a setting's web value, written when it is saved on the dashboard: before and after, who and when.
file_value: what the files or environment set then. Secrets (secret = 1) keep no values. removed: the setting was
forgotten entirely. revert_of: the change this one undid. */
CREATE TABLE IF NOT EXISTS server_config_history (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
file VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
old_value TEXT NULL DEFAULT NULL,
old_web_wins TINYINT NOT NULL DEFAULT 0,
new_value TEXT NULL DEFAULT NULL,
new_web_wins TINYINT NOT NULL DEFAULT 0,
file_value TEXT NULL DEFAULT NULL,
secret TINYINT NOT NULL DEFAULT 0,
removed TINYINT NOT NULL DEFAULT 0,
revert_of BIGINT UNSIGNED NOT NULL DEFAULT 0,
changed_at BIGINT NOT NULL,
account_id INT UNSIGNED NOT NULL DEFAULT 0,
changed_by VARCHAR(64) NOT NULL DEFAULT '',
INDEX server_config_history_setting (file, name, id)
);

View File

@@ -0,0 +1,18 @@
/*
* Indexes for lookups by owner/parent that otherwise scan the whole table: a property's models (and the model count
* on every property list row), a character's properties, an account's characters, a play key's accounts, friends
* both ways, a character's mail, a leaderboard's scores, and the dashboard's per-character/account related data.
* The columns' inline REFERENCES clauses in 0_initial.sql create no foreign key (and so no index) in MySQL/MariaDB.
*/
CREATE INDEX properties_contents_property ON properties_contents (property_id);
CREATE INDEX properties_contents_ugc ON properties_contents (ugc_id);
CREATE INDEX properties_owner ON properties (owner_id);
CREATE INDEX charinfo_account ON charinfo (account_id);
CREATE INDEX accounts_play_key ON accounts (play_key_id);
CREATE INDEX friends_friend ON friends (friend_id);
CREATE INDEX mail_receiver ON mail (receiver_id);
CREATE INDEX leaderboard_game ON leaderboard (game_id, character_id);
CREATE INDEX bug_reports_reporter ON bug_reports (reporter_id);
CREATE INDEX economy_flags_character ON economy_flags (character_id, id);
CREATE INDEX player_cheat_detections_account ON player_cheat_detections (account_id);
CREATE INDEX chat_log_recipient ON chat_log (recipient_id);

View File

@@ -0,0 +1,14 @@
/* Where players were, sampled every few seconds by the dashboard for the 3D world view's replays (staff only). Kept for position_history_days. */
CREATE TABLE IF NOT EXISTS player_positions (
character_id BIGINT NOT NULL,
time BIGINT NOT NULL,
zone_id INT UNSIGNED NOT NULL,
instance_id INT UNSIGNED NOT NULL,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
x FLOAT NOT NULL,
y FLOAT NOT NULL,
z FLOAT NOT NULL,
PRIMARY KEY (character_id, time),
INDEX player_positions_zone_time (zone_id, time),
INDEX player_positions_time (time)
);

View File

@@ -0,0 +1,19 @@
/* The dashboard's staff-side AI moderator helper: every suggestion asked for, with who asked, the model and tokens, kept with the item it is about. Drafts for staff only. */
CREATE TABLE IF NOT EXISTS ai_suggestions (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
kind VARCHAR(32) NOT NULL,
item_id BIGINT NOT NULL,
fingerprint CHAR(64) NOT NULL,
requested_by_id INT UNSIGNED NOT NULL DEFAULT 0,
requested_by VARCHAR(64) NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
model VARCHAR(100) NOT NULL DEFAULT '',
input_tokens INT UNSIGNED NOT NULL DEFAULT 0,
output_tokens INT UNSIGNED NOT NULL DEFAULT 0,
status TINYINT NOT NULL DEFAULT 0,
suggestion TEXT NULL,
error TEXT NULL,
context MEDIUMTEXT NULL,
INDEX ai_suggestions_item (kind, item_id, id),
INDEX ai_suggestions_time (created_at)
);

View File

@@ -0,0 +1,39 @@
/* Live events staff start from the dashboard (treasure hunts, bonus multipliers, invasions, celebrations). type names
the kind, config is its JSON settings. zones: comma separated zone IDs ('' for every world, bonus events only);
instance_id -1 for every instance of them. state: 0 running, 1 ended (time up or ended early), 2 cancelled. World
servers run each event in every matching instance and clean up after themselves at ends_at or when it is ended. */
CREATE TABLE IF NOT EXISTS live_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
type VARCHAR(32) NOT NULL,
title VARCHAR(100) NOT NULL DEFAULT '',
message TEXT NOT NULL,
zones TEXT NOT NULL,
instance_id INT NOT NULL DEFAULT -1,
config TEXT NOT NULL,
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
state TINYINT NOT NULL DEFAULT 0,
ended_at BIGINT NOT NULL DEFAULT 0,
end_reason VARCHAR(255) NOT NULL DEFAULT '',
created_by VARCHAR(64) NOT NULL DEFAULT '',
ended_by VARCHAR(64) NOT NULL DEFAULT '',
INDEX live_events_state (state)
);
/* What each world instance running an event last reported (JSON: spawned, found, kills, wave, ...). closed = 1 once
the instance stopped running it (event over or world shut down). */
CREATE TABLE IF NOT EXISTS live_event_instances (
event_id BIGINT UNSIGNED NOT NULL,
zone_id INT UNSIGNED NOT NULL,
instance_id INT UNSIGNED NOT NULL,
status TEXT NOT NULL,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, zone_id, instance_id)
);
/* Per character score in an event: treasures found, invaders smashed. */
CREATE TABLE IF NOT EXISTS live_event_scores (
event_id BIGINT UNSIGNED NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, character_id)
);

View File

@@ -0,0 +1,52 @@
/* Server-wide community challenges: reach target of a metric between starts_at and ends_at. metric_kind 0 = a player
statistic (StatisticID), 1 = a map event (IEconomyLedger::eMapEvent, optionally only lot). zones: '' for every zone.
state: 0 open, 1 completed, 2 expired (ended short of the target), 3 cancelled. milestone: the last percentage
announced. reward_items: JSON [{lot, count}] mailed to each character that contributed at least reward_min. */
CREATE TABLE IF NOT EXISTS challenges (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(100) NOT NULL,
description TEXT NOT NULL,
metric_kind TINYINT NOT NULL DEFAULT 0,
metric INT UNSIGNED NOT NULL,
lot INT NOT NULL DEFAULT 0,
zones TEXT NOT NULL,
target BIGINT NOT NULL,
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
include_staff TINYINT NOT NULL DEFAULT 0,
is_public TINYINT NOT NULL DEFAULT 1,
reward_coins BIGINT NOT NULL DEFAULT 0,
reward_items TEXT NOT NULL,
reward_min BIGINT NOT NULL DEFAULT 1,
state TINYINT NOT NULL DEFAULT 0,
milestone TINYINT UNSIGNED NOT NULL DEFAULT 0,
completed_at BIGINT NOT NULL DEFAULT 0,
rewarded_at BIGINT NOT NULL DEFAULT 0,
rewarded_count INT UNSIGNED NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT '',
INDEX challenges_state (state)
);
/* How much each character added to a challenge, counted by the world servers where the game records it. */
CREATE TABLE IF NOT EXISTS challenge_contributions (
challenge_id BIGINT UNSIGNED NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (challenge_id, character_id),
INDEX challenge_contributions_character (character_id)
);
/* Rewards given for a completed challenge: one row per character (items are mailed; coins wait until claimed_at is
set by the world the character plays in). */
CREATE TABLE IF NOT EXISTS challenge_rewards (
challenge_id BIGINT UNSIGNED NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
coins BIGINT NOT NULL DEFAULT 0,
rewarded_at BIGINT NOT NULL DEFAULT 0,
claimed_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (challenge_id, character_id),
INDEX challenge_rewards_character (character_id, claimed_at)
);

View File

@@ -0,0 +1,4 @@
/* economy_transfers.merged: 1 when new_item_id is a stack the receiver already had (the items joined it), 0 when the
hop made a new object. NULL for rows written before this was recorded. method 4 (INVENTORY_MOVE) is a move between
a character's own inventories, which also gives the item a new object id. */
ALTER TABLE economy_transfers ADD COLUMN merged TINYINT DEFAULT NULL;

View File

@@ -0,0 +1,8 @@
/* slash_commands.target_rule: who a command may be used on, by the dashboard's self and rank rules (SlashCommandLevels::eTargetRule):
'' (acts on nobody else), 'tools', 'items' or 'moderation' (on yourself with self_tools, self_items or self_moderation),
or 'others' (checked on other players only). Written by the world servers like the rest of the row.
slash_commands.follows_permission: 1 when the command uses its dashboard_permission's level (written by servers that know
this). Rows from before are 0, which is how a world tells that a server is upgrading and keeps the level each paired command
had then as an override (command_level_<name>, updated_by '[upgrade]'). */
ALTER TABLE slash_commands ADD COLUMN target_rule VARCHAR(16) NOT NULL DEFAULT '';
ALTER TABLE slash_commands ADD COLUMN follows_permission TINYINT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,4 @@
/* challenges.end_announced_at: when the dashboard told players in game that the challenge was complete (or over),
0 while that is still to do. A challenge that ends while the dashboard can't reach the worlds (it is restarting) is
announced once it can, instead of never. */
ALTER TABLE challenges ADD COLUMN end_announced_at BIGINT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,11 @@
/* featured_properties: what each slot of the news screen's "Today's Top Properties" panel shows, set on the dashboard.
template_id: the slot, a PropertyTemplate id (one per small property world the client's news screen knows).
mode: 0 the world's approved public property with the most reputation, 1 property_id, 2 nothing.
A slot without a row is mode 0. */
CREATE TABLE IF NOT EXISTS featured_properties (
template_id INT UNSIGNED NOT NULL PRIMARY KEY,
mode TINYINT UNSIGNED NOT NULL DEFAULT 0,
property_id BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,9 @@
/* Property worlds run one instance per property, all on the same zone: map events and player statistics now also
record the instance's clone id (the property owner's charinfo.prop_clone_id, as the world knows it), so one
property's data can be told apart from another's. 0 on every other zone, and on the rows written before this, which
the reports show as "unknown property" on property zones.
The map event kinds grow too: 8 = powerup dropped (lot = the powerup), 9 = powerup picked up. */
ALTER TABLE map_events_daily ADD COLUMN clone_id INT UNSIGNED NOT NULL DEFAULT 0,
DROP PRIMARY KEY, ADD PRIMARY KEY (zone, kind, day, clone_id, lot, cell_x, cell_z);
ALTER TABLE player_stats_daily ADD COLUMN clone_id INT UNSIGNED NOT NULL DEFAULT 0,
DROP PRIMARY KEY, ADD PRIMARY KEY (day, zone, clone_id, stat, gm);

View File

@@ -0,0 +1,21 @@
/* vanity_events: changes to the vanity NPCs that are only there while an event is on (see dCommon/VanityEvents.h).
file: a vanity file (in vanity/, not loaded by root.xml) whose NPCs are added, replacing base NPCs of the same name.
removals: NPC names taken out while it is on, one per line.
schedule: JSON rules ({utcOffset, match, rules}); priority: higher is laid on later and wins.
mode: 0 off, 1 on by its schedule, 2 always on.
applied: 1 when the dashboard last respawned the worlds with this event on. */
CREATE TABLE IF NOT EXISTS vanity_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
note TEXT NOT NULL,
file VARCHAR(64) NOT NULL DEFAULT '',
removals TEXT NOT NULL,
schedule TEXT NOT NULL,
priority INT NOT NULL DEFAULT 0,
mode TINYINT UNSIGNED NOT NULL DEFAULT 0,
applied TINYINT NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by VARCHAR(64) NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,11 @@
/* featured_properties.zone_id: the property world a "Today's Top Properties" slot shows a property of (any world with a
property entrance); 0, as on every row from before, is the slot's own world.
featured_properties_settings: one row (id 1). full_auto 1: the slots' rows are ignored and the panel shows the four
approved public properties with the most reputation across every property world. No row: full_auto 0. */
ALTER TABLE featured_properties ADD COLUMN zone_id INT UNSIGNED NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS featured_properties_settings (
id TINYINT UNSIGNED NOT NULL PRIMARY KEY,
full_auto TINYINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by VARCHAR(64) NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,4 @@
/* vanity_events.file_switches: vanity files the event switches on or off while it is on, as JSON
{"halloween.xml": true, "summer.xml": false}, overriding the <file enabled> entries that name them (see
dCommon/VanityEvents.h). Empty: none, as on every row from before. */
ALTER TABLE vanity_events ADD COLUMN file_switches TEXT NOT NULL DEFAULT '';

View File

@@ -0,0 +1,53 @@
/* Saved game message inspector captures (see dDashboardServer/routes/Inspector.h). One row per capture, written when it
starts and kept up to date while it runs; ended_at is 0 until it ends. only_messages / skip_messages: the capture's
message ID filters, comma separated. zones: the worlds it was captured in, in order, as zone:instance:clone separated by
spaces. byte_count: bytes stored for its messages (raw bytes plus decoded fields). The Message capture pruning task
deletes old sessions (inspector_session_days, inspector_max_mb). */
CREATE TABLE IF NOT EXISTS message_capture_sessions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
character_id BIGINT NOT NULL,
character_name VARCHAR(64) NOT NULL DEFAULT '',
account_id INT UNSIGNED NOT NULL DEFAULT 0,
account_name VARCHAR(64) NOT NULL DEFAULT '',
started_by_id INT UNSIGNED NOT NULL DEFAULT 0,
started_by VARCHAR(64) NOT NULL DEFAULT '',
started_at BIGINT NOT NULL DEFAULT 0,
ends_at BIGINT NOT NULL DEFAULT 0,
ended_at BIGINT NOT NULL DEFAULT 0,
end_reason VARCHAR(255) NOT NULL DEFAULT '',
to_server TINYINT NOT NULL DEFAULT 1,
to_client TINYINT NOT NULL DEFAULT 1,
only_messages TEXT NOT NULL,
skip_messages TEXT NOT NULL,
zone_id INT UNSIGNED NOT NULL DEFAULT 0,
instance_id INT UNSIGNED NOT NULL DEFAULT 0,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
zones TEXT NOT NULL,
message_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
byte_count BIGINT UNSIGNED NOT NULL DEFAULT 0,
dropped BIGINT UNSIGNED NOT NULL DEFAULT 0,
INDEX message_capture_sessions_character (character_id),
INDEX message_capture_sessions_account (account_id),
INDEX message_capture_sessions_started (started_at)
);
/* The messages of a saved capture. seq: the dashboard's number for the message in its session, from 1.
dropped_before: messages the world left out just before this one (too many at once). direction: 0 sent by the client,
1 sent to it. payload: the message's bytes after the object and message ID (at most 2 KB; bits is its full size).
decoded: its fields as JSON when the world server had a typed struct for it, else empty. */
CREATE TABLE IF NOT EXISTS message_capture_entries (
session_id BIGINT UNSIGNED NOT NULL,
seq INT UNSIGNED NOT NULL,
time_ms BIGINT NOT NULL,
direction TINYINT UNSIGNED NOT NULL,
message_id SMALLINT UNSIGNED NOT NULL,
object_id BIGINT NOT NULL,
bits INT UNSIGNED NOT NULL,
dropped_before INT UNSIGNED NOT NULL DEFAULT 0,
zone_id INT UNSIGNED NOT NULL DEFAULT 0,
instance_id INT UNSIGNED NOT NULL DEFAULT 0,
clone_id INT UNSIGNED NOT NULL DEFAULT 0,
payload BLOB NOT NULL,
decoded TEXT NOT NULL,
PRIMARY KEY (session_id, seq)
);

View File

@@ -0,0 +1,23 @@
/* Scheduled events become one system: an event has a name, a mode (0 off, 1 on by its schedule, 2 always on), a
schedule (recurring rules as JSON, see dCommon/ScheduleRules.h; empty: once, from starts_at to ends_at), a priority
and parts that switch on and off together (JSON [{kind, config, applied, state, status}], see
dDashboardServer/routes/EventParts.h): a feature flag, vanity changes, a live event, announcements, a restart.
The old columns feature, slot, previous_value and previous_web_wins are no longer read: each calendar event becomes
an event with one feature part holding them. Each vanity event becomes an event with one vanity part; the
vanity_events table is no longer read either (kept, so nothing is lost). */
ALTER TABLE scheduled_events ADD COLUMN name VARCHAR(100) NOT NULL DEFAULT '';
ALTER TABLE scheduled_events ADD COLUMN mode TINYINT UNSIGNED NOT NULL DEFAULT 1;
ALTER TABLE scheduled_events ADD COLUMN schedule TEXT NOT NULL DEFAULT '';
ALTER TABLE scheduled_events ADD COLUMN priority INT NOT NULL DEFAULT 0;
ALTER TABLE scheduled_events ADD COLUMN parts TEXT NOT NULL DEFAULT '';
UPDATE scheduled_events SET name = feature, mode = CASE WHEN state = 3 THEN 0 ELSE 1 END,
parts = JSON_ARRAY(JSON_OBJECT('kind', 'feature', 'config', JSON_OBJECT('feature', feature), 'applied', state = 1,
'state', JSON_OBJECT('slot', slot, 'previousValue', previous_value, 'previousWebWins', previous_web_wins <> 0), 'status', status))
WHERE parts = '' AND feature <> '';
INSERT INTO scheduled_events (feature, name, note, mode, schedule, starts_at, ends_at, priority, parts, state, status, created_at, created_by, updated_at, updated_by)
SELECT '', name, note, mode, schedule, 0, 0, priority,
JSON_ARRAY(JSON_OBJECT('kind', 'vanity', 'config', JSON_OBJECT('file', file, 'removals', removals,
'fileSwitches', CASE WHEN file_switches = '' THEN JSON_OBJECT() ELSE JSON_EXTRACT(file_switches, '$') END),
'applied', applied <> 0, 'state', JSON_OBJECT(), 'status', '')),
CASE WHEN applied <> 0 THEN 1 ELSE 0 END, '', created_at, created_by, updated_at, updated_by
FROM vanity_events;

View File

@@ -0,0 +1,6 @@
/* Migration: Add login tracking columns to accounts table */
/* Adds fields for tracking login attempts, lockouts, and last login */
ALTER TABLE accounts ADD COLUMN failed_attempts INTEGER NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN lockout_time DATETIME DEFAULT NULL;
ALTER TABLE accounts ADD COLUMN last_login DATETIME DEFAULT NULL;

View File

@@ -0,0 +1,8 @@
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL DEFAULT 0,
account_name TEXT NOT NULL DEFAULT '',
action TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
timestamp BIGINT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,5 @@
/* Columns used by the web dashboard. Names match NexusDashboard. */
ALTER TABLE play_keys ADD COLUMN notes TEXT DEFAULT NULL;
ALTER TABLE bug_reports ADD COLUMN resolved_time DATETIME DEFAULT NULL;
ALTER TABLE bug_reports ADD COLUMN resoleved_by_id INTEGER DEFAULT NULL;
ALTER TABLE bug_reports ADD COLUMN resolution TEXT DEFAULT NULL;

View File

@@ -0,0 +1,13 @@
/* Account email for password resets. Column names match NexusDashboard. */
ALTER TABLE accounts ADD COLUMN email TEXT DEFAULT '';
ALTER TABLE accounts ADD COLUMN email_confirmed_at DATETIME DEFAULT NULL;
/* Sessions issued before this unix time are rejected, e.g. after a password reset */
ALTER TABLE accounts ADD COLUMN sessions_valid_after BIGINT NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS account_tokens (
token_hash TEXT NOT NULL PRIMARY KEY,
account_id INTEGER NOT NULL,
purpose TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '',
expires_at BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS account_tokens_account ON account_tokens (account_id, purpose);

View File

@@ -0,0 +1,57 @@
/* Economy ledger for dashboard reports. See the MySQL migration for details. */
CREATE TABLE IF NOT EXISTS economy_currency_daily (
day INTEGER NOT NULL,
character_id BIGINT NOT NULL,
source INTEGER NOT NULL,
gained BIGINT NOT NULL DEFAULT 0,
spent BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, character_id, source)
);
CREATE INDEX IF NOT EXISTS economy_currency_character ON economy_currency_daily (character_id, day);
CREATE TABLE IF NOT EXISTS economy_uscore_daily (
day INTEGER NOT NULL,
character_id BIGINT NOT NULL,
source INTEGER NOT NULL,
gained BIGINT NOT NULL DEFAULT 0,
lost BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, character_id, source)
);
CREATE TABLE IF NOT EXISTS economy_items_daily (
day INTEGER NOT NULL,
lot INTEGER NOT NULL,
source INTEGER NOT NULL,
gm INTEGER NOT NULL DEFAULT 0,
created BIGINT NOT NULL DEFAULT 0,
destroyed BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, lot, source, gm)
);
CREATE INDEX IF NOT EXISTS economy_items_lot ON economy_items_daily (lot, day);
CREATE TABLE IF NOT EXISTS economy_transfers (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
time BIGINT NOT NULL,
method INTEGER NOT NULL,
item_id BIGINT NOT NULL DEFAULT 0,
new_item_id BIGINT NOT NULL DEFAULT 0,
lot INTEGER NOT NULL DEFAULT 0,
count INTEGER NOT NULL DEFAULT 0,
coins BIGINT NOT NULL DEFAULT 0,
from_character BIGINT NOT NULL DEFAULT 0,
to_character BIGINT NOT NULL DEFAULT 0,
zone INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS economy_transfers_item ON economy_transfers (item_id);
CREATE INDEX IF NOT EXISTS economy_transfers_new_item ON economy_transfers (new_item_id);
CREATE INDEX IF NOT EXISTS economy_transfers_from ON economy_transfers (from_character, time);
CREATE INDEX IF NOT EXISTS economy_transfers_to ON economy_transfers (to_character, time);
CREATE TABLE IF NOT EXISTS map_events_daily (
day INTEGER NOT NULL,
zone INTEGER NOT NULL,
kind INTEGER NOT NULL,
lot INTEGER NOT NULL,
cell_x INTEGER NOT NULL,
cell_z INTEGER NOT NULL,
events BIGINT NOT NULL DEFAULT 0,
quantity BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (zone, kind, day, lot, cell_x, cell_z)
);
ALTER TABLE mail ADD COLUMN attachment_config TEXT DEFAULT NULL;

View File

@@ -0,0 +1,46 @@
/* Outgoing webhooks, economy anomaly flags, dashboard state and two-factor login. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS dashboard_webhooks (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
url TEXT NOT NULL,
format TEXT NOT NULL DEFAULT 'discord',
events TEXT NOT NULL,
secret TEXT NOT NULL DEFAULT '',
enabled INTEGER NOT NULL DEFAULT 1,
created_at BIGINT NOT NULL,
last_sent_at BIGINT NOT NULL DEFAULT 0,
last_status INTEGER NOT NULL DEFAULT 0,
last_error TEXT NULL
);
CREATE TABLE IF NOT EXISTS economy_flags (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
created_at BIGINT NOT NULL,
day INTEGER NOT NULL,
kind INTEGER NOT NULL,
character_id BIGINT NOT NULL DEFAULT 0,
lot INTEGER NOT NULL DEFAULT 0,
item_id BIGINT NOT NULL DEFAULT 0,
value BIGINT NOT NULL DEFAULT 0,
baseline BIGINT NOT NULL DEFAULT 0,
details TEXT NULL,
status INTEGER NOT NULL DEFAULT 0,
reviewed_by INTEGER NOT NULL DEFAULT 0,
reviewed_at BIGINT NOT NULL DEFAULT 0,
note TEXT NULL,
UNIQUE (kind, day, character_id, lot, item_id)
);
CREATE INDEX IF NOT EXISTS economy_flags_status ON economy_flags (status, id);
CREATE TABLE IF NOT EXISTS dashboard_state (
name TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);
ALTER TABLE accounts ADD COLUMN totp_secret TEXT NULL;
ALTER TABLE accounts ADD COLUMN totp_enabled_at BIGINT NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN totp_last_step BIGINT NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS account_recovery_codes (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
code_hash TEXT NOT NULL,
used_at BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS account_recovery_codes_account ON account_recovery_codes (account_id);

View File

@@ -0,0 +1,4 @@
/* Lets old command log rows be pruned by age. Rows written before this have time 0 and are kept. */
ALTER TABLE command_log ADD COLUMN time BIGINT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS command_log_time ON command_log (time);
CREATE INDEX IF NOT EXISTS activity_log_time ON activity_log (time);

View File

@@ -0,0 +1,15 @@
/* Server settings in the database, editable from the dashboard. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS server_config (
file TEXT NOT NULL,
name TEXT NOT NULL,
file_value TEXT NULL,
file_source TEXT NOT NULL DEFAULT '',
web_value TEXT NULL,
web_wins INTEGER NOT NULL DEFAULT 0,
secret INTEGER NOT NULL DEFAULT 0,
description TEXT NULL,
seen_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT '',
PRIMARY KEY (file, name)
);

View File

@@ -0,0 +1,23 @@
/* Periodic dashboard tasks and their run history. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS scheduled_tasks (
name TEXT NOT NULL PRIMARY KEY,
schedule TEXT NULL,
enabled INTEGER NOT NULL DEFAULT 1,
last_scheduled_at BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS scheduled_task_runs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
task TEXT NOT NULL,
task_trigger TEXT NOT NULL,
actor TEXT NOT NULL DEFAULT '',
started_at BIGINT NOT NULL,
finished_at BIGINT NOT NULL,
status INTEGER NOT NULL,
summary TEXT NULL,
log TEXT NULL
);
CREATE INDEX IF NOT EXISTS scheduled_task_runs_task ON scheduled_task_runs (task, id);
CREATE INDEX IF NOT EXISTS scheduled_task_runs_finished ON scheduled_task_runs (finished_at);

View File

@@ -0,0 +1,13 @@
/* Earlier versions of characters' XML. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS character_snapshots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
character_id BIGINT NOT NULL,
taken_at BIGINT NOT NULL,
reason TEXT NOT NULL DEFAULT '',
actor TEXT NOT NULL DEFAULT '',
size INTEGER NOT NULL DEFAULT 0,
hash TEXT NOT NULL DEFAULT '',
xml BLOB NOT NULL
);
CREATE INDEX IF NOT EXISTS character_snapshots_character ON character_snapshots (character_id, id);
CREATE INDEX IF NOT EXISTS character_snapshots_taken ON character_snapshots (taken_at);

View File

@@ -0,0 +1,12 @@
/* Moderation history per account, and temporary bans. See the MySQL migration. */
ALTER TABLE accounts ADD COLUMN ban_expires BIGINT NOT NULL DEFAULT 0;
ALTER TABLE accounts ADD COLUMN ban_reason TEXT NULL;
CREATE TABLE IF NOT EXISTS account_notes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
kind TEXT NOT NULL,
text TEXT NOT NULL,
actor TEXT NOT NULL DEFAULT '',
created_at BIGINT NOT NULL
);
CREATE INDEX IF NOT EXISTS account_notes_account ON account_notes (account_id, id);

View File

@@ -0,0 +1,9 @@
/* Server health samples. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS server_health (
time BIGINT NOT NULL PRIMARY KEY,
players INTEGER NOT NULL DEFAULT 0,
worlds INTEGER NOT NULL DEFAULT 0,
auth_online INTEGER NOT NULL DEFAULT 0,
chat_online INTEGER NOT NULL DEFAULT 0,
memory_kb BIGINT NOT NULL DEFAULT 0
);

View File

@@ -0,0 +1,11 @@
/* Moderators' decisions on name requests and pet names. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS moderation_decisions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
subject_id BIGINT NOT NULL,
subject TEXT NOT NULL DEFAULT '',
approved INTEGER NOT NULL DEFAULT 0,
reason TEXT NOT NULL DEFAULT '',
decided_at BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS moderation_decisions_subject ON moderation_decisions (kind, subject_id);

View File

@@ -0,0 +1,3 @@
/* Who owns each named pet. See the MySQL migration. */
ALTER TABLE pet_names ADD COLUMN owner_id BIGINT NULL DEFAULT NULL;
CREATE INDEX IF NOT EXISTS pet_names_owner ON pet_names (owner_id);

View File

@@ -0,0 +1,4 @@
/* What a dashboard action was about. See the MySQL migration. */
ALTER TABLE audit_log ADD COLUMN target_account_id INTEGER NOT NULL DEFAULT 0;
ALTER TABLE audit_log ADD COLUMN target_character_id BIGINT NOT NULL DEFAULT 0;
CREATE INDEX IF NOT EXISTS audit_log_target_account ON audit_log (target_account_id);

View File

@@ -0,0 +1,19 @@
/* Chat, for moderation and chat bridges. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS chat_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
time BIGINT NOT NULL,
channel TEXT NOT NULL,
sender_id BIGINT NOT NULL DEFAULT 0,
sender_name TEXT NOT NULL DEFAULT '',
account_id INTEGER NOT NULL DEFAULT 0,
recipient_id BIGINT NOT NULL DEFAULT 0,
recipient_name TEXT NOT NULL DEFAULT '',
zone_id INTEGER NOT NULL DEFAULT 0,
instance_id INTEGER NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
message TEXT NOT NULL,
blocked INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS chat_log_time ON chat_log (time);
CREATE INDEX IF NOT EXISTS chat_log_sender ON chat_log (sender_id);
CREATE INDEX IF NOT EXISTS chat_log_account ON chat_log (account_id);

View File

@@ -0,0 +1,5 @@
/* Each account's dashboard view choices. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS dashboard_preferences (
account_id INTEGER NOT NULL PRIMARY KEY,
prefs TEXT NOT NULL
);

View File

@@ -0,0 +1,16 @@
/* Strikes against accounts. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS account_strikes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
account_id INTEGER NOT NULL,
character_id BIGINT NOT NULL DEFAULT 0,
source TEXT NOT NULL,
subject TEXT NOT NULL DEFAULT '',
reason TEXT NOT NULL DEFAULT '',
given_by_id INTEGER NOT NULL DEFAULT 0,
given_by TEXT NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
revoked_at BIGINT NOT NULL DEFAULT 0,
revoked_by TEXT NOT NULL DEFAULT '',
revoke_reason TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS account_strikes_account ON account_strikes (account_id);

View File

@@ -0,0 +1,9 @@
/* Daily totals of player statistics. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS player_stats_daily (
day INTEGER NOT NULL,
zone INTEGER NOT NULL,
stat INTEGER NOT NULL,
gm INTEGER NOT NULL DEFAULT 0,
amount BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (day, zone, stat, gm)
);

View File

@@ -0,0 +1,13 @@
/* The in-game slash commands as the world servers registered them. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS slash_commands (
name TEXT NOT NULL PRIMARY KEY,
aliases TEXT NOT NULL DEFAULT '',
help TEXT NOT NULL DEFAULT '',
info TEXT NOT NULL DEFAULT '',
default_level INTEGER NOT NULL,
min_level INTEGER NOT NULL DEFAULT 0,
fixed INTEGER NOT NULL DEFAULT 0,
client_handled INTEGER NOT NULL DEFAULT 0,
note TEXT NOT NULL DEFAULT '',
dashboard_permission TEXT NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,33 @@
/* Strike thresholds, player reports and chat filter words. See the MySQL migration. */
ALTER TABLE account_strikes ADD COLUMN step TEXT NOT NULL DEFAULT '';
ALTER TABLE account_strikes ADD COLUMN step_count INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS player_reports (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at BIGINT NOT NULL,
kind TEXT NOT NULL,
reporter_id BIGINT NOT NULL DEFAULT 0,
reporter_account_id INTEGER NOT NULL DEFAULT 0,
object_id BIGINT NOT NULL DEFAULT 0,
object_lot INTEGER NOT NULL DEFAULT 0,
target_character_id BIGINT NOT NULL DEFAULT 0,
target_account_id INTEGER NOT NULL DEFAULT 0,
property_id BIGINT NOT NULL DEFAULT 0,
zone_id INTEGER NOT NULL DEFAULT 0,
instance_id INTEGER NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
body TEXT NOT NULL DEFAULT '',
status INTEGER NOT NULL DEFAULT 0,
handled_by TEXT NOT NULL DEFAULT '',
handled_at BIGINT NOT NULL DEFAULT 0,
resolution TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS player_reports_status ON player_reports (status, id);
CREATE INDEX IF NOT EXISTS player_reports_target ON player_reports (target_account_id);
CREATE TABLE IF NOT EXISTS chat_filter_words (
word TEXT NOT NULL PRIMARY KEY,
allowed INTEGER NOT NULL,
added_by TEXT NOT NULL DEFAULT '',
added_at BIGINT NOT NULL
);

View File

@@ -0,0 +1,10 @@
/* Where accounts log in to the game from. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS account_login_addresses (
account_id INTEGER NOT NULL,
address TEXT NOT NULL,
first_seen BIGINT NOT NULL,
last_seen BIGINT NOT NULL,
logins INTEGER NOT NULL DEFAULT 1,
PRIMARY KEY (account_id, address)
);
CREATE INDEX IF NOT EXISTS account_login_addresses_address ON account_login_addresses (address);

View File

@@ -0,0 +1,50 @@
/* Scheduled announcements, events, zone limits and per-instance player samples. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS scheduled_announcements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
zones TEXT NOT NULL DEFAULT '',
schedule TEXT NOT NULL,
starts_at BIGINT NOT NULL DEFAULT 0,
ends_at BIGINT NOT NULL DEFAULT 0,
enabled INTEGER NOT NULL DEFAULT 1,
last_sent_at BIGINT NOT NULL DEFAULT 0,
sent_count INTEGER NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS scheduled_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
feature TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
state INTEGER NOT NULL DEFAULT 0,
slot INTEGER NOT NULL DEFAULT 0,
previous_value TEXT NULL DEFAULT NULL,
previous_web_wins INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL DEFAULT '',
created_at BIGINT NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS zone_limits (
zone_id INTEGER NOT NULL PRIMARY KEY,
soft_cap INTEGER NULL DEFAULT NULL,
hard_cap INTEGER NULL DEFAULT NULL,
spare_instances INTEGER NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);
CREATE TABLE IF NOT EXISTS server_health_instances (
time BIGINT NOT NULL,
zone_id INTEGER NOT NULL,
instance_id INTEGER NOT NULL,
clone_id INTEGER NOT NULL DEFAULT 0,
players INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (time, zone_id, instance_id)
);
CREATE INDEX IF NOT EXISTS server_health_instances_zone ON server_health_instances (zone_id, time);

View File

@@ -0,0 +1,18 @@
/* Every change of a setting's web value. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS server_config_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file TEXT NOT NULL,
name TEXT NOT NULL,
old_value TEXT NULL DEFAULT NULL,
old_web_wins INTEGER NOT NULL DEFAULT 0,
new_value TEXT NULL DEFAULT NULL,
new_web_wins INTEGER NOT NULL DEFAULT 0,
file_value TEXT NULL DEFAULT NULL,
secret INTEGER NOT NULL DEFAULT 0,
removed INTEGER NOT NULL DEFAULT 0,
revert_of BIGINT NOT NULL DEFAULT 0,
changed_at BIGINT NOT NULL,
account_id INTEGER NOT NULL DEFAULT 0,
changed_by TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS server_config_history_setting ON server_config_history (file, name, id);

View File

@@ -0,0 +1,13 @@
/* Indexes for lookups by owner/parent that otherwise scan the whole table. See the MySQL migration. */
CREATE INDEX IF NOT EXISTS properties_contents_property ON properties_contents (property_id);
CREATE INDEX IF NOT EXISTS properties_contents_ugc ON properties_contents (ugc_id);
CREATE INDEX IF NOT EXISTS properties_owner ON properties (owner_id);
CREATE INDEX IF NOT EXISTS charinfo_account ON charinfo (account_id);
CREATE INDEX IF NOT EXISTS accounts_play_key ON accounts (play_key_id);
CREATE INDEX IF NOT EXISTS friends_friend ON friends (friend_id);
CREATE INDEX IF NOT EXISTS mail_receiver ON mail (receiver_id);
CREATE INDEX IF NOT EXISTS leaderboard_game ON leaderboard (game_id, character_id);
CREATE INDEX IF NOT EXISTS bug_reports_reporter ON bug_reports (reporter_id);
CREATE INDEX IF NOT EXISTS economy_flags_character ON economy_flags (character_id, id);
CREATE INDEX IF NOT EXISTS player_cheat_detections_account ON player_cheat_detections (account_id);
CREATE INDEX IF NOT EXISTS chat_log_recipient ON chat_log (recipient_id);

View File

@@ -0,0 +1,14 @@
/* Player position samples for replays. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS player_positions (
character_id BIGINT NOT NULL,
time BIGINT NOT NULL,
zone_id INTEGER NOT NULL,
instance_id INTEGER NOT NULL,
clone_id INTEGER NOT NULL DEFAULT 0,
x REAL NOT NULL,
y REAL NOT NULL,
z REAL NOT NULL,
PRIMARY KEY (character_id, time)
);
CREATE INDEX IF NOT EXISTS player_positions_zone_time ON player_positions (zone_id, time);
CREATE INDEX IF NOT EXISTS player_positions_time ON player_positions (time);

View File

@@ -0,0 +1,19 @@
/* AI moderator helper suggestions (staff only). See the MySQL migration. */
CREATE TABLE IF NOT EXISTS ai_suggestions (
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
kind TEXT NOT NULL,
item_id BIGINT NOT NULL,
fingerprint TEXT NOT NULL,
requested_by_id INTEGER NOT NULL DEFAULT 0,
requested_by TEXT NOT NULL DEFAULT '',
created_at BIGINT NOT NULL,
model TEXT NOT NULL DEFAULT '',
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
status INTEGER NOT NULL DEFAULT 0,
suggestion TEXT NULL,
error TEXT NULL,
context TEXT NULL
);
CREATE INDEX IF NOT EXISTS ai_suggestions_item ON ai_suggestions (kind, item_id, id);
CREATE INDEX IF NOT EXISTS ai_suggestions_time ON ai_suggestions (created_at);

View File

@@ -0,0 +1,33 @@
/* Live events started from the dashboard. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS live_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
title TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL DEFAULT '',
zones TEXT NOT NULL DEFAULT '',
instance_id INTEGER NOT NULL DEFAULT -1,
config TEXT NOT NULL DEFAULT '{}',
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
state INTEGER NOT NULL DEFAULT 0,
ended_at BIGINT NOT NULL DEFAULT 0,
end_reason TEXT NOT NULL DEFAULT '',
created_by TEXT NOT NULL DEFAULT '',
ended_by TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS live_events_state ON live_events (state);
CREATE TABLE IF NOT EXISTS live_event_instances (
event_id INTEGER NOT NULL,
zone_id INTEGER NOT NULL,
instance_id INTEGER NOT NULL,
status TEXT NOT NULL DEFAULT '{}',
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, zone_id, instance_id)
);
CREATE TABLE IF NOT EXISTS live_event_scores (
event_id INTEGER NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (event_id, character_id)
);

View File

@@ -0,0 +1,46 @@
/* Server-wide community challenges. See the MySQL migration. */
CREATE TABLE IF NOT EXISTS challenges (
id INTEGER PRIMARY KEY AUTOINCREMENT,
title TEXT NOT NULL,
description TEXT NOT NULL DEFAULT '',
metric_kind INTEGER NOT NULL DEFAULT 0,
metric INTEGER NOT NULL,
lot INTEGER NOT NULL DEFAULT 0,
zones TEXT NOT NULL DEFAULT '',
target BIGINT NOT NULL,
starts_at BIGINT NOT NULL,
ends_at BIGINT NOT NULL,
include_staff INTEGER NOT NULL DEFAULT 0,
is_public INTEGER NOT NULL DEFAULT 1,
reward_coins BIGINT NOT NULL DEFAULT 0,
reward_items TEXT NOT NULL DEFAULT '[]',
reward_min BIGINT NOT NULL DEFAULT 1,
state INTEGER NOT NULL DEFAULT 0,
milestone INTEGER NOT NULL DEFAULT 0,
completed_at BIGINT NOT NULL DEFAULT 0,
rewarded_at BIGINT NOT NULL DEFAULT 0,
rewarded_count INTEGER NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);
CREATE INDEX IF NOT EXISTS challenges_state ON challenges (state);
CREATE TABLE IF NOT EXISTS challenge_contributions (
challenge_id INTEGER NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (challenge_id, character_id)
);
CREATE INDEX IF NOT EXISTS challenge_contributions_character ON challenge_contributions (character_id);
CREATE TABLE IF NOT EXISTS challenge_rewards (
challenge_id INTEGER NOT NULL,
character_id BIGINT NOT NULL,
amount BIGINT NOT NULL DEFAULT 0,
coins BIGINT NOT NULL DEFAULT 0,
rewarded_at BIGINT NOT NULL DEFAULT 0,
claimed_at BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY (challenge_id, character_id)
);
CREATE INDEX IF NOT EXISTS challenge_rewards_character ON challenge_rewards (character_id, claimed_at);

View File

@@ -0,0 +1,2 @@
/* economy_transfers.merged: see the MySQL migration. */
ALTER TABLE economy_transfers ADD COLUMN merged INTEGER DEFAULT NULL;

View File

@@ -0,0 +1,3 @@
/* slash_commands.target_rule and follows_permission: see the MySQL migration. */
ALTER TABLE slash_commands ADD COLUMN target_rule TEXT NOT NULL DEFAULT '';
ALTER TABLE slash_commands ADD COLUMN follows_permission INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,2 @@
/* challenges.end_announced_at: see the MySQL migration. */
ALTER TABLE challenges ADD COLUMN end_announced_at BIGINT NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,8 @@
/* featured_properties: see the MySQL migration. */
CREATE TABLE IF NOT EXISTS featured_properties (
template_id INTEGER NOT NULL PRIMARY KEY,
mode INTEGER NOT NULL DEFAULT 0,
property_id BIGINT NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,33 @@
/* map_events_daily and player_stats_daily get clone_id in their keys: see the MySQL migration. SQLite can't change a
primary key, so the tables are rebuilt. */
BEGIN TRANSACTION;
CREATE TABLE map_events_daily_2 (
day INTEGER NOT NULL,
zone INTEGER NOT NULL,
kind INTEGER NOT NULL,
lot INTEGER NOT NULL,
cell_x INTEGER NOT NULL,
cell_z INTEGER NOT NULL,
events BIGINT NOT NULL DEFAULT 0,
quantity BIGINT NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (zone, kind, day, clone_id, lot, cell_x, cell_z)
);
INSERT INTO map_events_daily_2 (day, zone, kind, lot, cell_x, cell_z, events, quantity)
SELECT day, zone, kind, lot, cell_x, cell_z, events, quantity FROM map_events_daily;
DROP TABLE map_events_daily;
ALTER TABLE map_events_daily_2 RENAME TO map_events_daily;
CREATE TABLE player_stats_daily_2 (
day INTEGER NOT NULL,
zone INTEGER NOT NULL,
stat INTEGER NOT NULL,
gm INTEGER NOT NULL DEFAULT 0,
amount BIGINT NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
PRIMARY KEY (day, zone, clone_id, stat, gm)
);
INSERT INTO player_stats_daily_2 (day, zone, stat, gm, amount)
SELECT day, zone, stat, gm, amount FROM player_stats_daily;
DROP TABLE player_stats_daily;
ALTER TABLE player_stats_daily_2 RENAME TO player_stats_daily;
COMMIT;

View File

@@ -0,0 +1,16 @@
/* vanity_events: see the MySQL migration. */
CREATE TABLE IF NOT EXISTS vanity_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
file TEXT NOT NULL DEFAULT '',
removals TEXT NOT NULL DEFAULT '',
schedule TEXT NOT NULL DEFAULT '',
priority INTEGER NOT NULL DEFAULT 0,
mode INTEGER NOT NULL DEFAULT 0,
applied INTEGER NOT NULL DEFAULT 0,
created_at BIGINT NOT NULL DEFAULT 0,
created_by TEXT NOT NULL DEFAULT '',
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,8 @@
/* featured_properties.zone_id and featured_properties_settings: see the MySQL migration. */
ALTER TABLE featured_properties ADD COLUMN zone_id INTEGER NOT NULL DEFAULT 0;
CREATE TABLE IF NOT EXISTS featured_properties_settings (
id INTEGER NOT NULL PRIMARY KEY,
full_auto INTEGER NOT NULL DEFAULT 0,
updated_at BIGINT NOT NULL DEFAULT 0,
updated_by TEXT NOT NULL DEFAULT ''
);

View File

@@ -0,0 +1,2 @@
/* vanity_events.file_switches: see the MySQL migration. */
ALTER TABLE vanity_events ADD COLUMN file_switches TEXT NOT NULL DEFAULT '';

View File

@@ -0,0 +1,45 @@
/* message_capture_sessions and message_capture_entries: see the MySQL migration. */
CREATE TABLE IF NOT EXISTS message_capture_sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
character_id BIGINT NOT NULL,
character_name TEXT NOT NULL DEFAULT '',
account_id INTEGER NOT NULL DEFAULT 0,
account_name TEXT NOT NULL DEFAULT '',
started_by_id INTEGER NOT NULL DEFAULT 0,
started_by TEXT NOT NULL DEFAULT '',
started_at BIGINT NOT NULL DEFAULT 0,
ends_at BIGINT NOT NULL DEFAULT 0,
ended_at BIGINT NOT NULL DEFAULT 0,
end_reason TEXT NOT NULL DEFAULT '',
to_server INTEGER NOT NULL DEFAULT 1,
to_client INTEGER NOT NULL DEFAULT 1,
only_messages TEXT NOT NULL DEFAULT '',
skip_messages TEXT NOT NULL DEFAULT '',
zone_id INTEGER NOT NULL DEFAULT 0,
instance_id INTEGER NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
zones TEXT NOT NULL DEFAULT '',
message_count BIGINT NOT NULL DEFAULT 0,
byte_count BIGINT NOT NULL DEFAULT 0,
dropped BIGINT NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS message_capture_sessions_character ON message_capture_sessions (character_id);
CREATE INDEX IF NOT EXISTS message_capture_sessions_account ON message_capture_sessions (account_id);
CREATE INDEX IF NOT EXISTS message_capture_sessions_started ON message_capture_sessions (started_at);
CREATE TABLE IF NOT EXISTS message_capture_entries (
session_id BIGINT NOT NULL,
seq INTEGER NOT NULL,
time_ms BIGINT NOT NULL,
direction INTEGER NOT NULL,
message_id INTEGER NOT NULL,
object_id BIGINT NOT NULL,
bits INTEGER NOT NULL,
dropped_before INTEGER NOT NULL DEFAULT 0,
zone_id INTEGER NOT NULL DEFAULT 0,
instance_id INTEGER NOT NULL DEFAULT 0,
clone_id INTEGER NOT NULL DEFAULT 0,
payload BLOB NOT NULL,
decoded TEXT NOT NULL DEFAULT '',
PRIMARY KEY (session_id, seq)
);

View File

@@ -0,0 +1,18 @@
/* scheduled_events name, mode, schedule, priority and parts, and the calendar and vanity events moved into them: see
the MySQL migration. */
ALTER TABLE scheduled_events ADD COLUMN name TEXT NOT NULL DEFAULT '';
ALTER TABLE scheduled_events ADD COLUMN mode INTEGER NOT NULL DEFAULT 1;
ALTER TABLE scheduled_events ADD COLUMN schedule TEXT NOT NULL DEFAULT '';
ALTER TABLE scheduled_events ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
ALTER TABLE scheduled_events ADD COLUMN parts TEXT NOT NULL DEFAULT '';
UPDATE scheduled_events SET name = feature, mode = CASE WHEN state = 3 THEN 0 ELSE 1 END,
parts = json_array(json_object('kind', 'feature', 'config', json_object('feature', feature), 'applied', json(CASE WHEN state = 1 THEN 'true' ELSE 'false' END),
'state', json_object('slot', slot, 'previousValue', previous_value, 'previousWebWins', json(CASE WHEN previous_web_wins <> 0 THEN 'true' ELSE 'false' END)), 'status', status))
WHERE parts = '' AND feature <> '';
INSERT INTO scheduled_events (feature, name, note, mode, schedule, starts_at, ends_at, priority, parts, state, status, created_at, created_by, updated_at, updated_by)
SELECT '', name, note, mode, schedule, 0, 0, priority,
json_array(json_object('kind', 'vanity', 'config', json_object('file', file, 'removals', removals,
'fileSwitches', CASE WHEN file_switches = '' THEN json_object() ELSE json(file_switches) END),
'applied', json(CASE WHEN applied <> 0 THEN 'true' ELSE 'false' END), 'state', json_object(), 'status', '')),
CASE WHEN applied <> 0 THEN 1 ELSE 0 END, '', created_at, created_by, updated_at, updated_by
FROM vanity_events;