diff --git a/README.md b/README.md index 03f02e540..ce2a1d349 100644 --- a/README.md +++ b/README.md @@ -168,6 +168,9 @@ The following flags (with their non-default values) can be passed to `cmake`: > doxygen Doxyfile > ``` +> [!TIP] +> For a local Servatrice with no database, or a seeded MySQL development stack with real accounts and moderation fixtures, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). + # Run diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 000000000..6fa9feacf --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +services: + mysql: + image: mysql:8.4 + command: + - --sql_mode= + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_unicode_ci + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: servatrice + MYSQL_USER: servatrice + MYSQL_PASSWORD: password + ports: + - "127.0.0.1:3306:3306" + volumes: + - cockatrice-dev-mysql:/var/lib/mysql + - ./servatrice/servatrice.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro + - ./servatrice/dev/seed.sql:/docker-entrypoint-initdb.d/02-seed.sql:ro + healthcheck: + test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -uroot -proot --silent"] + interval: 3s + timeout: 5s + retries: 30 + start_period: 20s + +volumes: + cockatrice-dev-mysql: diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 000000000..68877ed61 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,155 @@ +# Local development servers + +This page covers the two supported ways to run a local Servatrice for +manual client and server work: + +- **Guest profile** — no database, no mail server, two rooms and chat. Best for + client UI work, room/chat flows, and quick server regressions. +- **Seeded MySQL stack** — real accounts, staff roles, moderation fixtures, and + SQL-backed rooms. Best for authentication, permissions, reports, replay, and + migration work. + +Both paths run the binaries from your local build. Neither changes production +configuration. + +## Build the server and client + +From the repository root: + +```bash +cmake -S . -B build/test -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 +cmake --build build/test -j 24 +``` + +`build/test` contains both `servatrice` and `cockatrice`. `dev-servatrice.sh` +prefers `build/test`; `dev-client.sh` prefers the client-only `build/debug` tree +and falls back to `build/test`. + +## Guest profile (no database) + +Start the server: + +```bash +./scripts/dev-servatrice.sh +``` + +The script runs `servatrice/dev.ini`, which listens on `127.0.0.1:4747`, accepts +guest logins, and exposes the **General** and **Testing** rooms with the +`general`, `moderators`, `developer-news`, and `testing` channels. + +Start one or more clients in separate terminals: + +```bash +./scripts/dev-client.sh +./scripts/dev-client.sh --profile second --connect devplayer2@127.0.0.1:4747 +``` + +Each profile gets isolated `XDG_DATA_HOME`, `XDG_CACHE_HOME`, and +`XDG_CONFIG_HOME` directories under `build/dev/profiles/`, so profiles do not +share settings or card databases. A second plain launch runs independently +rather than handing off to the first. Delete a profile directory to reset it. + +Useful script options: + +```bash +./scripts/dev-servatrice.sh --port 14747 +./scripts/dev-servatrice.sh --config /path/to/servatrice.ini +./scripts/dev-client.sh --no-connect --profile scratch +``` + +Any argument the scripts do not recognise is passed through to the binary +unchanged. + +## Seeded MySQL stack + +The stack is defined in `docker-compose.dev.yml`. On first start, MySQL applies +`servatrice/servatrice.sql` and then `servatrice/dev/seed.sql`. + +```bash +./scripts/dev-db.sh up +./scripts/dev-servatrice.sh --mysql +``` + +Then connect with any seeded account. The password for every seeded account is +`password`: + +| Account | Role | +| ------------ | ----------------------------- | +| `devadmin` | admin (implies moderator) | +| `devmod` | moderator | +| `devjudge` | judge | +| `devdev` | developer | +| `devplayer` | registered player | +| `devplayer2` | registered player | + +`--mysql` exports `DATABASE_URL`, which Servatrice already honours: it selects +MySQL, forces the `cockatrice` table prefix and SQL-backed rooms, and switches +authentication to `sql`. No ini edit is needed. + +Seeded fixtures also include: + +- rooms `General` and `Testing`, their game types, and the same channels as the + guest profile; +- an open report and a resolved report, with a moderator comment; +- a warning against `devplayer2` and a permanent ban on `192.0.2.10`; +- a buddy/ignore entry and one deck in `devplayer`'s deck storage; +- two chat log rows in `General`. + +### Managing the database + +```bash +./scripts/dev-db.sh up # start MySQL, wait for health +./scripts/dev-db.sh seed # re-apply the fixtures +./scripts/dev-db.sh reset # drop the volume and start fresh +./scripts/dev-db.sh stop # stop containers, keep the volume +./scripts/dev-db.sh url # print DATABASE_URL +``` + +The schema and seed are not re-run automatically on an existing volume. Use +`reset` when `servatrice/servatrice.sql` changes. Do not apply the schema file a +second time by hand: its `cockatrice_schema_version` insert is not idempotent. +The fixture file itself is idempotent and can be re-applied while iterating. + +### External MySQL + +Set `COCKATRICE_DEV_DATABASE_URL` to skip the compose stack: + +```bash +export COCKATRICE_DEV_DATABASE_URL='mysql://user:password@127.0.0.1/database' +./scripts/dev-servatrice.sh --mysql +``` + +`up`, `seed`, and `reset` only manage the compose stack. For an external +database, apply `servatrice/servatrice.sql` and `servatrice/dev/seed.sql` +yourself. + +> [!NOTE] +> Servatrice reads the host, database, user, and password from `DATABASE_URL`, +> but not its port: MySQL connections use port 3306. Keep the compose stack on +> 3306 or configure the database through the ini file instead. + +## Per-run configuration overrides + +`servatrice` accepts `--set key=value` (repeatable) and `--print-config`: + +```bash +./scripts/dev-servatrice.sh --port 4747 -- --set security/max_users_per_address=50 +./build/test/servatrice/servatrice --config servatrice/dev.ini --print-config +``` + +Overrides live only in the running process; the ini file is never written. +`--print-config` includes override-only keys and redacts values whose key +contains `password`, `secret`, or `legacy_backup_key`. + +## Troubleshooting + +- **Port already in use** — pass `--port` to the server and `--connect` to the + clients, or stop the other instance. +- **MySQL port 3306 already in use** — stop the other MySQL instance, or run the + server against it by setting `database/*` in a custom ini file. `DATABASE_URL` + does not carry a port. +- **Client opens with stale data** — remove `build/dev/profiles//`. +- **Server cannot open the database** — the compose volume may predate a schema + change; run `./scripts/dev-db.sh reset`. +- **`servatrice` target missing** — the client-only `build/debug` tree does not + contain it; build `build/test` with `-DWITH_SERVER=1`. diff --git a/scripts/dev-client.sh b/scripts/dev-client.sh new file mode 100755 index 000000000..91d481985 --- /dev/null +++ b/scripts/dev-client.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash +# +# Run a Cockatrice client with disposable, per-profile settings. +# +# Every profile gets its own XDG data, cache, and config directories under +# build/dev/profiles//, so several clients can run side by side with +# separate settings and card databases. +# +# Unrecognised arguments are passed through to the client unchanged. + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +profile="default" +connect="${COCKATRICE_DEV_CONNECT:-devplayer@127.0.0.1:4747}" +no_connect=0 +passthrough=() + +usage() { + cat <<'EOF' +Usage: scripts/dev-client.sh [options] [-- client-options...] + +Options: + --profile NAME Isolated settings profile (default: default) + --connect URL Connect on startup (default: devplayer@127.0.0.1:4747) + --no-connect Start without connecting anywhere + -h, --help Show this help + +Environment: + COCKATRICE_BIN Client binary to run + COCKATRICE_DEV_HOME Base directory for profiles (default: build/dev) + COCKATRICE_DEV_CONNECT Default --connect value + +Examples: + scripts/dev-client.sh + scripts/dev-client.sh --profile second --connect devplayer2@127.0.0.1:4747 + scripts/dev-client.sh --no-connect --profile scratch +EOF +} + +die() { + echo "dev-client: $*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + [[ $# -ge 2 ]] || die "--profile needs a name" + profile="$2" + shift 2 + ;; + --profile=*) + profile="${1#*=}" + shift + ;; + --connect) + [[ $# -ge 2 ]] || die "--connect needs a value" + connect="$2" + shift 2 + ;; + --connect=*) + connect="${1#*=}" + shift + ;; + --no-connect) + no_connect=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + passthrough+=("$@") + break + ;; + *) + passthrough+=("$1") + shift + ;; + esac +done + +[[ "$profile" =~ ^[A-Za-z0-9._-]+$ ]] || die "profile name may only contain letters, numbers, '.', '_', and '-'" + +if [[ -n "${COCKATRICE_BIN:-}" ]]; then + binary="$COCKATRICE_BIN" +elif [[ -x "$repo_root/build/debug/cockatrice/cockatrice" ]]; then + binary="$repo_root/build/debug/cockatrice/cockatrice" +elif [[ -x "$repo_root/build/test/cockatrice/cockatrice" ]]; then + binary="$repo_root/build/test/cockatrice/cockatrice" +else + die "no cockatrice binary found; build it with: + cmake -S . -B build/debug -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=0 -DWITH_GUI_TEST=ON + cmake --build build/debug -j 24" +fi + +base="${COCKATRICE_DEV_HOME:-$repo_root/build/dev}" +profile_dir="$base/profiles/$profile" + +export XDG_DATA_HOME="$profile_dir/data" +export XDG_CACHE_HOME="$profile_dir/cache" +export XDG_CONFIG_HOME="$profile_dir/config" +mkdir -p "$XDG_DATA_HOME" "$XDG_CACHE_HOME" "$XDG_CONFIG_HOME" + +args=() +if [[ "$no_connect" -eq 0 ]]; then + has_connect=0 + for arg in "${passthrough[@]}"; do + case "$arg" in + --connect|--connect=*|-c) has_connect=1 ;; + esac + done + if [[ "$has_connect" -eq 0 ]]; then + args+=(--connect "$connect") + echo "Connecting to: $connect" + fi +else + args+=(--no-connect) +fi +args+=("${passthrough[@]}") + +echo "Client profile: $profile ($profile_dir)" + +exec "$binary" "${args[@]}" diff --git a/scripts/dev-db.sh b/scripts/dev-db.sh new file mode 100755 index 000000000..5d4a67cd1 --- /dev/null +++ b/scripts/dev-db.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +# +# Manage the seeded MySQL development database. +# +# The stack is defined in docker-compose.dev.yml. On a fresh volume MySQL runs +# servatrice/servatrice.sql and then servatrice/dev/seed.sql, so the database +# starts with accounts, rooms, channels, and moderation fixtures already in +# place. + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +compose_file="$repo_root/docker-compose.dev.yml" +seed_file="$repo_root/servatrice/dev/seed.sql" +url="${COCKATRICE_DEV_DATABASE_URL:-mysql://servatrice:password@127.0.0.1:3306/servatrice}" + +usage() { + cat <<'EOF' +Usage: scripts/dev-db.sh + +Commands: + up Start MySQL and wait until it is healthy + seed Re-apply servatrice/dev/seed.sql to the running database + reset Remove the container and volume, then start a fresh seeded database + stop Stop the container, keeping the database volume + url Print the DATABASE_URL servatrice expects + +Environment: + COCKATRICE_DEV_DATABASE_URL Use an external MySQL instead of the compose stack + +Examples: + scripts/dev-db.sh up + scripts/dev-db.sh seed + scripts/dev-db.sh reset +EOF +} + +die() { + echo "dev-db: $*" >&2 + exit 1 +} + +compose=() +detect_compose() { + if docker compose version >/dev/null 2>&1; then + compose=(docker compose) + elif command -v docker-compose >/dev/null 2>&1; then + compose=(docker-compose) + else + die "Docker Compose is not available; install the docker compose plugin or set COCKATRICE_DEV_DATABASE_URL" + fi +} + +compose_cmd() { + "${compose[@]}" -f "$compose_file" "$@" +} + +command="${1:-}" +[[ -n "$command" ]] || { + usage + exit 1 +} + +case "$command" in + url) + echo "$url" + ;; + up) + if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then + echo "Using external database: $url" + echo "Apply the schema and seed yourself if the database is empty:" + echo " mysql < servatrice/servatrice.sql" + echo " mysql < servatrice/dev/seed.sql" + exit 0 + fi + detect_compose + compose_cmd up -d --wait + echo + echo "MySQL is ready." + echo "DATABASE_URL=$(bash "$repo_root/scripts/dev-db.sh" url)" + echo "Start the server with: scripts/dev-servatrice.sh --mysql" + ;; + seed) + [[ -f "$seed_file" ]] || die "seed file not found: $seed_file" + if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then + die "seed is only automated for the compose stack; apply $seed_file manually for an external database" + fi + detect_compose + compose_cmd exec -T mysql mysql -uservatrice -ppassword servatrice < "$seed_file" + echo "Seed applied." + ;; + reset) + if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then + die "reset only manages the compose stack; reset the external database yourself" + fi + detect_compose + compose_cmd down -v + compose_cmd up -d --wait + echo + echo "Fresh seeded database ready." + echo "Start the server with: scripts/dev-servatrice.sh --mysql" + ;; + stop) + if [[ -n "${COCKATRICE_DEV_DATABASE_URL:-}" ]]; then + die "stop only manages the compose stack" + fi + detect_compose + compose_cmd down + ;; + -h|--help|help) + usage + ;; + *) + usage + die "unknown command: $command" + ;; +esac diff --git a/scripts/dev-servatrice.sh b/scripts/dev-servatrice.sh new file mode 100755 index 000000000..c8e74282b --- /dev/null +++ b/scripts/dev-servatrice.sh @@ -0,0 +1,148 @@ +#!/usr/bin/env bash +# +# Run a local Servatrice with the checked-in development profile. +# +# The default profile needs no database: it listens on localhost, accepts +# guest logins, and exposes the General and Testing rooms from +# servatrice/dev.ini. Pass --mysql to run against the seeded development +# database managed by scripts/dev-db.sh. +# +# Any unrecognised argument is passed through to servatrice unchanged, so +# options such as --set key=value or --test-hash keep working. + +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +config="$repo_root/servatrice/dev.ini" +database_url="${DATABASE_URL:-}" +port="" +log_to_console=1 +passthrough=() + +usage() { + cat <<'EOF' +Usage: scripts/dev-servatrice.sh [options] [-- servatrice-options...] + +Options: + --config FILE Configuration file (default: servatrice/dev.ini) + --port PORT Listen on PORT instead of the configured port + --mysql[=URL] Use the seeded MySQL dev database + --database-url URL Use an explicit mysql:// URL + --no-log-to-console Do not force --log-to-console + -h, --help Show this help + +Environment: + SERVATRICE_BIN Servatrice binary to run + DATABASE_URL Database URL used by --mysql when no URL is given + +Examples: + scripts/dev-servatrice.sh + scripts/dev-servatrice.sh --port 14747 + scripts/dev-db.sh up && scripts/dev-servatrice.sh --mysql +EOF +} + +die() { + echo "dev-servatrice: $*" >&2 + exit 1 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --config) + [[ $# -ge 2 ]] || die "--config needs a file" + config="$2" + shift 2 + ;; + --config=*) + config="${1#*=}" + shift + ;; + --port) + [[ $# -ge 2 ]] || die "--port needs a value" + port="$2" + shift 2 + ;; + --port=*) + port="${1#*=}" + shift + ;; + --mysql) + database_url="${DATABASE_URL:-$(bash "$repo_root/scripts/dev-db.sh" url)}" + shift + ;; + --mysql=*) + database_url="${1#*=}" + shift + ;; + --database-url) + [[ $# -ge 2 ]] || die "--database-url needs a value" + database_url="$2" + shift 2 + ;; + --database-url=*) + database_url="${1#*=}" + shift + ;; + --no-log-to-console) + log_to_console=0 + shift + ;; + -h|--help) + usage + exit 0 + ;; + --) + shift + passthrough+=("$@") + break + ;; + *) + passthrough+=("$1") + shift + ;; + esac +done + +[[ -f "$config" ]] || die "configuration file not found: $config" + +if [[ -n "${SERVATRICE_BIN:-}" ]]; then + binary="$SERVATRICE_BIN" +elif [[ -x "$repo_root/build/test/servatrice/servatrice" ]]; then + binary="$repo_root/build/test/servatrice/servatrice" +elif [[ -x "$repo_root/build/debug/servatrice/servatrice" ]]; then + binary="$repo_root/build/debug/servatrice/servatrice" +else + die "no servatrice binary found; build it with: + cmake -S . -B build/test -DCMAKE_BUILD_TYPE=Debug -DWITH_SERVER=1 -DTEST=1 + cmake --build build/test -j 24" +fi + +args=(--config "$config") +[[ "$log_to_console" -eq 1 ]] && args+=(--log-to-console) +[[ -n "$port" ]] && args+=("--set" "server/port=$port") +args+=("${passthrough[@]}") + +# Resolve the effective host and port through the binary itself, so a port in +# the ini file and --set overrides are both reflected in the printed command. +print_args=(--config "$config") +[[ -n "$port" ]] && print_args+=(--set "server/port=$port") +effective_config="$("$binary" "${print_args[@]}" --print-config 2>/dev/null || true)" +effective_port="$(printf '%s\n' "$effective_config" | awk -F= '$1 == "server/port" { print $2; exit }')" +effective_host="$(printf '%s\n' "$effective_config" | awk -F= '$1 == "server/host" { print $2; exit }')" +effective_port="${effective_port:-${port:-4747}}" +if [[ -z "$effective_host" || "$effective_host" == "any" ]]; then + effective_host="127.0.0.1" +fi + +if [[ -n "$database_url" ]]; then + export DATABASE_URL="$database_url" + redacted_url="$(printf '%s' "$database_url" | sed -E 's#(://[^:/@]+):[^@]*@#\1:***@#')" + echo "Database: $redacted_url" +fi + +echo "Servatrice listening on ${effective_host}:${effective_port}" +echo "Connect with: scripts/dev-client.sh --connect devplayer@${effective_host}:${effective_port}" +echo + +exec "$binary" "${args[@]}" diff --git a/servatrice/dev.ini b/servatrice/dev.ini new file mode 100644 index 000000000..23e297cb8 --- /dev/null +++ b/servatrice/dev.ini @@ -0,0 +1,104 @@ +; Servatrice development configuration. +; +; This profile needs no database and no mail server: it starts a +; localhost-only server that accepts guest logins, two rooms, and chat +; channels. Use scripts/dev-servatrice.sh to run it; the script can layer +; per-run overrides on top without editing this file. +; +; For registered accounts, roles, moderation data, and SQL, see +; docs/DEVELOPMENT.md and scripts/dev-db.sh instead. + +[server] +name="Cockatrice dev server" +id=1 +host=127.0.0.1 +port=4747 +number_pools=1 +websocket_number_pools=0 +statusupdate=0 +writelog=0 +; An empty logfile keeps the repository clean; console output still works. +logfile= +clientkeepalive=1 +max_player_inactivity_time=15 +idleclienttimeout=0 +officialwarnings="Abusive Language|1,Causing Drama|1,Cheating|2,Spamming|1,Targeted Harassment|2" + +[authentication] +method=none +regonly=false + +[registration] +enabled=false + +[users] +minnamelength=3 +maxnamelength=35 +allowlowercase=true +allowuppercase=true +allownumerics=true +allowedpunctuation=_.- +allowpunctuationprefix=false +disallowedwords="" +disallowedregexp="" +minpasswordlength=6 + +[database] +type=none + +[rooms] +method=config +roomlist\size=2 + +roomlist\1\name="General" +roomlist\1\description="Play anything here." +roomlist\1\permissionlevel=none +roomlist\1\privilegelevel=none +roomlist\1\autojoin=true +roomlist\1\joinmessage="" +roomlist\1\chathistorysize=100 +roomlist\1\game_types\size=1 +roomlist\1\game_types\1\name="Standard" +roomlist\1\channels\size=3 +roomlist\1\channels\1\id=general +roomlist\1\channels\1\displayname="General" +roomlist\1\channels\1\access=public +roomlist\1\channels\2\id=moderators +roomlist\1\channels\2\displayname="Moderators" +roomlist\1\channels\2\access=moderator +roomlist\1\channels\3\id=developer-news +roomlist\1\channels\3\displayname="Developer news" +roomlist\1\channels\3\access=public +roomlist\1\channels\3\writeaccess=developer +roomlist\1\channels\3\banner="Official announcements from the developers." + +roomlist\2\name="Testing" +roomlist\2\description="Scratch room for server and client work." +roomlist\2\permissionlevel=none +roomlist\2\privilegelevel=none +roomlist\2\autojoin=true +roomlist\2\joinmessage="" +roomlist\2\chathistorysize=100 +roomlist\2\game_types\size=2 +roomlist\2\game_types\1\name="Standard" +roomlist\2\game_types\2\name="Commander" +roomlist\2\channels\size=2 +roomlist\2\channels\1\id=testing +roomlist\2\channels\1\displayname="Testing" +roomlist\2\channels\1\access=public +roomlist\2\channels\2\id=moderators +roomlist\2\channels\2\displayname="Moderators" +roomlist\2\channels\2\access=moderator + +[game] +max_game_inactivity_time=120 +store_replays=false +allow_create_as_judge=false + +[security] +max_users_per_address=20 +max_login_attempts_per_ip=0 +max_registrations_per_ip=0 + +[smtp] +enableinternalsmtpclient=false diff --git a/servatrice/dev/seed.sql b/servatrice/dev/seed.sql new file mode 100644 index 000000000..9a72b005f --- /dev/null +++ b/servatrice/dev/seed.sql @@ -0,0 +1,159 @@ +-- Development fixtures for a local Servatrice database. +-- +-- Accounts, rooms, channels, moderation data, and a deck are all keyed by +-- stable names or fixed ids, so this file can be applied repeatedly while +-- iterating. Passwords are the scrypt verifier for the literal password +-- "password"; regenerate them with PasswordHasher if that ever changes. +-- +-- This file is loaded automatically by docker-compose.dev.yml on a fresh +-- volume, and can be re-applied with scripts/dev-db.sh seed. + +-- Accounts ----------------------------------------------------------------- +-- admin bitfield: 1 = admin (implies moderator), 2 = moderator, +-- 4 = judge, 8 = developer. +INSERT INTO cockatrice_users + (admin, name, realname, password_sha512, email, country, avatar_bmp, + registrationDate, active, clientid, adminnotes, privlevel, + privlevelStartDate, privlevelEndDate, passwordLastChangedDate) +VALUES + (15, 'devadmin', 'Dev Admin', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devadmin@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development administrator.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'), + (2, 'devmod', 'Dev Moderator', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devmod@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development moderator.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'), + (4, 'devjudge', 'Dev Judge', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devjudge@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development judge.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'), + (8, 'devdev', 'Dev Developer', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devdev@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development developer.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'), + (0, 'devplayer', 'Dev Player', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devplayer@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development player.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00'), + (0, 'devplayer2', 'Dev Player Two', '$scrypt$32768$8$1$Y29ja2F0cmljZS1kZXYhIQ==$L1m5vF9LgraerY880GvTPNdNpuYzmE4/My4wa7q6wTa3+QuoYeTpYERPhnH9I2WXBNLUrrvdYaCrOZ6EuI8ofQ==', 'devplayer2@example.invalid', 'US', '', '2026-01-01 00:00:00', 1, '', 'Seeded development player.', 'NONE', '2026-01-01 00:00:00', '2026-01-01 00:00:00', '2026-01-01 00:00:00') +ON DUPLICATE KEY UPDATE + admin = VALUES(admin), + realname = VALUES(realname), + password_sha512 = VALUES(password_sha512), + email = VALUES(email), + active = VALUES(active), + adminnotes = VALUES(adminnotes); + +SET @devadmin_id = (SELECT id FROM cockatrice_users WHERE name = 'devadmin'); +SET @devmod_id = (SELECT id FROM cockatrice_users WHERE name = 'devmod'); +SET @devjudge_id = (SELECT id FROM cockatrice_users WHERE name = 'devjudge'); +SET @devdev_id = (SELECT id FROM cockatrice_users WHERE name = 'devdev'); +SET @devplayer_id = (SELECT id FROM cockatrice_users WHERE name = 'devplayer'); +SET @devplayer2_id = (SELECT id FROM cockatrice_users WHERE name = 'devplayer2'); + +-- Rooms and channels ------------------------------------------------------- +INSERT INTO cockatrice_rooms + (id, name, descr, permissionlevel, privlevel, auto_join, join_message, chat_history_size, id_server) +VALUES + (1, 'General', 'Play anything here.', 'NONE', 'NONE', 1, '', 100, 1), + (2, 'Testing', 'Scratch room for server and client work.', 'NONE', 'NONE', 1, '', 100, 1) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + descr = VALUES(descr), + permissionlevel = VALUES(permissionlevel), + privlevel = VALUES(privlevel), + auto_join = VALUES(auto_join), + join_message = VALUES(join_message), + chat_history_size = VALUES(chat_history_size), + id_server = VALUES(id_server); + +DELETE FROM cockatrice_rooms_gametypes WHERE id_room IN (1, 2) AND id_server = 1; +INSERT INTO cockatrice_rooms_gametypes (id_room, name, id_server) VALUES + (1, 'Standard', 1), + (2, 'Standard', 1), + (2, 'Commander', 1); + +DELETE FROM cockatrice_rooms_channels WHERE id_room IN (1, 2) AND id_server = 1; +INSERT INTO cockatrice_rooms_channels + (id_room, id_server, channel_id, display_name, access_level, write_access_level, banner_message) +VALUES + (1, 1, 'general', 'General', 'public', NULL, ''), + (1, 1, 'moderators', 'Moderators', 'moderator', NULL, ''), + (1, 1, 'developer-news', 'Developer news', 'public', 'developer', 'Official announcements from the developers.'), + (2, 1, 'testing', 'Testing', 'public', NULL, ''), + (2, 1, 'moderators', 'Moderators', 'moderator', NULL, ''); + +-- Social lists ------------------------------------------------------------- +INSERT IGNORE INTO cockatrice_buddylist (id_user1, id_user2) VALUES (@devplayer_id, @devmod_id); +INSERT IGNORE INTO cockatrice_ignorelist (id_user1, id_user2) VALUES (@devplayer_id, @devplayer2_id); + +-- Moderation fixtures ------------------------------------------------------ +INSERT INTO cockatrice_reports + (id, reporter_id, reporter_name, reported_user_id, reported_user_name, game_id, room_id, + category, description, chat_log, created_at, resolution_time, status, assigned_to, + resolved_by, resolution_note, notified) +VALUES + (9001, @devplayer_id, 'devplayer', @devplayer2_id, 'devplayer2', NULL, 1, + 'cheating', 'Seeded open report: suspected cheat in a test game.', NULL, + '2026-01-02 12:00:00', NULL, 'open', NULL, NULL, NULL, 0), + (9002, @devplayer2_id, 'devplayer2', @devplayer_id, 'devplayer', NULL, 1, + 'spam', 'Seeded resolved report: repeated messages in General.', NULL, + '2026-01-02 13:00:00', '2026-01-02 15:00:00', 'resolved', @devmod_id, + @devmod_id, 'Resolved during development seeding.', 0) +ON DUPLICATE KEY UPDATE + reporter_id = VALUES(reporter_id), + reporter_name = VALUES(reporter_name), + reported_user_id = VALUES(reported_user_id), + reported_user_name = VALUES(reported_user_name), + room_id = VALUES(room_id), + category = VALUES(category), + description = VALUES(description), + created_at = VALUES(created_at), + resolution_time = VALUES(resolution_time), + status = VALUES(status), + assigned_to = VALUES(assigned_to), + resolved_by = VALUES(resolved_by), + resolution_note = VALUES(resolution_note); + +INSERT INTO cockatrice_report_comments + (id, report_id, author_name, author_id, comment_text, created_at, is_moderator, notified) +VALUES + (9001, 9001, 'devmod', @devmod_id, 'Looking into this now.', '2026-01-02 12:30:00', 1, 0) +ON DUPLICATE KEY UPDATE + comment_text = VALUES(comment_text), + created_at = VALUES(created_at), + is_moderator = VALUES(is_moderator); + +INSERT INTO cockatrice_warnings (user_id, user_name, mod_name, reason, time_of, clientid) +VALUES (@devplayer2_id, 'devplayer2', 'devmod', 'Seeded warning: spamming the General channel.', '2026-01-03 12:00:00', '') +ON DUPLICATE KEY UPDATE + mod_name = VALUES(mod_name), + reason = VALUES(reason); + +-- A permanent ban on a documentation-only address: it exercises the ban list +-- without locking out 127.0.0.1 or any seeded account. +INSERT INTO cockatrice_bans + (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason, clientid) +VALUES + ('griefer', '192.0.2.10', @devmod_id, '2026-01-04 12:00:00', 0, + 'Seeded ban: abusive behaviour.', 'Abusive behaviour.', '') +ON DUPLICATE KEY UPDATE + id_admin = VALUES(id_admin), + minutes = VALUES(minutes), + reason = VALUES(reason), + visible_reason = VALUES(visible_reason); + +DELETE FROM cockatrice_log WHERE log_time = '2026-01-05 12:00:00'; +INSERT INTO cockatrice_log + (log_time, sender_id, sender_name, sender_ip, log_message, target_type, target_id, target_name, channel) +VALUES + ('2026-01-05 12:00:00', @devplayer_id, 'devplayer', '127.0.0.1', 'Anyone up for a test game?', 'room_channel', 1, 'General', 'general'), + ('2026-01-05 12:00:00', @devplayer2_id, 'devplayer2', '127.0.0.1', 'Sure, create it in Testing.', 'room_channel', 1, 'General', 'general'); + +-- Deck storage ------------------------------------------------------------- +INSERT INTO cockatrice_decklist_folders (id, id_parent, id_user, name, is_public) +VALUES (9001, 0, @devplayer_id, 'Dev decks', 0) +ON DUPLICATE KEY UPDATE + id_user = VALUES(id_user), + name = VALUES(name), + is_public = VALUES(is_public); + +INSERT INTO cockatrice_decklist_files + (id, id_folder, id_user, name, upload_time, content, is_public) +VALUES + (9001, 9001, @devplayer_id, 'Dev burn deck', '2026-01-06 12:00:00', + '\n\n Dev burn deck\n Seeded development deck.\n \n \n \n \n\n', + 0) +ON DUPLICATE KEY UPDATE + id_folder = VALUES(id_folder), + id_user = VALUES(id_user), + name = VALUES(name), + upload_time = VALUES(upload_time), + content = VALUES(content), + is_public = VALUES(is_public);