commit 2a9d9fa157b30b20fcdba2349bb645ba25199aa3 Author: Baptiste Boulongne Date: Sun Aug 9 17:45:43 2026 +0200 Add Docker Compose stack and GHCR publish workflow for Tortoise WoW. Packages Shyalya/tortoise-wow with MariaDB, one-shot DB init, and nightly image builds for playerbots and no-bots tags. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c9224c3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +.git +.github +.env +.env.* +!.env.example +data +logs +*.md +docs +agent-transcripts +**/.DS_Store diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8667182 --- /dev/null +++ b/.env.example @@ -0,0 +1,39 @@ +# Copy to .env and edit. + +# GHCR tag from this repo's workflow (default: playerbots) +# TAG=playerbots +# TAG=no-bots + +# MariaDB +DB_ROOT_PASSWORD=changeme +DB_USER=mangos +DB_PASSWORD=mangos +DB_PUBLISH_PORT=3306 + +# Database names (defaults match upstream) +DB_LOGIN=tw_logon +DB_WORLD=tw_world +DB_CHAR=tw_char +DB_LOGS=tw_logs + +# Realm / network — REALM_ADDRESS is what the *game client* uses to reach mangosd +REALM_NAME=TurtleWoW +REALM_ADDRESS=127.0.0.1 +REALM_ID=1 +REALM_PORT=3724 +WORLD_PORT=8090 + +# Client data (dbc, maps, vmaps, mmaps) on the host +DATA_PATH=./data + +# mangosd tuning +LOG_SQL=0 +DATABASE_AUTOUPDATE_ENABLED=1 +LFT_BOTFILL_ENABLE=1 +SOLO_DUNGEON_REPOP_ALIVE_ENABLE=1 +LEECH_ENABLE=1 + +# Playerbots runtime (only effective on the :playerbots image) +AI_PLAYERBOT_ENABLED=1 +AI_MIN_RANDOM_BOTS=10 +AI_MAX_RANDOM_BOTS=10 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ad70cb3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +docker/*.sh text eol=lf +Dockerfile text eol=lf +docker-compose.yml text eol=lf diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4e24cf2 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,89 @@ +name: Publish to GHCR + +on: + push: + branches: [main, master] + paths-ignore: + - "**.md" + - ".env.example" + pull_request: + paths: + - "Dockerfile" + - "docker/**" + - ".github/workflows/publish.yml" + schedule: + # 00:00 UTC + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + source_ref: + description: "Git branch or tag of Shyalya/tortoise-wow to build" + required: false + default: "playerbots-integration-gh" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + timeout-minutes: 360 + strategy: + fail-fast: false + matrix: + include: + - tag: playerbots + build_playerbots: "ON" + is_latest: true + - tag: no-bots + build_playerbots: "OFF" + is_latest: false + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ matrix.tag }} + type=sha,prefix=${{ matrix.tag }}- + type=raw,value=latest,enable=${{ matrix.is_latest && github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + BUILD_PLAYERBOTS=${{ matrix.build_playerbots }} + USE_EXTRACTORS=OFF + SOURCE_REPO=https://github.com/Shyalya/tortoise-wow.git + SOURCE_REF=${{ github.event.inputs.source_ref || 'playerbots-integration-gh' }} + CMAKE_BUILD_TYPE=Release + CMAKE_INSTALL_PREFIX=/opt/turtle + cache-from: type=gha,scope=${{ matrix.tag }} + cache-to: type=gha,mode=max,scope=${{ matrix.tag }} + provenance: false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b84d543 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.env +data/ +logs/ +*.log +.DS_Store diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bd02e6a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,122 @@ +# syntax=docker/dockerfile:1.7 +# Tortoise WoW (Shyalya/tortoise-wow) — Ubuntu 22.04 build for GHCR + compose. +# Build-arg BUILD_PLAYERBOTS controls whether the playerbots module is compiled in. + +ARG UBUNTU_VERSION=22.04 + +# ----------------------------------------------------------------------------- +# Builder +# ----------------------------------------------------------------------------- +FROM ubuntu:${UBUNTU_VERSION} AS builder + +ARG BUILD_PLAYERBOTS=ON +ARG USE_EXTRACTORS=OFF +ARG SOURCE_REPO=https://github.com/Shyalya/tortoise-wow.git +ARG SOURCE_REF=playerbots-integration-gh +ARG CMAKE_BUILD_TYPE=Release +ARG CMAKE_INSTALL_PREFIX=/opt/turtle + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=UTC + +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + cmake \ + git \ + libace-dev \ + libboost-all-dev \ + default-libmysqlclient-dev \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /src + +RUN git clone --depth 1 --branch "${SOURCE_REF}" "${SOURCE_REPO}" tortoise-wow + +WORKDIR /src/tortoise-wow + +RUN cmake -B build \ + -DCMAKE_BUILD_TYPE="${CMAKE_BUILD_TYPE}" \ + -DCMAKE_INSTALL_PREFIX="${CMAKE_INSTALL_PREFIX}" \ + -DBUILD_PLAYERBOTS="${BUILD_PLAYERBOTS}" \ + -DUSE_EXTRACTORS="${USE_EXTRACTORS}" \ + -DALLOW_TURTLE_ADDONS=ON \ + && cmake --build build -j"$(nproc)" \ + && cmake --install build + +# Keep SQL needed for first-time DB init + AutoUpdate path. +RUN mkdir -p /opt/turtle/sql \ + && cp -a sql/create_databases.sql sql/base sql/database_updates /opt/turtle/sql/ \ + && if [ -d src/modules/PlayerBots/sql ]; then \ + mkdir -p /opt/turtle/sql/playerbots \ + && cp -a src/modules/PlayerBots/sql/. /opt/turtle/sql/playerbots/; \ + fi + +# ----------------------------------------------------------------------------- +# Runtime +# ----------------------------------------------------------------------------- +FROM ubuntu:${UBUNTU_VERSION} AS runtime + +ARG BUILD_PLAYERBOTS=ON +ARG CMAKE_INSTALL_PREFIX=/opt/turtle + +LABEL org.opencontainers.image.title="tortoise-docker" \ + org.opencontainers.image.description="Turtle WoW / Tortoise server (realmd + mangosd)" \ + org.opencontainers.image.source="https://github.com/Shyalya/tortoise-wow" \ + org.opencontainers.image.licenses="GPL-2.0" + +ENV DEBIAN_FRONTEND=noninteractive \ + TZ=UTC \ + TURTLE_HOME=/opt/turtle \ + PLAYERBOTS_BUILT=${BUILD_PLAYERBOTS} \ + PATH=/opt/turtle/bin:$PATH + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + libace-7.0.6 \ + libboost-atomic1.74.0 \ + libboost-chrono1.74.0 \ + libboost-date-time1.74.0 \ + libboost-filesystem1.74.0 \ + libboost-iostreams1.74.0 \ + libboost-program-options1.74.0 \ + libboost-regex1.74.0 \ + libboost-serialization1.74.0 \ + libboost-system1.74.0 \ + libboost-thread1.74.0 \ + libmysqlclient21 \ + libssl3 \ + zlib1g \ + libbz2-1.0 \ + libreadline8 \ + libncurses6 \ + mariadb-client \ + tini \ + gosu \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --gid 1000 turtle \ + && useradd --uid 1000 --gid turtle --home-dir /opt/turtle --shell /usr/sbin/nologin turtle + +COPY --from=builder /opt/turtle /opt/turtle +COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh +COPY docker/init-db.sh /usr/local/bin/init-db.sh +COPY docker/render-config.sh /usr/local/bin/render-config.sh +COPY docker/repair-migrations.sh /usr/local/bin/repair-migrations.sh + +RUN chmod +x /usr/local/bin/entrypoint.sh \ + /usr/local/bin/init-db.sh \ + /usr/local/bin/render-config.sh \ + /usr/local/bin/repair-migrations.sh \ + && mkdir -p /opt/turtle/data /opt/turtle/logs /opt/turtle/run /var/lib/turtle-init \ + && chown -R turtle:turtle /opt/turtle /var/lib/turtle-init + +WORKDIR /opt/turtle/bin + +EXPOSE 3724/tcp 8090/tcp + +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] +CMD ["mangosd"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..e66707a --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Tortoise WoW (Docker) + +Run a private [Turtle WoW](https://turtle-wow.org/) server with Docker. This stack uses [Shyalya/tortoise-wow](https://github.com/Shyalya/tortoise-wow) with playerbots. + +The server work and install steps come from this video: + +**[Tortoise WoW / playerbots setup (YouTube)](https://www.youtube.com/watch?v=CNgkHs3btNE)** + +This repository ships a Compose file. CI builds and publishes the server images to GHCR. You do not build the server on your machine. + +## What you need + +- Docker Desktop (or Docker Engine with Compose v2) +- A Turtle WoW **1.18.1** client (**build 7272**) +- Client data folders: `dbc`, `maps`, `vmaps`, `mmaps` +- Several GB of free disk space + +The images do not include client data. You extract that data from your game client. The video and the [Linux install guide](https://github.com/Shyalya/tortoise-wow/blob/playerbots-integration-gh/INSTALL-LINUX.md) show how. + +## Quick start + +### 1. Get the Compose files + +```bash +git clone https://github.com/Nescabir/tortoise-docker.git +cd tortoise-docker +``` + +### 2. Create your settings file + +```bash +cp .env.example .env +``` + +Edit `.env`: + +1. Set strong values for `DB_ROOT_PASSWORD` and `DB_PASSWORD`. +2. Set `REALM_ADDRESS` to an address your game client can reach. +3. Set `DATA_PATH` if your client data is not in `./data`. + +Use `127.0.0.1` for `REALM_ADDRESS` only when the client runs on the same machine. For another PC on your LAN, use your host LAN IP. + +### 3. Add client data + +Put the extracted folders here (or under your `DATA_PATH`): + +```text +data/ + dbc/ + maps/ + vmaps/ + mmaps/ +``` + +### 4. Start with Compose + +Compose pulls the published images and starts the stack: + +```bash +docker compose up -d +``` + +The first start downloads the images (if needed) and imports the world database. This takes several minutes. + +Then watch the world server: + +```bash +docker compose logs -f mangosd +``` + +Wait until the log shows: + +```text +World server is up and running +``` + +The first start with playerbots is slow. The server builds bot gear data before it is ready. Do not create an account before that line appears. + +### 5. Create a game account + +```bash +docker compose exec -u turtle mangosd bash -c 'echo "account create myuser mypass" > /opt/turtle/run/mangosd.in' +``` + +Check that the account exists: + +```bash +docker compose exec -T db mariadb -uroot -pYOUR_ROOT_PASSWORD -e "SELECT id, username FROM tw_logon.account;" +``` + +Replace `YOUR_ROOT_PASSWORD` with the value of `DB_ROOT_PASSWORD` from `.env`. + +### 6. Connect with the client + +Edit `realmlist.wtf` in your Turtle WoW client: + +```text +set realmlist 127.0.0.1 +``` + +Use the same host as `REALM_ADDRESS` in `.env`. Then log in with the account you created. + +## Useful settings + +| Setting | Default | Meaning | +|---|---|---| +| `REALM_ADDRESS` | `127.0.0.1` | Host the client uses to reach the world server | +| `REALM_NAME` | `TurtleWoW` | Name of the realm in the client list | +| `DATA_PATH` | `./data` | Folder with `dbc`, `maps`, `vmaps`, `mmaps` | +| `TAG` | `playerbots` | Image variant (`playerbots` or `no-bots`) | +| `AI_PLAYERBOT_ENABLED` | `1` | Turn bots on or off (`playerbots` image only) | +| `AI_MIN_RANDOM_BOTS` / `AI_MAX_RANDOM_BOTS` | `10` / `10` | How many random bots to keep online | + +Keep bot counts low for the first start. Raise them later in `.env`, then run: + +```bash +docker compose up -d mangosd +``` + +## Common commands + +View logs: + +```bash +docker compose logs -f realmd +docker compose logs -f mangosd +``` + +Stop the stack: + +```bash +docker compose down +``` + +Start again (keeps your database): + +```bash +docker compose up -d +``` + +Reset the database (deletes characters and accounts): + +```bash +docker compose down +docker volume ls +docker volume rm tortoise-docker_db-data tortoise-docker_init-marker +docker compose up -d +``` + +Volume names can include your Compose project name. Use `docker volume ls` to confirm the names. + +## Troubleshooting + +| Problem | What to do | +|---|---| +| Login fails / unknown account | Wait for `World server is up and running`, then create the account again | +| Account create does nothing | mangosd is still starting; wait and retry | +| Realm list is empty or offline | Check that `realmd` and `mangosd` are up: `docker compose ps` | +| Client hangs after you pick the realm | Set `REALM_ADDRESS` to an IP the client can reach; world port is `8090` | +| Empty world / no NPCs | First database import failed; check `docker compose logs db-init` | +| No bots | Use `TAG=playerbots` and `AI_PLAYERBOT_ENABLED=1` | +| Client crash: interface corrupt | Use the published image from this project; do not strip Turtle addons | + +## Credits + +- Setup walkthrough: [YouTube video](https://www.youtube.com/watch?v=CNgkHs3btNE) +- Server source: [Shyalya/tortoise-wow](https://github.com/Shyalya/tortoise-wow) +- Install notes: [INSTALL-LINUX.md](https://github.com/Shyalya/tortoise-wow/blob/playerbots-integration-gh/INSTALL-LINUX.md) + +Server code stays under the upstream project license. This repository only provides the Docker packaging. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..71a867f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,113 @@ +# Example stack: MariaDB + one-shot DB init + realmd + mangosd +# Copy .env.example to .env and set secrets before starting. +# Image: ghcr.io/nescabir/tortoise-docker — override tag with TAG=no-bots if needed. + +x-turtle-image: &turtle-image ghcr.io/nescabir/tortoise-docker:${TAG:-playerbots} + +services: + db: + image: mariadb:11.8 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-changeme} + MARIADB_AUTO_UPGRADE: "1" + command: + - --character-set-server=utf8mb4 + - --collation-server=utf8mb4_general_ci + volumes: + - db-data:/var/lib/mysql + - ./docker/mariadb.cnf:/etc/mysql/conf.d/turtle.cnf:ro + ports: + - "${DB_PUBLISH_PORT:-3306}:3306" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 10s + timeout: 5s + retries: 30 + start_period: 30s + + db-init: + image: *turtle-image + depends_on: + db: + condition: service_healthy + environment: + DB_HOST: db + DB_PORT: 3306 + DB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-changeme} + DB_USER: ${DB_USER:-mangos} + DB_PASSWORD: ${DB_PASSWORD:-mangos} + DB_LOGIN: ${DB_LOGIN:-tw_logon} + DB_WORLD: ${DB_WORLD:-tw_world} + DB_CHAR: ${DB_CHAR:-tw_char} + DB_LOGS: ${DB_LOGS:-tw_logs} + REALM_NAME: ${REALM_NAME:-TurtleWoW} + REALM_ADDRESS: ${REALM_ADDRESS:-127.0.0.1} + WORLD_PORT: ${WORLD_PORT:-8090} + REALM_ID: ${REALM_ID:-1} + volumes: + - init-marker:/var/lib/turtle-init + entrypoint: ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"] + command: ["db-init"] + restart: "no" + + realmd: + image: *turtle-image + depends_on: + db-init: + condition: service_completed_successfully + restart: unless-stopped + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: ${DB_USER:-mangos} + DB_PASSWORD: ${DB_PASSWORD:-mangos} + DB_LOGIN: ${DB_LOGIN:-tw_logon} + REALM_PORT: ${REALM_PORT:-3724} + BIND_IP: "0.0.0.0" + ports: + - "${REALM_PORT:-3724}:3724" + command: ["realmd"] + + mangosd: + image: *turtle-image + depends_on: + db-init: + condition: service_completed_successfully + realmd: + condition: service_started + restart: unless-stopped + environment: + DB_HOST: db + DB_PORT: 3306 + DB_USER: ${DB_USER:-mangos} + DB_PASSWORD: ${DB_PASSWORD:-mangos} + DB_LOGIN: ${DB_LOGIN:-tw_logon} + DB_WORLD: ${DB_WORLD:-tw_world} + DB_CHAR: ${DB_CHAR:-tw_char} + DB_LOGS: ${DB_LOGS:-tw_logs} + DATA_DIR: /opt/turtle/data + WORLD_PORT: ${WORLD_PORT:-8090} + REALM_ID: ${REALM_ID:-1} + BIND_IP: "0.0.0.0" + LOG_SQL: ${LOG_SQL:-0} + DATABASE_AUTOUPDATE_ENABLED: ${DATABASE_AUTOUPDATE_ENABLED:-1} + LFT_BOTFILL_ENABLE: ${LFT_BOTFILL_ENABLE:-1} + SOLO_DUNGEON_REPOP_ALIVE_ENABLE: ${SOLO_DUNGEON_REPOP_ALIVE_ENABLE:-1} + LEECH_ENABLE: ${LEECH_ENABLE:-1} + AI_PLAYERBOT_ENABLED: ${AI_PLAYERBOT_ENABLED:-1} + AI_MIN_RANDOM_BOTS: ${AI_MIN_RANDOM_BOTS:-10} + AI_MAX_RANDOM_BOTS: ${AI_MAX_RANDOM_BOTS:-10} + ports: + - "${WORLD_PORT:-8090}:8090" + volumes: + - ${DATA_PATH:-./data}:/opt/turtle/data:ro + - mangosd-logs:/opt/turtle/logs + command: ["mangosd"] + # First start builds bot caches / travel data and can take a long time. + stop_grace_period: 2m + +volumes: + db-data: + init-marker: + mangosd-logs: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..3eb4280 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROLE="${1:-mangosd}" +shift || true + +case "${ROLE}" in + db-init|init-db) + exec /usr/local/bin/init-db.sh "$@" + ;; + realmd) + /usr/local/bin/render-config.sh + echo "Starting realmd..." + exec gosu turtle /opt/turtle/bin/realmd -c /opt/turtle/etc/realmd.conf "$@" + ;; + mangosd) + /usr/local/bin/render-config.sh + RUN_DIR="${TURTLE_HOME:-/opt/turtle}/run" + FIFO="${MANGOSD_FIFO:-${RUN_DIR}/mangosd.in}" + mkdir -p "$(dirname "${FIFO}")" + chown turtle:turtle "$(dirname "${FIFO}")" + rm -f "${FIFO}" + # Open the FIFO as the same user that owns it. Creating it in sticky /tmp, + # chowning to turtle, then opening as root fails when fs.protected_fifos=1. + echo "Starting mangosd (console FIFO: ${FIFO})..." + exec gosu turtle bash -c ' + set -euo pipefail + FIFO="$1" + shift + mkfifo "${FIFO}" + exec 3<>"${FIFO}" + exec /opt/turtle/bin/mangosd -c /opt/turtle/etc/mangosd.conf "$@" <&3 + ' bash "${FIFO}" "$@" + ;; + bash|sh) + exec "$@" + ;; + *) + echo "Unknown role: ${ROLE}" >&2 + echo "Usage: entrypoint.sh {db-init|realmd|mangosd}" >&2 + exit 1 + ;; +esac diff --git a/docker/init-db.sh b/docker/init-db.sh new file mode 100644 index 0000000..4c97daa --- /dev/null +++ b/docker/init-db.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +set -euo pipefail + +MARKER_DIR="${INIT_MARKER_DIR:-/var/lib/turtle-init}" +MARKER_FILE="${MARKER_DIR}/initialized" +SQL_ROOT="${SQL_DIR:-/opt/turtle/sql}" + +DB_HOST="${DB_HOST:-db}" +DB_PORT="${DB_PORT:-3306}" +DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-${MYSQL_ROOT_PASSWORD:-}}" +DB_USER="${DB_USER:-mangos}" +DB_PASSWORD="${DB_PASSWORD:-mangos}" +DB_LOGIN="${DB_LOGIN:-tw_logon}" +DB_WORLD="${DB_WORLD:-tw_world}" +DB_CHAR="${DB_CHAR:-tw_char}" +DB_LOGS="${DB_LOGS:-tw_logs}" + +REALM_NAME="${REALM_NAME:-TurtleWoW}" +REALM_ADDRESS="${REALM_ADDRESS:-127.0.0.1}" +WORLD_PORT="${WORLD_PORT:-8090}" +REALM_ID="${REALM_ID:-1}" + +PLAYERBOTS_BUILT="${PLAYERBOTS_BUILT:-ON}" + +if [[ -z "${DB_ROOT_PASSWORD}" ]]; then + echo "DB_ROOT_PASSWORD (or MYSQL_ROOT_PASSWORD) is required." >&2 + exit 1 +fi + +mysql_root() { + mysql -h"${DB_HOST}" -P"${DB_PORT}" -uroot -p"${DB_ROOT_PASSWORD}" --protocol=TCP "$@" +} + +echo "Waiting for MariaDB at ${DB_HOST}:${DB_PORT}..." +for i in $(seq 1 90); do + if mysql_root -e "SELECT 1" &>/dev/null; then + break + fi + if [[ "${i}" -eq 90 ]]; then + echo "MariaDB did not become ready in time." >&2 + exit 1 + fi + sleep 2 +done +echo "MariaDB is ready." + +if [[ -f "${MARKER_FILE}" ]]; then + echo "Init marker found (${MARKER_FILE}); skipping database import." + exit 0 +fi + +if [[ ! -f "${SQL_ROOT}/create_databases.sql" ]]; then + echo "Missing ${SQL_ROOT}/create_databases.sql" >&2 + exit 1 +fi + +echo "Creating databases and base schemas..." +mysql_root < "${SQL_ROOT}/create_databases.sql" + +echo "Creating application user '${DB_USER}' and grants..." +mysql_root <&2 + exit 1 +fi +for f in "${base_files[@]}"; do + echo " -> $(basename "${f}")" + mysql_root "${DB_WORLD}" < "${f}" +done + +echo "Applying database_updates with --force (duplicate keys expected)..." +update_files=("${SQL_ROOT}"/database_updates/*.sql) +for f in "${update_files[@]}"; do + echo " -> $(basename "${f}")" + mysql_root --force "${DB_WORLD}" < "${f}" || true +done + +# AutoUpdater keys applied rows by file SHA1 (not by name). Hash 'manual' +# never matches, so mangosd would retry every update and die on duplicates. +echo "Recording migrations as applied (SHA1 hashes)..." +mysql_root -e "DELETE FROM ${DB_WORLD}.migrations;" +for f in "${update_files[@]}"; do + n="$(basename "${f}" .sql)" + h="$(sha1sum "${f}" | awk '{ print toupper($1) }')" + mysql_root -e "INSERT INTO ${DB_WORLD}.migrations (Name, Hash, AppliedAt) VALUES ('${n}','${h}',NOW());" +done + +# Verify a known schema change from migrations landed. +col_count="$(mysql_root -N -e "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='${DB_WORLD}' AND TABLE_NAME='spell_template' AND COLUMN_NAME='script_name';")" +if [[ "${col_count}" != "1" ]]; then + echo "WARNING: spell_template.script_name not found after migrations (got count=${col_count})." >&2 +fi + +# Playerbot tables (only when this image was built with BUILD_PLAYERBOTS=ON) +normalized="$(echo "${PLAYERBOTS_BUILT}" | tr '[:lower:]' '[:upper:]')" +if [[ "${normalized}" == "ON" || "${normalized}" == "1" || "${normalized}" == "TRUE" ]]; then + PB_SQL="${SQL_ROOT}/playerbots" + if [[ -d "${PB_SQL}" ]]; then + echo "Importing playerbots world SQL..." + cat "${PB_SQL}"/world/*.sql "${PB_SQL}"/world/classic/*.sql | mysql_root "${DB_WORLD}" + echo "Importing playerbots characters SQL..." + cat "${PB_SQL}"/characters/*.sql | mysql_root "${DB_CHAR}" + else + echo "PLAYERBOTS_BUILT=${PLAYERBOTS_BUILT} but ${PB_SQL} is missing." >&2 + exit 1 + fi +else + echo "Skipping playerbots SQL (PLAYERBOTS_BUILT=${PLAYERBOTS_BUILT})." +fi + +echo "Inserting realmlist row..." +mysql_root < "${MARKER_FILE}" +echo "Database init complete." diff --git a/docker/mariadb.cnf b/docker/mariadb.cnf new file mode 100644 index 0000000..4452935 --- /dev/null +++ b/docker/mariadb.cnf @@ -0,0 +1,9 @@ +[mysqld] +character-set-server = utf8mb4 +collation-server = utf8mb4_general_ci +skip-name-resolve +innodb_buffer_pool_size = 512M +max_connections = 200 + +[client] +default-character-set = utf8mb4 diff --git a/docker/render-config.sh b/docker/render-config.sh new file mode 100644 index 0000000..2eca32c --- /dev/null +++ b/docker/render-config.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +ETC="${TURTLE_HOME:-/opt/turtle}/etc" +DATA_DIR="${DATA_DIR:-/opt/turtle/data}" +LOGS_DIR="${LOGS_DIR:-/opt/turtle/logs}" +SQL_DIR="${SQL_DIR:-/opt/turtle/sql}" + +DB_HOST="${DB_HOST:-db}" +DB_PORT="${DB_PORT:-3306}" +DB_USER="${DB_USER:-mangos}" +DB_PASSWORD="${DB_PASSWORD:-mangos}" +DB_LOGIN="${DB_LOGIN:-tw_logon}" +DB_WORLD="${DB_WORLD:-tw_world}" +DB_CHAR="${DB_CHAR:-tw_char}" +DB_LOGS="${DB_LOGS:-tw_logs}" + +WORLD_PORT="${WORLD_PORT:-8090}" +REALM_PORT="${REALM_PORT:-3724}" +REALM_ID="${REALM_ID:-1}" +BIND_IP="${BIND_IP:-0.0.0.0}" + +LOG_SQL="${LOG_SQL:-0}" +AUTO_UPDATE="${DATABASE_AUTOUPDATE_ENABLED:-1}" +LFT_BOTFILL="${LFT_BOTFILL_ENABLE:-1}" +SOLO_DUNGEON_REPOP="${SOLO_DUNGEON_REPOP_ALIVE_ENABLE:-1}" +LEECH_ENABLE="${LEECH_ENABLE:-1}" + +AI_PLAYERBOT_ENABLED="${AI_PLAYERBOT_ENABLED:-1}" +AI_MIN_RANDOM_BOTS="${AI_MIN_RANDOM_BOTS:-10}" +AI_MAX_RANDOM_BOTS="${AI_MAX_RANDOM_BOTS:-10}" + +DB_INFO() { + local db="$1" + printf '%s;%s;%s;%s;%s' "${DB_HOST}" "${DB_PORT}" "${DB_USER}" "${DB_PASSWORD}" "${db}" +} + +set_conf() { + local file="$1" key="$2" value="$3" + if grep -qE "^[[:space:]]*${key}[[:space:]]*=" "${file}"; then + sed -i -E "s|^[[:space:]]*${key}[[:space:]]*=.*|${key} = ${value}|" "${file}" + else + printf '\n%s = %s\n' "${key}" "${value}" >> "${file}" + fi +} + +ensure_conf() { + local dist="$1" conf="$2" + if [[ ! -f "${conf}" ]]; then + if [[ ! -f "${dist}" ]]; then + echo "Missing config template: ${dist}" >&2 + exit 1 + fi + cp "${dist}" "${conf}" + fi +} + +mkdir -p "${ETC}" + +ensure_conf "${ETC}/mangosd.conf.dist" "${ETC}/mangosd.conf" +ensure_conf "${ETC}/realmd.conf.dist" "${ETC}/realmd.conf" + +if [[ -f "${ETC}/aiplayerbot.conf.dist" ]]; then + ensure_conf "${ETC}/aiplayerbot.conf.dist" "${ETC}/aiplayerbot.conf" +fi +if [[ -f "${ETC}/ahbot.conf.dist" ]]; then + ensure_conf "${ETC}/ahbot.conf.dist" "${ETC}/ahbot.conf" +fi + +# mangosd +set_conf "${ETC}/mangosd.conf" "LoginDatabase.Info" "\"$(DB_INFO "${DB_LOGIN}")\"" +set_conf "${ETC}/mangosd.conf" "WorldDatabase.Info" "\"$(DB_INFO "${DB_WORLD}")\"" +set_conf "${ETC}/mangosd.conf" "CharacterDatabase.Info" "\"$(DB_INFO "${DB_CHAR}")\"" +set_conf "${ETC}/mangosd.conf" "LogsDatabase.Info" "\"$(DB_INFO "${DB_LOGS}")\"" +set_conf "${ETC}/mangosd.conf" "DataDir" "\"${DATA_DIR}\"" +set_conf "${ETC}/mangosd.conf" "LogsDir" "\"${LOGS_DIR}\"" +set_conf "${ETC}/mangosd.conf" "WorldServerPort" "${WORLD_PORT}" +set_conf "${ETC}/mangosd.conf" "BindIP" "\"${BIND_IP}\"" +set_conf "${ETC}/mangosd.conf" "RealmID" "${REALM_ID}" +set_conf "${ETC}/mangosd.conf" "LogSQL" "${LOG_SQL}" +set_conf "${ETC}/mangosd.conf" "Database.AutoUpdate.Enabled" "${AUTO_UPDATE}" +set_conf "${ETC}/mangosd.conf" "Database.AutoUpdate.Path" "\"${SQL_DIR}/\"" +set_conf "${ETC}/mangosd.conf" "LFT.BotFill.Enable" "${LFT_BOTFILL}" +set_conf "${ETC}/mangosd.conf" "SoloDungeonRepopAlive.Enable" "${SOLO_DUNGEON_REPOP}" +set_conf "${ETC}/mangosd.conf" "Leech.Enable" "${LEECH_ENABLE}" + +# realmd (note: key name has no dots between LoginDatabase and Info) +set_conf "${ETC}/realmd.conf" "LoginDatabaseInfo" "\"$(DB_INFO "${DB_LOGIN}")\"" +set_conf "${ETC}/realmd.conf" "RealmServerPort" "${REALM_PORT}" +set_conf "${ETC}/realmd.conf" "BindIP" "\"${BIND_IP}\"" + +if [[ -f "${ETC}/aiplayerbot.conf" ]]; then + set_conf "${ETC}/aiplayerbot.conf" "AiPlayerbot.Enabled" "${AI_PLAYERBOT_ENABLED}" + set_conf "${ETC}/aiplayerbot.conf" "AiPlayerbot.MinRandomBots" "${AI_MIN_RANDOM_BOTS}" + set_conf "${ETC}/aiplayerbot.conf" "AiPlayerbot.MaxRandomBots" "${AI_MAX_RANDOM_BOTS}" +fi + +mkdir -p "${LOGS_DIR}" +# Writable for the turtle user (configs may be regenerated each start). +chown -R turtle:turtle "${ETC}" "${LOGS_DIR}" 2>/dev/null || true + +echo "Configs rendered under ${ETC}" diff --git a/docker/repair-migrations.sh b/docker/repair-migrations.sh new file mode 100644 index 0000000..1927b7b --- /dev/null +++ b/docker/repair-migrations.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Repair tw_world.migrations after an init that stored Hash='manual'. +# Safe to re-run. Does not re-import SQL. +set -euo pipefail + +SQL_ROOT="${SQL_DIR:-/opt/turtle/sql}" +DB_HOST="${DB_HOST:-db}" +DB_PORT="${DB_PORT:-3306}" +DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-${MYSQL_ROOT_PASSWORD:-}}" +DB_USER="${DB_USER:-mangos}" +DB_PASSWORD="${DB_PASSWORD:-mangos}" +DB_WORLD="${DB_WORLD:-tw_world}" + +mysql_cmd() { + if [[ -n "${DB_ROOT_PASSWORD}" ]]; then + mysql -h"${DB_HOST}" -P"${DB_PORT}" -uroot -p"${DB_ROOT_PASSWORD}" --protocol=TCP "$@" + else + mysql -h"${DB_HOST}" -P"${DB_PORT}" -u"${DB_USER}" -p"${DB_PASSWORD}" --protocol=TCP "$@" + fi +} + +shopt -s nullglob +files=("${SQL_ROOT}"/database_updates/*.sql) +if [[ "${#files[@]}" -eq 0 ]]; then + echo "No migration files under ${SQL_ROOT}/database_updates" >&2 + exit 1 +fi + +echo "Clearing old migration rows..." +mysql_cmd -e "DELETE FROM ${DB_WORLD}.migrations;" + +echo "Inserting ${#files[@]} rows with SHA1 hashes..." +for f in "${files[@]}"; do + n="$(basename "${f}" .sql)" + h="$(sha1sum "${f}" | awk '{ print toupper($1) }')" + mysql_cmd -e "INSERT INTO ${DB_WORLD}.migrations (Name, Hash, AppliedAt) VALUES ('${n}','${h}',NOW());" +done + +count="$(mysql_cmd -N -e "SELECT COUNT(*) FROM ${DB_WORLD}.migrations;")" +echo "Done. migrations rows: ${count}"