[econ] implement catalog

This commit is contained in:
Devin Zuczek
2026-08-27 22:43:22 -04:00
parent 5e138a0646
commit a169cf3e6e
17 changed files with 214338 additions and 18 deletions
+59
View File
@@ -0,0 +1,59 @@
-- The item catalog: every avatar item and every equipment skin the game knows about.
--
-- STRUCTURE ONLY. This migration holds no rows: the catalog's CONTENTS are loaded by
-- `runx catalog load [--remote]` from apps/econ/static/db/avatar-items.json and
-- apps/econ/static/db/skins.json.
--
-- That split is deliberate. The item list changes as the game's does, and that is not a
-- schema change -- putting rows here would mean a migration and a deploy per refresh, an
-- ever-growing pile of near-identical data migrations, and no way to reload without writing
-- another one. Versioning the structure and reloading the contents on demand also makes a
-- refresh a readable `git diff` of the JSON rather than of 700KB of generated SQL.
--
-- ONE KEY spans both kinds: `item_key` is an avatar item's `AvatarItemDesc` and a skin's
-- `ModificationGuid`. Neither repeats, the two never collide, and no item has both -- so the
-- catalog is keyed the same way the INVENTORY is, and resolving what a player owns is a
-- lookup on this column rather than a join keyed on which kind of thing it turned out to be.
--
-- Not every key is a GUID: 191 skins and 109 avatar items carry the short alpha-string ids
-- the game used before it moved to GUIDs. The column is TEXT and compared as text; do not add
-- a uuid-shaped constraint, and do not try to parse one.
--
-- `avatar_item_id` is carried as DATA ONLY and is deliberately not indexed. It cannot key
-- anything: it is missing from 22 avatar items (the permanent hair dyes have no id at all)
-- and repeated on 9 more -- id 9503 alone is shared by five unrelated developer items.
--
-- `tooltip` is deliberately nullable, on both kinds: the capture carries NULL and "" as
-- different values and the client's DTOs serve the difference through. Do not default it.
--
-- `Favorited` from skins.json is NOT loaded: it is a player's flag, not a property of the
-- skin, and the capture recorded one account's. It is projected as false and overwritten
-- from the player's own `equipment` row -- see src/catalog-db.ts.
CREATE TABLE IF NOT EXISTS catalog (
item_key TEXT PRIMARY KEY,
kind TEXT NOT NULL,
friendly_name TEXT NOT NULL,
tooltip TEXT,
rarity INTEGER NOT NULL DEFAULT 0,
platform_mask INTEGER NOT NULL DEFAULT -1,
thumbnail_image TEXT,
avatar_item_type INTEGER,
avatar_item_id INTEGER,
is_base_avatar_item INTEGER,
tag_list TEXT,
created_at TEXT,
prefab_name TEXT,
unlocked_level INTEGER
);
-- The search index. Folded, because a name search is case-insensitive and SQLite's LIKE only
-- folds ASCII -- which these names are not all of.
CREATE INDEX IF NOT EXISTS idx_catalog_name ON catalog (kind, lower(friendly_name));
-- Every skin of one prefab, which is how a skin picker is filled.
CREATE INDEX IF NOT EXISTS idx_catalog_prefab
ON catalog (prefab_name) WHERE prefab_name IS NOT NULL;
-- Seasonal rows ('halloween', 'music', ...): a handful of tags over 3000-odd rows.
CREATE INDEX IF NOT EXISTS idx_catalog_tag ON catalog (tag_list) WHERE tag_list IS NOT NULL;
+33
View File
@@ -0,0 +1,33 @@
-- A small numeric id for a catalog row, for the places that need to name an item as a NUMBER
-- rather than as its `item_key` (a comma-laden `AvatarItemDesc` or a 22-to-36 character guid,
-- neither of which belongs in a URL or a compact index).
--
-- It is a LOAD-ORDER SURROGATE and nothing more. `runx catalog load` assigns it, so it is
-- stable only until the next load: a `--replace` renumbers everything from 1, and a merge
-- renumbers whatever the captures mention. Nothing may store it, reference it across a load,
-- or treat it as an item's identity -- `item_key` is the identity, and it is what the
-- inventory stores. Anything durable that points at a catalog row must point with the key.
--
-- Unique, because a numeric handle that names two rows is useless as a handle. The loader
-- numbers the captures from 10000 upward and pushes anything already in the table but absent
-- from them above that range, so the numbering stays collision-free through a merge as well as
-- a replace.
--
-- It starts at 10000 rather than 1 because a generated storefront lists a row under this very
-- number as its `PurchasableItemId`, and every captured storefront's own ids are 2764 or below.
-- Numbering from 1 would have collided with sf3's head-on, so one id would mean two different
-- items depending on which storefront the client read it from.
--
-- Nullable, because that is the state between "the row exists" and "the load has numbered
-- it": the loader clears the column first so a merge cannot collide with stale numbers, and
-- fills it as it goes. A NULL here after a load means the load did not finish -- the CLI
-- verifies row counts and probe keys for exactly that reason. SQLite treats NULLs as distinct
-- for uniqueness, so any number of un-numbered rows can coexist.
--
-- Its own migration rather than folded into 0015, because 0015 is already applied: an edit
-- there would never re-run on a database that has recorded it.
ALTER TABLE catalog ADD COLUMN catalog_id INTEGER;
CREATE UNIQUE INDEX IF NOT EXISTS idx_catalog_id
ON catalog (catalog_id) WHERE catalog_id IS NOT NULL;