mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-11 12:33:25 -08:00
Merge 969a473c62 into 4581e649ac
This commit is contained in:
commit
df529c5649
8 changed files with 840 additions and 0 deletions
|
|
@ -168,6 +168,9 @@ The following flags (with their non-default values) can be passed to `cmake`:
|
||||||
> doxygen Doxyfile
|
> 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
|
# Run
|
||||||
|
|
||||||
|
|
|
||||||
27
docker-compose.dev.yml
Normal file
27
docker-compose.dev.yml
Normal 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
155
docs/DEVELOPMENT.md
Normal 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
127
scripts/dev-client.sh
Executable 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
117
scripts/dev-db.sh
Executable 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
148
scripts/dev-servatrice.sh
Executable 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
104
servatrice/dev.ini
Normal 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
159
servatrice/dev/seed.sql
Normal 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);
|
||||||
Loading…
Add table
Add a link
Reference in a new issue