Online high scores (WinINet protocol)¶
This is the classic 1.9.93 online leaderboard client logic. The implementation lives in two threads:
- High score submit/receive thread:
highscore_sync_worker(0x0042d0e0,decomp/1.9/crimsonland/crimsonland/highscore_sync_worker.cpp) (logs "beginthread () highscores thread.") - Version check thread:
statistics_update_check_worker(0x0042d8a0,decomp/1.9/crimsonland/crimsonland/statistics_update_check_worker.cpp) (logs "beginthread () (version check)")
The client uses WinINet (InternetOpenA, InternetConnectA, HttpOpenRequestA,
HttpSendRequestA, InternetReadFile) and sends/receives raw binary payloads.
Shared state globals¶
online_sync_status(0x004d11f0): cross-screen worker status (0idle,1..5progress/done,6failed).update_notice_url(0x004d11f4): URL staged by update-check worker.update_notice_pending(0x004d11f8): latch indicating update notice/open path is active.update_notice_open_requested(0x00480838): set when the in-UI "Get the update" button is pressed.highscore_batch_sync_mode(0x004d11f9): Shift-modified "update all / receive all" mode that accelerates worker delays for batch cycling.highscore_batch_sync_stage_index(0x004ccb58): stage/mode cursor used by batch sync to iterate quest/game-mode combinations.highscore_post_sync_update_check_latch(0x004d1218): one-shot guard for the follow-up version-check worker after sync completion.
HTTP endpoints¶
High scores:
- Host:
scores.crimsonland.com - Port:
80 - Method:
POST - Path:
/scoring_v2_7.php - HTTP version:
HTTP/1.1 - Referrer:
none - Accept types array includes:
image/gifimage/x-xbitmapimage/jpegimage/pjpegapplication/vnd.ms-powerpointapplication/vnd.ms-excelapplication/mswordapplication/x-cometapplication/octet-stream*/*- User agent:
Crimsonland - Username:
guest - Password: empty string
- Extra headers (exact string):
Content-Disposition: inline; filename="test"\r\nContent-type: application/octet-stream
Version check (statistics_update_check_worker):
- Host:
www.crimsonland.com - Port:
80 - Method:
POST - Path:
/ra_version.php - Same headers/accept list/user agent/guest user as above.
High score submit payload (client -> server)¶
The client allocates a 0x8000 buffer and builds a binary payload. The payload starts with a 10-byte header followed by a NUL-terminated player name, then a sequence of 0x40-byte score records.
Header layout (offsets are from start of payload):
0x00 u8 0x42
0x01 u8 0x48
0x02 u8 0xF3
0x03 u8 0x85
0x04 u8 name_slot_valid (1 if config_selected_saved_name_slot != 0 and full version; else 0)
0x05 u8 score_count (filled later; 0 in demo)
0x06 u8 mode_or_hardcore (config_hardcore ? 5 : config_game_mode)
0x07 u8 quest_stage_major
0x08 u8 quest_stage_minor
0x09 u8 config_player_count
0x0A .. selected name (NUL-terminated, from config_saved_name_0[config_selected_saved_name_slot])
Score records:
- Each submitted record is exactly
0x40bytes. -
Records are built via
highscore_record_pack_for_submit, which copies the base high score record fields (name + metadata) and zeroes bytes0x3c..0x3f. -
The client appends each record immediately after the name string.
- Total payload length =
0x0b + name_len + (score_count * 0x40).
Selection logic:
- Iterates
highscore_tableup tohighscore_table_count. -
Skips entries whose
flags(record offset0x44) have bit 0 set and bit 1 clear: bit 0 gates submission/update; bit 1 marks replacement records and bypasses that gate. -
Calls
highscore_submit_full_version_guard(illegal-score guard) per entry; on failure, logs "Detected a potential illegal score" and does not include that entry.
High score response payload (server -> client)¶
The client reads the HTTP response body and expects a binary payload:
0x00 u8 0x15 (magic)
0x01 u8 count_a
0x02 u8 count_b
0x03 .. records (count_a + count_b) * 0x44 bytes
- The client validates
payload_len - 3 == (count_a + count_b) * 0x44. - Each record is
0x44bytes (bytes0x00..0x43of the high score record). - Before parsing, the client prepares a reusable local record with the native
tail sentinels. For each
0x44-byte response record it then: - copies bytes
0x00..0x43from the response; - sets
record[0x44] = 1(flags); - sets
record[0x45] = 0x75in hardcore mode, otherwise0; - preserves
record[0x46] = 0x7c('|') andrecord[0x47] = 0xff. - Records are saved via
highscore_save_record(0x0043b450).
Note: The response record size (0x44) includes bytes 0x40..0x43 (date fields),
but the local save path overwrites date/checksum on write, so these bytes are
not critical for correctness.
Practical server notes¶
- The client expects raw binary bodies, not JSON or text.
- A mismatched record length is a hard receive failure. A missing
0x15magic byte is logged as invalid feedback, but the native worker still follows its success/status path after printing the response diagnostics. - The simplest compatible response is:
0x15,count_a,count_b, then0x44 * (count_a + count_b)bytes.- You can return zero records by sending three bytes:
0x15 0x00 0x00.
Client-side semantics¶
- Header byte
0x04is only computed and sent: it is1when saved-name slot other than 0 is selected (config_selected_saved_name_slot != 0) in the full version, otherwise0. Nothing else in the client depends on it. count_aandcount_bare only summed (for the length check) and logged as"<-- %d scores (%d+%d) received"; every received record is saved the same way, so the split carries no meaning on the client.- Any validation of the submitted
0x40-byte records is server-side; the client only filters withhighscore_submit_full_version_guardbefore sending.