Throwdowns: a grouped line score is no longer cut at its first separator

A Jam Session takes the score of the line just landed from the score HUD's
text, with the script native ParseInt, which is the C library's atoi. atoi
stops at the first character that is not a digit, so a line shown as 5,432
was read as 5 and added 5 to the player's total. Reported for Jam Sessions
and Spot Battles: lines of 5,000 and more adding one or two points.

ParseInt now reads groups of three digits whole, set apart by a comma, a
full stop, an apostrophe or a space: "5,432" is 5432. Text that is not a
grouped number is read exactly as before. The first few grouped scores are
logged, to confirm the cause in play.
This commit is contained in:
AyeZeeBB committed 2026-10-13 01:14:01 -05:00
1 parent 180cdc9c85
commit 30a320ff62
4 files changed
+103

No files matched your search

@@ -1,4 +1,5 @@
#include "native_throwdowns.h"
#include "grouped_number.h"
#include "throwdown_lab.h"
#include "throwdown_relay.h"
#include "virtual_player_names.h"
@@ -566,10 +567,36 @@ void apply_throwdown_strings(Address base) noexcept {
}
} catch (...) {}
}
// ParseInt, reading a grouped number whole (grouped_number.h). Any script may call it.
int (*parse_int_original)(const char* const*){};
int parse_int_hook(const char* const* text) {
const auto parsed = throwdowns_text::parse_grouped_int(text ? *text : nullptr);
if (parsed.grouped) {
static std::atomic<unsigned> logged{};
if (logged.fetch_add(1, std::memory_order_relaxed) < 12) {
std::string shown(*text, strnlen(*text, 32));
for (auto& ch : shown) if (static_cast<unsigned char>(ch) < 0x20) ch = ' ';
logging::log(logging::Level::info, logging::Channel::progression,
"Scripts: \"{}\" read as {} (the game's own ParseInt stops at the first separator).", shown, parsed.value);
}
}
return parsed.value;
}
void install_parse_int(Address base) noexcept {
try {
if (read<std::array<unsigned char, 10>>(base + throwdowns::parse_int) != throwdowns::parse_int_prefix)
throw std::runtime_error("ParseInt fingerprint differs; grouped scores are read as the game reads them");
auto* const target = reinterpret_cast<void*>(base + throwdowns::parse_int);
if (hook_prepare(target, reinterpret_cast<void*>(&parse_int_hook), reinterpret_cast<void**>(&parse_int_original)) != HookOk ||
hook_enable(target) != HookOk)
throw std::runtime_error("Cannot replace ParseInt; grouped scores are read as the game reads them");
} catch (const std::exception& e) { logging::write(logging::Level::warning, logging::Channel::progression, e.what()); }
}
void initialize_native_throwdowns(Address base) noexcept {
auto& s = state();
if (s.attempted || !base) return;
s.attempted = true; s.base = base;
install_parse_int(base);
struct Hook { Address rva; std::array<unsigned char, 19> prefix; void* replacement; void** original; };
const std::array hooks{
Hook{throwdowns::mode_available, throwdowns::mode_available_prefix, reinterpret_cast<void*>(&mode_available_hook), reinterpret_cast<void**>(&s.mode_available)},