audit findings #1 and #2, the pair that makes the idle-board checksum mean something again. #1 the server hashed at its own free-running tick. sector_command calls advance(client_tick) but advance_to is forward-only and the 50ms ticker already ran the session past it, so it hashed at session.tick, not the client's - and the tick is the first field in the checksum, so it could never agree even on a bit-exact board. the session now records the checksum of every tick it simulates and advance() returns the one for the tick the client actually reported. #2 the real idle divergence. the king is dormant at full health, and LogicSummoner::updateCombatComponentState clears the combat bit of its component mask every tick (13 -> 12) while field_256 <= KING_ACTIVATE _TIME_MS; the mask is hashed for every object, so a server holding 13 disagreed on every tick and re-agreed only on the snapshot tick. the sim now mirrors it: field_256 stays 0 while the king is unhurt and both princess towers stand, ramps by 50/tick once it takes damage or loses a tower, and the combat bit turns on only past KING_ACTIVATE_TIME_MS. the king is also built with the bit already clear. princess towers are plain characters and keep bit0 - verified against the client, which routes them through the base updateCombatComponentState that sets it. harness: dormant kings read mask 12, all four princess towers 13, and an idle state hashes the same twice. |
||
|---|---|---|
| assets | ||
| config | ||
| crates | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
scroll-server
A server for the scroll client (Clash Royale 2.0.1306, iOS). Takes a real
client from TCP connect to the lobby: login, own home data, chests, card
upgrades, the shop.
Requirements
- Rust 1.75+
- PostgreSQL 16
Running
createdb scroll
cargo run --release -p scroll-server
Listens on 0.0.0.0:9339, the port the client hardcodes. The schema is created
on first boot. DATABASE_URL defaults to postgres:///scroll.
On macOS a Homebrew cluster may need LC_ALL=C to start at all.
That runs everything in one process. For real isolation, SCROLL_MODE=supervised
makes it spawn the three services as child processes and restart any that dies,
with backoff:
SCROLL_MODE=supervised ./target/release/scroll-server
They can also be started by hand, in any order:
cargo run --release -p auth-service
cargo run --release -p game-service
cargo run --release -p gateway
Checking it without a phone
scroll-probe speaks the same protocol and prints the decoded lobby:
cargo run --release -p gateway --bin scroll-probe -- 127.0.0.1:9339
Flags: --claim-free-chest, --buy-chest <name>, --desync. Pass an account
and pass token to reconnect as an existing player:
scroll-probe 127.0.0.1:9339 0 1 <passtoken>.
Pointing a client at it
The client hardcodes game.clashroyaleapp.com. Patch that string in the binary
inside the ipa to your machine's address, keeping the 23 byte slot NUL padded so
nothing around it moves.
Configuration
| variable | default |
|---|---|
DATABASE_URL |
postgres:///scroll |
SCROLL_MODE |
single, or supervised |
SCROLL_BIN_DIR |
next to the running binary |
SCROLL_AUTH_LISTEN |
127.0.0.1:9401 |
SCROLL_GAME_LISTEN |
127.0.0.1:9402 |
SCROLL_GATEWAY_LISTEN |
0.0.0.0:9339 |
SCROLL_CSV_ROOT |
assets |
SCROLL_SHOP |
config/shop.json |
SCROLL_STARTER_PROFILE |
built in |
RUST_LOG |
info |
Shop
config/shop.json lists what the server sells, as Name#count:
{
"offers": [
{ "id": 1, "give": "Gold#1000" },
{ "id": 5, "give": "chest:Gold#1" },
{ "id": 7, "give": "Diamonds#500", "cost": "Gold#20000" }
]
}
Names resolve against the csv tables. cost is optional; without it the server
charges the same price the client computes from the game data.