[Dev] Add local Servatrice development profiles

Add a no-database guest profile (servatrice/dev.ini) and scripts to run the server and client side by side, with per-profile XDG directories for the client. Add a MySQL 8.4 compose stack published on loopback that loads the schema and an idempotent seed with accounts, roles, rooms, channels, reports, a warning, a ban, a deck, and chat log rows. Document both workflows in docs/DEVELOPMENT.md and link it from the README.
This commit is contained in:
Lukas Brübach 2026-11-09 15:29:10 +01:00 • committed by BruebachL
parent 214b284814
commit 969a473c62
8 changed files with 840 additions and 0 deletions

View file

@ -168,6 +168,9 @@ The following flags (with their non-default values) can be passed to `cmake`:
> doxygen Doxyfile
> ```
> [!TIP]
> For a local <kbd>Servatrice</kbd> with no database, or a seeded MySQL development stack with real accounts and moderation fixtures, see [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
# Run

27
docker-compose.dev.yml Normal file
View file

@ -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:

155
docs/DEVELOPMENT.md Normal file
View file

@ -0,0 +1,155 @@
# Local development servers
This page covers the two supported ways to run a local <kbd>Servatrice</kbd> 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/<name>/`.
- **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`.

127
scripts/dev-client.sh Executable file
View file

@ -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/<name>/, 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[@]}"

117
scripts/dev-db.sh Executable file
View file

@ -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 <command>
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

148
scripts/dev-servatrice.sh Executable file
View file

@ -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[@]}"

104
servatrice/dev.ini Normal file
View file

@ -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

159
servatrice/dev/seed.sql Normal file
View file

@ -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',
'<?xml version="1.0" encoding="UTF-8"?>\n<cockatrice_deck version="1">\n <deckname>Dev burn deck</deckname>\n <comments>Seeded development deck.</comments>\n <zone name="main">\n <card number="4" name="Lightning Bolt"/>\n <card number="4" name="Mountain"/>\n </zone>\n</cockatrice_deck>\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);