Skip to content

About

A locally hosted private clash royale server with an admin panel serving full control and customizability - one-click installation in pinokio.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Repository files navigation

Slash Royale Legacy

Clash Royale 1.9.2 private server based on HashRoyale (itself a fork of ZrdRoyale), running on C#/.NET 8 with MariaDB, packaged with a web admin panel and a Pinokio launcher.

Install it from Pinokio, and the panel gives you an all-in-one way to run and manage your server:

  • one-click start for the whole stack (MariaDB + main game server + battle server) with live logs
  • edit game data (csv_logic / csv_client CSV files) and browse/edit the game database
  • administer players (level, trophies, cards, admin/ban), tune game rules, set the address players connect to
  • rebuild your client APK patched to point at your server, signed with a locally generated keystore

Components & ports

Component Lives in Managed in
Admin panel (Node.js) app/ —
MariaDB (portable) env/mariadb Server tab
Main game server (.NET 8) server/publish Server tab
Battle server (.NET 8) server/publish-battles Server tab (only started when enabled)
APK toolchain (OpenJDK 21+) env/jdk APK Builder tab

Ports (fixed by the server code):

  • 9339 – main server, where the game client connects
  • 9449 – battle server (only when the battle server is enabled, see Config tab)
  • 9876 – main ↔ battle cluster (only when the battle server is enabled; it is a plain TCP socket despite the "UDP" naming)
  • 3306 – MariaDB (the server connection setting has no port option, so MariaDB must stay here)
  • 3000 – admin panel

Battle server is on by default. With use_udp: true matches run on the separate ClashRoyale.Battles process, which the main server hands each client over on UDP 9449 (so UDP 9449 must be forwarded to this machine just like TCP 9339 is, for remote players). Turn it off under Config tab → Battle server to run battles on the main server over each player's existing 9339 connection instead.

Getting started

  1. Install (install.js) – installs the .NET 8 SDK (env/dotnet), an OpenJDK 21+ (env/jdk) and a portable MariaDB (env/mariadb) for your platform, retargets the battle project to .NET 8, publishes both game servers, and installs panel dependencies.
  2. Start (start.js) – launches the admin panel. Hit Start server on the Server tab to bring up the database and main server together (the battle server too if it is enabled). Each process is only marked "up" once it genuinely reports ready, and nothing listens on 3306/9339/9449 until you start the stack. The database and its schema are created automatically on the first start.
  3. Build an APK and install it on a phone (see below) — players then connect to your address on port 9339.

The launcher menu also offers Update (re-apply the .NET 8 retarget, re-publish both servers, refresh panel dependencies) and Reset (wipe env/, app/node_modules, app/data and both server/publish* folders so you can reinstall from scratch).

Using the panel

Server & live logs

The Server tab shows the status of the database, main and battle servers, a Start / Restart button, and the live log with history replayed on open. The battle server pill shows a "disabled" state when the battle server is off. Sources: app, db, main, battles, apk, gamefiles, activity. Starting the stack clears the log first so you only see the current session.

While the stack runs, player activity is detected by polling the player table every 2 s and logged under the activity source:

  • registered – a new player row appeared
  • logged in (#N) – the player's session count went up
  • disconnected (session lasted …) – a play session ended, with its duration

Edit game data (CSV tab)

Edits the server's GameAssets files live (csv_logic/* card stats and csv_client/* client data) — no rebuild needed. Changes are picked up on the next main-server restart. Restore pristine data resets all GameAssets from the untouched source clone.

Why your edit didn't show up in-game: card stats (damage, hitpoints, unit counts…) are computed by the client from the CSV copies shipped inside the APK — the servers only relay battle commands. Editing the server's CSVs changes what the server knows, but the client keeps its baked-in stats until you rebuild the APK (baking is enabled by default) so your edited CSVs are written into the client.

Database (Database tab)

Start the server stack first. Lists every table in the game database with row counts and sizes; browses rows (paginated, editable cells) and runs an SQL console (one statement at a time, reads and writes; results capped at 1000 rows — add a LIMIT). Save row writes an UPDATE keyed on the primary key and Delete removes the row; both are guarded by a confirm when "confirm writes" is on. Tables without a primary key are read-only. Uses the same root credentials the servers use.

Player Trophies is read-only-ish. The server mirrors this column from the player's Home JSON and overwrites it on every save. Use the Players tab instead — it writes the underlying Home JSON and keeps the mirror in sync.

Players (Players tab)

A no-raw-JSON way to administer accounts: list/filter every player (level, trophies, gold, gems, session count, admin/banned badges); rename, set level (1–13), trophies, gold, gems (trophy edits recompute the arena and the mirrored Trophies column); Unlock all cards / Max all cards (level 13); toggle admin / banned — written to the main server's config.json (admins / banned_ids, the same lists the /admin and /ban chat commands use), applied on the next main-server restart. Data edits apply on the player's next login; best done while the stack is stopped or the player is offline.

Config tab

  • Connect – the server address baked into rebuilt APKs. Defaults to this machine's LAN IP; Use local IP fills it in automatically (the default, for players on your network) and Use public IP asks an echo service for the address outside players must reach (only works if TCP 9339 is forwarded to this machine). It's written into the client the next time you build an APK.
  • Battle server – enables/disables the separate battle server (use_udp in the main server's config.json, written immediately; on by default). When off, no battle-server process is started and matches run on the main server over each player's existing connection. When on, matches run on the separate ClashRoyale.Battles process. Disabling stops the battle server process right away; both directions apply for sure on the next server-stack restart.
  • Game rules – edits the main server's config.json directly (restart the stack to apply). Out-of-the-box values: MinTrophies 25 / MaxTrophies 34 — the winner of a regular battle gets a random trophy value in that range (Random.Next(Min, Max), so 25–33), while friendly and 2v2 battles always award 0; DefaultGold 1000, DefaultGems 1000, DefaultLevel 1, GemsToGiveAfterMatch 0, GoldToGiveAfterMatch 0 (0 = grant the current arena's win gold — the same amount the client shows on the win screen, e.g. 7 in Arena 2; set a non-zero value to override with a fixed gold per battle).

Building the client APK

The APK Builder tab repackages the RetroRoyale base client at app/assets/retroroyale.apk:

  • Base client download – if the APK file is missing, the builder shows a popup that downloads it for you (~90 MB).
  • Patches the server address into both 32-bit libg.so binaries (cluster.retroroyale.xyz is replaced; see the length note below), conditionally applies the battle checksum patch, and re-signs with a locally generated keystore (app/data/keystore/release.jks).
  • Sets the installed app name & icon – the name defaults to SlashRoyale and the launcher icon to the bundled app/assets/slashroyalelegacy.png; both are editable under Icon & app name in the APK Builder tab (Choose image uploads a custom PNG, Restore default resets both). They are applied to every build. The name is written into the string/app_name value in resources.arsc for all 6 locales, so it also sticks on non-English devices; AndroidManifest.xml is never touched. The icon is regenerated for every res/drawable-*/ic_launcher.png density. Settings persist in app/data/settings.json.
  • Bakes your CSV edits (csv_logic + csv_client) into the client: each genuine, schema-valid edit is decompressed, checked against the client's own copy (identical headers/type rows, column and row counts, no embedded newlines/NULs), then re-encoded and compressed into the client's SC/LZMA format inside the APK. Unchanged, stale, reduced or corrupt files (e.g. upstream's skins.csv, globals.csv) stay byte-for-byte untouched so a bad server file can never break the client at boot. Requires Python with the standard lzma module on PATH.
  • If a battle patch offset doesn't match the expected bytes it is skipped with a warning and the build still completes. The base APK is never written to.
  • The build output (e.g. app/data/apk/clash-royale-<timestamp>.apk) replaces any previous build.

Android version caveat: the RetroRoyale-era client ships 32-bit native libraries only (armeabi-v7a, x86). Android 16+ phones are 64-bit-only and will refuse to install it. Use a 32-bit-capable device or an Android ≤ 15 handset. The builder shows the client's ABIs so you can check before transferring.

API

The panel exposes a JSON API under http://127.0.0.1:3000/api.

JavaScript

const base = "http://127.0.0.1:3000/api";
const res = await fetch(`${base}/status`);
const data = await res.json();
console.log(data.servers.main.state);

curl

# overall status (db, server stack, toolchain)
curl http://127.0.0.1:3000/api/status

# one-click stack: start / stop / restart everything together
curl -X POST http://127.0.0.1:3000/api/stack/start
curl -X POST http://127.0.0.1:3000/api/stack/stop
curl -X POST http://127.0.0.1:3000/api/stack/restart
curl http://127.0.0.1:3000/api/stack/status

# set the address baked into the client APK
curl -X POST http://127.0.0.1:3000/api/settings/address \
  -H "Content-Type: application/json" \
  -d '{"address":"192.168.1.50"}'

# look up the LAN IP or the public IP the echo service sees
curl http://127.0.0.1:3000/api/settings/localip
curl http://127.0.0.1:3000/api/settings/publicip

# enable/disable the separate battle server (use_udp); on by default
curl -X POST http://127.0.0.1:3000/api/settings/battles \
  -H "Content-Type: application/json" \
  -d '{"enabled":false}'

# edit a game CSV (card stats / client data)
curl http://127.0.0.1:3000/api/gamefiles/csv_logic/characters.csv
curl -X POST http://127.0.0.1:3000/api/gamefiles/csv_logic/characters.csv \
  -H "Content-Type: application/json" \
  -d '{"headers":["Name","Rarity"],"types":["text","text"],"data":[["Golem","Epic"]]}'

# build a patched APK (long-running; watch /api/logs/all/stream)
curl -X POST http://127.0.0.1:3000/api/apk/build \
  -H "Content-Type: application/json" \
  -d '{"address":"127.0.0.1","patchAddress":true,"patchBattles":true,"bakeGamefiles":true}'

# installed app name & icon (applied on the next build)
curl http://127.0.0.1:3000/api/apk/meta
curl -X POST http://127.0.0.1:3000/api/apk/meta \
  -H "Content-Type: application/json" \
  -d '{"appName":"SlashRoyale","icon":"default"}'
# custom icon: upload a base64 PNG data URL (sets icon to "custom")
curl -X POST http://127.0.0.1:3000/api/apk/icon \
  -H "Content-Type: application/json" \
  -d '{"dataUrl":"data:image/png;base64,<...>"}'
# preview the icon the next build will use
curl http://127.0.0.1:3000/api/apk/icon/preview --output icon.png

# live logs (Server-Sent Events)
curl -N http://127.0.0.1:3000/api/logs/all/stream

Endpoint reference

Method Path Purpose
GET /api/health liveness probe
GET /api/status db / servers / toolchain / address / configs summary
GET /api/settings/localip this machine's suggested LAN address
GET /api/settings/publicip the machine's public IP as seen by an external echo service (api.ipify.org)
POST /api/settings/address set the address baked into client APKs ({address})
POST /api/settings/battles enable/disable the battle server, {enabled: bool} → writes use_udp; disabling stops the battles process (returns requiresRestart)
POST /api/stack/start /api/stack/stop /api/stack/restart one-click stack control
GET /api/stack/status current stack state
GET /api/update/check check GitHub for newer versions (?force=1 bypasses the 5-min cache)
POST /api/db/start /api/db/stop database lifecycle (low-level)
GET /api/db/status database status (low-level)
POST /api/server/:name/:action main/battles × start/stop/restart (low-level)
GET /api/gamefiles list csv_logic / csv_client files
GET/POST /api/gamefiles/:file.csv read / edit a CSV table (e.g. csv_logic/characters.csv)
POST /api/gamefiles/restore restore pristine GameAssets
GET /api/gamefiles/diff files changed vs. pristine
GET /api/dbview/tables list tables in the game database (rows, sizes)
GET /api/dbview/table/:table paginated rows + schema (?limit=&offset=; returns pk)
POST /api/dbview/query run one SQL statement, read or write
POST /api/dbview/update UPDATE a row by primary key ({table, pk, set})
GET /api/players list players (level, trophies, gold, gems, admin, banned)
POST /api/players/:id edit a player: {patch: {name, level, trophies, gold, gems}}, {cards: "unlock"|"max"}, {admin: bool}, {banned: bool} (admin/ban → restart main)
GET/POST /api/config/main read / patch the main server's config.json (numeric knobs via {config: {...}})
POST /api/apk/build build patched APK (address, patchAddress, patchBattles, bakeGamefiles, outputPath)
GET /api/apk/status base APK / keystore / JDK / last build info
GET/POST /api/apk/meta read / set installed app name & icon ({appName, icon:"default"|"custom"})
POST /api/apk/meta/restore reset the name to SlashRoyale and the icon to the default
POST /api/apk/icon upload a custom launcher icon (base64 PNG data URL in {dataUrl})
GET /api/apk/icon/preview PNG of the icon the next build will use
POST /api/apk/download start the base-client download (if missing)
GET /api/apk/download/stream base-client download progress (SSE)
GET /api/logs all buffered log lines per source
GET /api/logs/all/stream SSE stream of every source
GET /api/logs/:proc/stream SSE stream for one source (app, db, main, battles, apk, activity, gamefiles)

Notes & limitations

  • MariaDB occupies 3306. If a system MySQL/MariaDB already listens there, the panel refuses to start its own instance instead of fighting it. Move the other service to a different port.
  • Battle server (UDP) needs its own forwarding. When use_udp: true (the default) the main server hands clients your Connect address on UDP 9449, so besides TCP 9339 you must forward UDP 9449 to this machine. Until that port is reachable, remote devices freeze ~3 s into a battle. Turn it off under Config tab → Battle server to run matches on the main server over TCP instead.
  • Address is ≤ 21 characters. It replaces cluster.retroroyale.xyz (21 bytes) in each libg.so; a longer value cannot fit. Defaults to this machine's LAN IP; change it on the Config tab → Connect.
  • The battle checksum patch targets specific offsets in the RetroRoyale-derived libg.so. On a base APK where those bytes differ, the patch is skipped with a warning rather than corrupting the binary.
  • The APK is signed with v1 (JAR) signatures only (needed for old-Android compatibility) and is not zipaligned; the retro client runs from anywhere, but release-store distribution should run zipalign afterwards.

Every mutating call returns { ok: true, ... } and errors return { ok: false, error } with a 4xx/5xx status.

About

A locally hosted private clash royale server with an admin panel serving full control and customizability - one-click installation in pinokio.

Topics

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages