Skip to content

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 (0 idle, 1..5 progress/done, 6 failed).
  • 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/gif
  • image/x-xbitmap
  • image/jpeg
  • image/pjpeg
  • application/vnd.ms-powerpoint
  • application/vnd.ms-excel
  • application/msword
  • application/x-comet
  • application/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 0x40 bytes.
  • Records are built via highscore_record_pack_for_submit, which copies the base high score record fields (name + metadata) and zeroes bytes 0x3c..0x3f.

  • The client appends each record immediately after the name string.

  • Total payload length = 0x0b + name_len + (score_count * 0x40).

Selection logic:

  • Iterates highscore_table up to highscore_table_count.
  • Skips entries whose flags (record offset 0x44) 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 0x44 bytes (bytes 0x00..0x43 of 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..0x43 from the response;
  • sets record[0x44] = 1 (flags);
  • sets record[0x45] = 0x75 in hardcore mode, otherwise 0;
  • preserves record[0x46] = 0x7c ('|') and record[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 0x15 magic 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, then 0x44 * (count_a + count_b) bytes.
  • You can return zero records by sending three bytes: 0x15 0x00 0x00.

Client-side semantics

  • Header byte 0x04 is only computed and sent: it is 1 when saved-name slot other than 0 is selected (config_selected_saved_name_slot != 0) in the full version, otherwise 0. Nothing else in the client depends on it.
  • count_a and count_b are 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 with highscore_submit_full_version_guard before sending.