Commit graph

83 commits

Author SHA1 Message Date
WiseDev
7a0ba1b652 give the server its own battle clock and end condition
LogicBattle::isEndConditionMatched, transcribed: the battle is over when
end_counter is positive, when either king is dead, when tick/20 seconds
reach MatchLength + OvertimeSeconds, or - once past MatchLength - when
the crowns differ. the divisor is the 0x66666667/2^35 multiply in the
client, which is a divide by twenty, so the battle runs at 20 ticks a
second.

crowns come from LogicSummoner::getStars: three when the enemy king is
down, otherwise two minus the enemy princess towers still standing,
which is exactly what leader_towers holds.

BattleRegistry keeps the LogicGameMode we built for each account and
advances it on the tick the client reports in its turn message, so the
server now tracks the clock, the crowns and whether the battle is over,
and drops the session when the player goes home. the two isSummoner
guards in the overtime branch are left out: they only fire for an object
that is not a summoner, which a king tower always is.
2026-08-23 12:01:26 +03:00
WiseDev
a8dcd683b2 resume the home logic when the player comes back
BattleScreen::createBattleEndHUD takes a null result message and builds
the end screen from the local battle, then BattleScreen::sendGoHomeMessage
fires - so in this mode the client decides the battle is over and asks to
go home on its own. we now clear the stopped flag on that request instead
of leaving the session wedged until the next login.

battle turns are logged with their tick, checksum and command types while
the flag is up, so the traffic at the end of a battle is visible.
2026-08-23 11:56:04 +03:00
WiseDev
9d47484fe1 decode the two battle commands
command type 1 is LogicDoSpellCommand - the card the player put on the
field. it carries the usual command header, the deck slot, an optional
LogicSpell and the drop position. type 2 is
LogicCompleteTutorialBattleCommand, which reads its global id before the
header the way the achievement command does.

both execute to Ignored for now: the home logic is stopped during the
battle, so nothing on the server acts on them yet. decoding them stops
EndClientTurnMessage from being thrown away whole, which is what the
"unknown command type 1" warnings were.
2026-08-23 11:51:17 +03:00
WiseDev
5bca19da50 refill the hand, and decode SendBattleEventMessage
LogicSummoner::tick guards the whole hand-refill block on the first vint
of the deck block being at least 1 - it is the number of hand slots the
client scans for an empty one before pulling from the draw queue. we
sent zero, so the hand we dealt was the only one the player ever got.
named after what it is and set to four.

12951 is SendBattleEventMessage, the in-battle emotes and quick chat. it
carries a LogicBattleEvent: a type byte, the sender account and three
int lists - ticks, coordinate pairs and params. it decodes now instead
of being dropped with a warning. the client renders its own emote
locally, so nothing is echoed back yet; the relay belongs with a real
opponent.
2026-08-23 11:47:25 +03:00
WiseDev
3cba764841 deal the summoner a starting hand
the card bar was empty because the hand lives on the king tower, not on
the battle. LogicSummoner::decode gates a whole block behind one boolean
- setEncodeDeckDataEnabled on the client side - and we always wrote it
false, so the client kept the hand and the draw queue it was born with,
which is nothing.

the block holds four deck indices for the hand, then two int lists: the
draw queue getNextSpell walks and the used pile reshuffleDeck folds back
into it. we now hand out the first four slots and queue the rest, leave
the used pile empty and keep last used at -1, which is what
getLastUsedSpell reads as "none".

starting mana comes from the START_MANA global instead of zero, and the
field after last-used is named after clearSpellCooldowns, which is the
only thing that writes it.
2026-08-23 11:39:49 +03:00
WiseDev
58d137ccd2 teach the snapshot verifier about decks
verify_snapshot still carried the stub from when both decks were always
absent, so the first battle that actually carried one was refused before
it reached the client. it reads them now: eight presence bits, then a
data reference, five vints and two booleans per filled slot.

while in there, the six battle booleans were all being discarded, which
hid the fact that the two score-change vints are only present when the
first of them is set. the verifier tracks it now, same as the encoder.
2026-08-23 11:31:50 +03:00
WiseDev
27f99ee898 match against a bot player instead of an npc
the decks were the visible half: LogicBattle carries one LogicSpellDeck
per player and we wrote both as absent, so the client cleared them and
the card bar came up empty. both sides get a real deck now - the player
from their profile, the bot from the fullest row of predefined_decks.

matchmaking no longer picks a row out of npcs.csv. it takes the location
from the arena's PvpLocation column and builds an opponent avatar with
its own name, arena and trophies, so the battle reads as a player match
rather than a trainer one. StartMissionMessage still goes through the
npc path unchanged.

the battle type stays 1 on purpose. LogicGameMode::isImmediateMessageExecution
is (type - 1) < 3, so 1, 2 and 3 let the client simulate locally while 0
makes it wait for the server to drive the sector - which needs the real
tick loop we do not have yet.
2026-08-23 11:28:17 +03:00
WiseDev
4477b12f0a stop the home logic once the battle starts
the client sends EndClientTurnMessage during the battle too, but its
tick and checksum belong to the battle, not to the home. we kept
comparing them against the home checksum and answered with
OutOfSyncMessage, which is the "Client and server are out of sync!"
dialog on tick 60.

HomeMode carries the stopped flag now and returns an empty turn result
while it is set, and sector_state_for raises it, so both the mission and
the matchmaking entry points are covered.
2026-08-23 11:19:26 +03:00
WiseDev
6c5295bc07 fix the LogicGameObject field mapping
the base object writes owner index, component mask, position and z, in
that order. we had the first field unnamed and were writing the owner
into the z slot, so every object belonged to player 0, and the mask slot
carried a zero.

the mask matters: getHitpointComponent tests bit 2 of it before touching
components[2], so a zero mask made every tower report no hitpoints,
LogicCharacter::isAlive fell through to the z field, shouldDestruct went
true and the towers were destroyed on the first tick - which then hit
"cant find summoner tower" in LogicBattle::removeGameObjectReferences.

entries are built through LogicGameObjectEntry::new now so the mask is
derived from the component array and cannot drift from it.

also rename the first character flag after what sets it: kamikaze
death and morph both raise it right before the object is removed.
2026-08-23 11:17:10 +03:00
WiseDev
bc04651a73 write the compression flag byte in SectorStateMessage
SectorManager::receiveSectorState reads one byte off the front of the
message body and branches on it: 1 goes to receiveCompressedSectorState,
anything else falls through to the plain decode. we never wrote that
byte, so the client ate the first byte of the snapshot as the flag and
then read every field one byte early - server tick 0, discard 11, and
the section sentinel landed on the LogicTime tick instead of 11.

drop the two bisect switches, they served their purpose: both settings
aborted identically, which is what ruled out the object payload.
2026-08-23 11:09:27 +03:00
WiseDev
e984aa7d15 verify the snapshot before sending it, and add two bisect switches
the client aborts in Debugger::error("Full update stream is corrupted!"), tail
called out of LogicGameMode::decode, which is why the stack blames
receiveSectorState. only one error path exists there so it is a sentinel.

verify_snapshot reads the snapshot back following the client decoders and
refuses to send anything that does not land both sentinels with no trailing
bytes. it passes today, so the layout matches what i believe the client reads.

checked every reader against the binary: LogicGameMode, LogicBattle,
LogicGameObjectManager, LogicGameObject, LogicCharacter, LogicSummoner, all
three components, LogicClientAvatar, readGlobalID, readDataReference,
readConstantSizeIntArray, readGameObjectReference, decodeComponent, and
setLevelIndex for the component set. all match.

SCROLL_BATTLE_SUMMONER=0 encodes the king towers as plain characters,
SCROLL_BATTLE_PRINCESS=0 drops the princess towers. one client run with each
splits the remaining hypotheses.
2026-08-23 10:50:03 +03:00
WiseDev
fcb07bba64 fix the buff component array size, it desynced the snapshot
the client reads getTable(8)->getItemCount() entries there, table 8 is
damage_types with 5 rows. i was writing character_buffs, 12 rows, so every
snapshot carried 7 extra vints and 7 extra booleans and the stream slid.

that is what tripped the sentinel and aborted the client in
Debugger::error("Full update stream is corrupted!").

verified the rest field by field against the client decoders: LogicGameMode,
LogicBattle, LogicGameObjectManager, LogicGameObject, LogicCharacter,
LogicSummoner and LogicCombatComponent all match. the conditional character
fields stay off because ReloadAfterHits and ManaGenerateLimit are empty for both
tower rows.
2026-08-23 10:35:26 +03:00
WiseDev
6dc4caede9 matchmake against a bot instead of searching forever
537 only tells the client's ui to start searching, the handshake after it was
missing: server sends 24106 StopHomeLogic, client answers 14105, server sends
21903. that last step now builds the same snapshot the npc mission does, using
npcs row 0 as the opponent.

battle type stays 1 so it runs on the client's offline path. a real pvp battle
is type 0 and needs the udp sector channel, which does not exist here.

14107 CancelMatchmake now answers 24125 instead of being ignored.
2026-08-23 10:28:28 +03:00
WiseDev
774e7c60cf build and send the battle sector state
npc missions used to get a ServerErrorMessage back. now StartMission builds a
LogicGameMode snapshot off the arena tilemap and answers 21903.

towers come from assets/locations/*.csv the way initDefaultSector does it: tile
coordinates times 500, leader index decided by which half of the map the tower
sits in. the two king towers must be there, the client dereferences them without
a null check. they live in buildings.csv, not characters.csv.

LogicCharacter puts the base object fields fourth, not first. the buff component
writes a fixed array sized by the character_buffs row count even with no buffs.

training_arena parses to 2 kings, 4 princess towers, 36x64 subtiles. snapshot is
602 bytes over 6 objects. the real client has not seen it yet.
2026-08-23 10:24:20 +03:00
WiseDev
d439027735 pull the arena tilemaps out of the ipa
res/locations/*.csv, eight of them. these are what LogicBattle::initDefaultSector
walks to place the towers: training_arena has KingTower at tile 18,6 and 18,58
and four PrincessTowers, plus a 64x24 tile bitmask.

without these there is no way to build a battle snapshot, the leaders come from
here and the client dereferences them without a null check.
2026-08-23 10:13:37 +03:00
WiseDev
41eca5b620 battle model: LogicBattle, the object manager and BattleResultMessage
field order taken straight off LogicBattle::encode. the object manager is
columnar, not object major: 6 counters, a count, then all data refs, then all
global ids, then all objects, then four component passes. global ids must be
ascending, the client binary searches them.

LogicRandom moved out of the shop into its own module. its abs was
checked_abs().unwrap_or(0), the binary uses NEGS so i32::MIN stays i32::MIN.
one seed in four billion, but it would have desynced the card shop.

per object subclass encodes and the component payloads are still missing, so no
snapshot can be sent yet.
2026-08-23 10:00:16 +03:00
WiseDev
68e2b87a0a decode the four types the client kept sending
14303 AskForJoinableAlliancesList and 14107 CancelMatchmake are both genuinely
empty payloads. 516 UpdateLastShownLevelUp is header only, 537 StartMatchmake
carries a vint and a bool. none of them move the home checksum.

that clears the unknown type warnings out of the log.
2026-08-23 09:48:49 +03:00
WiseDev
c0713b50d7 supervised mode: run the services as child processes
SCROLL_MODE=supervised spawns auth-service, game-service and gateway, waits for
each rpc port before starting the next, and restarts whatever dies with backoff
from 500ms to 30s. the counter resets once a service has been up a minute.

single process mode is still the default and unchanged.

kill_on_drop means ctrl-c takes the children with it, no orphans.
2026-08-23 09:46:59 +03:00
WiseDev
e43f3fc02c unwind panics instead of aborting the process
a panic in a session task now kills that connection, tokio catches it and the
accept loop carries on. with abort it took the whole server down.

the data table RwLock is std so it poisons on panic, and every lookup went
through expect(). one panic would have bricked every later table read, which is
worse than the abort was. it reads the inner value now, the table is read only
once installed.
2026-08-23 09:42:44 +03:00
WiseDev
ca8e41c2c5 roll chest loot from the csv instead of a fixed config
RandomSpells / DifferentSpells / RareChance / EpicChance / MinGold / MaxGold all
come off the chest row now, gold is scaled by the player's arena. magic chest is
30 cards over 8 cards with 1 epic and 6 rares, like the client says it is.

arena chest rows inherit everything through BaseChest, so Free_Arena1 reads Free.
2026-08-23 09:30:38 +03:00
WiseDev
573d70ff08 deck swapping and achievement claims
500 LogicSwapSpellsCommand does the real work, 520 and 521 are no-ops in this
build so they just decode. 535 puts its data reference BEFORE the base header,
unlike every other command.

collection only shrinks when a card moves into an empty deck slot, that is the
one case where the home checksum moves.
2026-08-23 09:24:30 +03:00
WiseDev
9d10cc38f9 add MIT license 2026-08-23 09:13:19 +03:00
WiseDev
f218a45ed2 fix chest shop prices and gate the free chest
rounding is banded and the 5s band biases +3, so 22 goes to 25 not 20. numbers match the csv for arena 1 and 2.
free chest end timestamp needs migration 0002.
2026-08-23 09:02:49 +03:00
WiseDev
a2bf58a627 stop minting a new account on every login
if the client presents an id we don't know, create it under that id and bump the sequence past it.
also chest offers match through BaseChest, so one silver row covers every arena.
2026-08-23 08:54:43 +03:00
WiseDev
8a745d634c generate the daily card shop server side
prices come from the csv globals now, a json cost is just an override.
DATABASE_URL defaults to postgres:///scroll when unset.
2026-08-23 08:47:55 +03:00
WiseDev
ad5a1613e3 move players and accounts into postgres
json stores are gone, they nulled out every data ref on load and wrote the null straight back on save.
shop json and the purchase commands ended up in here too.
2026-08-23 08:40:13 +03:00
WiseDev
d25a6de423 move all crates into crates/, glob members
pure move, no code touched. readme and protocol.md paths fixed up.
2026-08-23 08:15:45 +03:00
WiseDev
972c61dae7 replace command enum with a dyn trait + registry
new logic-derive Command macro submits each command to inventory, manager decodes by vint type. titan-derive loses tagged enums since nothing uses them now. rest of the diff is rustfmt.
2026-08-23 08:05:58 +03:00
scroll
d8ed6ae2ee drop unused Payload import 2026-08-23 07:56:30 +03:00
scroll
2151d4ccbe auto-register messages via inventory
message_registry! is gone, the registry is a process-wide global now.
2026-08-23 07:56:09 +03:00
scroll
323450a977 home mode docs + --desync flag on the probe 2026-08-23 07:52:21 +03:00
scroll
20a75dbb25 nuke the tests and the dead code
data/ is runtime state, keep it out of git.
2026-08-23 07:51:04 +03:00
scroll
812052bc3e initial import, client gets to the lobby
titan engine, logic model, auth/game/gateway services.
csv tables pulled out of the ipa are checked in so it runs out of the box.
2026-08-23 07:50:31 +03:00