mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 12:57:01 +02:00
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:
1 parent
180cdc9c85
commit
30a320ff62
4 files changed
+103
No files matched your search
@@ -149,6 +149,10 @@ inline constexpr std::uintptr_t bool_and = 0x16e7130, bool_or = 0x16eb6e0, bool_
|
||||
inline constexpr std::array<unsigned char, 9> bool_and_prefix{0x80,0x39,0x00,0x74,0x0a,0x80,0x3a,0x00,0x74};
|
||||
inline constexpr std::array<unsigned char, 9> bool_or_prefix{0x80,0x39,0x00,0x75,0x0a,0x80,0x3a,0x00,0x75};
|
||||
inline constexpr std::array<unsigned char, 9> bool_not_prefix{0x80,0x39,0x00,0x0f,0x94,0xc0,0x88,0x02,0xc3};
|
||||
// ParseInt expression native (09586973): int (const char* const* text), which is
|
||||
// "mov rcx, [rcx]; jmp [atoi]". Its record's generic and fast entries both call this.
|
||||
inline constexpr std::uintptr_t parse_int = 0x864dc0;
|
||||
inline constexpr std::array<unsigned char, 10> parse_int_prefix{0x48,0x8b,0x09,0x48,0xff,0x25,0x7e,0x66,0x78,0x05};
|
||||
inline constexpr std::uintptr_t player_id = 0x854b90;
|
||||
inline constexpr std::array<unsigned char, 19> player_id_prefix{
|
||||
0x40,0x53,0x48,0x83,0xec,0x20,0x48,0x8b,0x11,0x48,0x8b,0xd9,0x48,0x85,0xd2,0x0f,0x84,0xb9,0x00};
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include "Extension/Throwdowns/throwdown_lab.h"
|
||||
#include "Extension/Throwdowns/throwdown_wire.h"
|
||||
#include "Extension/Throwdowns/native_throwdowns.h"
|
||||
#include "Extension/Throwdowns/grouped_number.h"
|
||||
#include "Extension/Multiplayer/Hud/native_party.h"
|
||||
#include "Extension/Settings/named_settings.h"
|
||||
#include <Windows.h>
|
||||
@@ -780,12 +781,33 @@ void level_change_flow() {
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// A score read from text: what atoi reads, and digit groups read whole.
|
||||
void grouped_number_checks() {
|
||||
using dingosdk::throwdowns_text::parse_grouped_int;
|
||||
const auto same = [](const char *text, int value, bool grouped) {
|
||||
const auto parsed = parse_grouped_int(text);
|
||||
return parsed.value == value && parsed.grouped == grouped;
|
||||
};
|
||||
if (!(same("5432", 5432, false) && same("0", 0, false) && same(" -17", -17, false) && same("+9", 9, false) &&
|
||||
same("", 0, false) && same("abc", 0, false) && same("12abc", 12, false) && same(nullptr, 0, false)))
|
||||
throw std::runtime_error("A plain number was not read as atoi reads it");
|
||||
if (!(same("5,432", 5432, true) && same("1,250", 1250, true) && same("12,345,678", 12345678, true) &&
|
||||
same("5.432", 5432, true) && same("5 432", 5432, true) && same("5'432", 5432, true) &&
|
||||
same("5\xC2\xA0" "432", 5432, true) && same("5\xE2\x80\xAF" "432", 5432, true) && same("-1,000 pts", -1000, true)))
|
||||
throw std::runtime_error("A grouped score was cut short");
|
||||
if (!(same("12.5", 12, false) && same("5,43", 5, false) && same("5,4321", 5, false) && same("1234,567", 1234, false) &&
|
||||
same("5, 432", 5, false) && same("5,,432", 5, false) && same("1,234.5", 1234, true)))
|
||||
throw std::runtime_error("Text that is not a grouped number was read as one");
|
||||
if (!(same("99999999999", INT_MAX, false) && same("-99999999999", INT_MIN, false) && same("999,999,999,999", INT_MAX, true)))
|
||||
throw std::runtime_error("A number out of range did not saturate");
|
||||
}
|
||||
int main() {
|
||||
try {
|
||||
guest_flow();
|
||||
leader_flow();
|
||||
removal_flow();
|
||||
spot_battle_flow();
|
||||
grouped_number_checks();
|
||||
skate_flow();
|
||||
alone_flow();
|
||||
challenge_flow();
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
#pragma once
|
||||
#include <climits>
|
||||
#include <cstddef>
|
||||
|
||||
// The game's scripts read a number out of text with ParseInt, which is the C library's atoi:
|
||||
// it stops at the first character that is not a digit. A Jam Session takes the score of the
|
||||
// line just landed from the score HUD's own label (analysis/throwdowns-re/jam.md), so where
|
||||
// that label groups its digits a 5,432 line is read as 5 and adds 5 to the player's total
|
||||
// (a 1,250 one adds 1). This reads the same text and takes the groups as one number.
|
||||
namespace dingosdk::throwdowns_text {
|
||||
struct GroupedInt {
|
||||
int value{};
|
||||
bool grouped{}; // the text had digit groups, which atoi would have cut short
|
||||
};
|
||||
|
||||
// atoi's reading of `text` (leading space, a sign, digits), going on through groups of
|
||||
// exactly three digits set apart by one of , . ' a space, or a no-break, narrow no-break or
|
||||
// thin space (UTF-8). "5,432" is 5432 and "1 234 567" is 1234567; "12.5", "5,43" and
|
||||
// "1234,567" end where atoi ends them. Out of range saturates.
|
||||
inline GroupedInt parse_grouped_int(const char *text) noexcept {
|
||||
if (!text) return {};
|
||||
const auto *p = reinterpret_cast<const unsigned char *>(text);
|
||||
const auto digit = [](unsigned char c) { return c >= '0' && c <= '9'; };
|
||||
while (*p == ' ' || (*p >= '\t' && *p <= '\r')) ++p;
|
||||
bool negative{};
|
||||
if (*p == '+' || *p == '-') negative = *p++ == '-';
|
||||
constexpr long long limit = static_cast<long long>(INT_MAX) + 1;
|
||||
long long value{};
|
||||
int first{};
|
||||
for (; digit(*p); ++p, ++first) value = value * 10 + (*p - '0') > limit ? limit : value * 10 + (*p - '0');
|
||||
GroupedInt result;
|
||||
// Only a number that starts with one to three digits is grouped.
|
||||
while (first >= 1 && first <= 3) {
|
||||
std::size_t separator{};
|
||||
if (*p == ',' || *p == '.' || *p == ' ' || *p == '\'') separator = 1;
|
||||
else if (p[0] == 0xC2 && p[1] == 0xA0) separator = 2;
|
||||
else if (p[0] == 0xE2 && p[1] == 0x80 && (p[2] == 0xAF || p[2] == 0x89)) separator = 3;
|
||||
if (!separator) break;
|
||||
const auto *group = p + separator;
|
||||
if (!digit(group[0]) || !digit(group[1]) || !digit(group[2]) || digit(group[3])) break;
|
||||
const auto thousands = (group[0] - '0') * 100 + (group[1] - '0') * 10 + (group[2] - '0');
|
||||
value = value * 1000 + thousands > limit ? limit : value * 1000 + thousands;
|
||||
p = group + 3;
|
||||
result.grouped = true;
|
||||
}
|
||||
if (negative) result.value = value >= limit ? INT_MIN : -static_cast<int>(value);
|
||||
else result.value = value >= limit ? INT_MAX : static_cast<int>(value);
|
||||
return result;
|
||||
}
|
||||
} // namespace dingosdk::throwdowns_text
|
||||
@@ -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)},
|
||||
|
||||
Reference in new issue
Block a user