Commit graph

9 commits

Author SHA1 Message Date
WiseDev
f0f739c02c drop the commentary and run a workspace-wide format pass 2026-08-28 16:27:34 +03:00
WiseDev
0451fcdf43 grant achievement progress and roll exp into levels
two separate breakages behind "no achievement, no level up", both
checked against the client in IDA.

level up: LogicClientAvatar::xpGainHelper adds a gain to exp_points -
which is progress WITHIN the level, not a running total - then rolls it
into levels: while exp_points >= ExpToNextLevel(level) it subtracts that
threshold and increments the level, calling levelUp for the deck slots
and diamond reward. our add_exp only grew exp_points, so the bar filled
past full and the level never moved. it now rolls over the same way,
against the exp_levels table.

achievement: the client only offers a claim when progress >= ActionCount
(LogicClientAvatar::isAchievementCompleted reads commodity 2), and the
claim command re-reads that same commodity - and build_avatar populated
none of it, so every claim came back "not completed". build_avatar now
serves real progress: findcard from the count of distinct cards owned,
reacharena from the arena reached. donate / jointeam / watchtv need
social features we do not have and stay at zero.

tests cover both and fail without the fixes: 25 exp reaches level 2 with
5 carried, and a completed findcard tier claims once, grants exp, and is
rejected the second time.
2026-08-24 09:15:22 +03:00
WiseDev
8699a6884d answer the client when it asks for the sector state
RequestSectorStateMessage, 12903, one vint of client tick, sent through
sendUdpMessage and so arriving on tcp like everything else. we were
ignoring it.

it matters because of how the client builds models. the factory marks a
freshly created object at [obj+0x14], and only for those does
LogicGameObjectManager::decode call the listener at [mgr+0x28] that
builds the visual. every later snapshot matches the same object by
global id and reuses it, so the flag is never set again - an object that
was decoded before the battle screen installed its listener stays
invisible for the whole battle while still walking and fighting. that is
the tower archers and the invisible units; the knight shows because the
client creates that one itself, after the screen is up.

the client asks for the state when it is ready, and now it gets it.
2026-08-23 14:32:20 +03:00
WiseDev
498f2cb62d switch the battle to type 0 and drive it from the server
type 0 turns out not to need a UDP transport. MessageManager::sendUdpMessage
checks for a socket and a valid connection, and falls straight back to
sendMessage when there is neither - we never send UdpConnectionInfoMessage,
so the client has no socket and the sector traffic arrives on the tcp
connection we already have.

so SectorCommandMessage, 12904, is decoded now: a client tick, a client
checksum and an optional command. a card played this way goes through the
same summon path as before. the client stops sending EndClientTurnMessage
in a battle - sendEndTurn asserts on isImmediateMessageExecution - so the
checksum comparison moves onto the sector command, which carries the same
two numbers.

with the type at 0 the client no longer simulates. it renders what the
snapshot says, which is why the snapshots start flowing again: the gate
on them was the battle type all along. the bot's cards reach the player
for the first time, because there is finally one simulation rather than
two arguing.
2026-08-23 13:51:01 +03:00
WiseDev
ba235a9b4c matchmake two players before falling back to a bot
matchmaking now queues. the first player in waits, the second one to
arrive pairs with them, and both get the same battle: one shared session
keyed by both accounts, one snapshot, the client working out which side
it is from the account ids it already carries. if nobody turns up within
ten seconds the ticker polls the queue out and builds the bot battle
instead. cancelling or disconnecting leaves the queue.

also fixes the movement component tail: the extracted layout counts
"n + 18" vints including the path length itself, so seventeen follow the
path, not eighteen. the verifier read one too many and every snapshot
carrying a moving unit came apart after it - which is what the guard
caught and refused to send, rather than the client aborting on it.
2026-08-23 13:19:38 +03:00
WiseDev
2ea283cdda push messages to the client during a battle
the client never receives individual commands in a battle - SectorManager
has receiveSectorState, receiveCompressedSectorState and a heartbeat, and
nothing else. so everything the opponent does has to reach the player
inside server state, which means the server needs to be able to speak
first. it could not: the rpc only ever answered.

the gateway now runs a ticker for the length of a battle, calling a new
battle_tick on the service five times a second and writing whatever it
returns straight to the socket. the simulation stays in the service and
the socket stays in the gateway.

the first rider is emotes. the player's SendBattleEventMessage reaches
the service instead of being logged and dropped, and the bot answers with
a taunt of its own; it also sends one unprompted every twelve to thirty
seconds, drawn from the rows of taunts.csv that TauntMenu marks as
usable. the reply carries the opponent account so it renders on their
side of the arena.
2026-08-23 13:03:07 +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
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