159 lines
3.8 KiB
C++
159 lines
3.8 KiB
C++
|
// An reverse-engineered implementation of the PRNG used in the SSX games.
|
||
|
|
||
|
#include <cstdint>
|
||
|
#include <cstring>
|
||
|
|
||
|
// only bring this in for the test
|
||
|
#ifdef COMPILE_TEST
|
||
|
#include <cstdio>
|
||
|
#include <cassert>
|
||
|
#endif
|
||
|
|
||
|
struct bxPsuedoRng {
|
||
|
|
||
|
|
||
|
/**
|
||
|
* The initalization state.
|
||
|
* Weirdly, these constants seem to show up in tables
|
||
|
* to convert from milliseconds -> NTP fractional time.
|
||
|
*
|
||
|
* Interesting source for psuedorandom constants.
|
||
|
*/
|
||
|
constexpr static std::uint32_t InitState[6] {
|
||
|
0xf22d0e56,
|
||
|
0x883126e9,
|
||
|
0xc624dd2f,
|
||
|
0x702c49c,
|
||
|
0x9e353f7d,
|
||
|
0x6fdf3b64
|
||
|
};
|
||
|
|
||
|
|
||
|
constexpr explicit bxPsuedoRng() {
|
||
|
std::memcpy(this, &InitState[0], sizeof(*this));
|
||
|
}
|
||
|
|
||
|
constexpr explicit bxPsuedoRng(std::uint32_t seed) {
|
||
|
Seed(seed);
|
||
|
}
|
||
|
|
||
|
constexpr void Seed(std::uint32_t seed = 0) {
|
||
|
state[0] = seed + InitState[0];
|
||
|
state[1] = seed + InitState[1];
|
||
|
state[2] = seed + InitState[2];
|
||
|
state[3] = seed + InitState[3];
|
||
|
state[4] = seed + InitState[4];
|
||
|
state[5] = seed + InitState[5];
|
||
|
}
|
||
|
|
||
|
|
||
|
// These were implemented in the original code so I'm just doing it here to be nice
|
||
|
|
||
|
constexpr void CopyTo(bxPsuedoRng* dest) {
|
||
|
std::memcpy(dest, this, sizeof(*this));
|
||
|
}
|
||
|
|
||
|
constexpr void CopyFrom(bxPsuedoRng* src) {
|
||
|
std::memcpy(this, src, sizeof(*this));
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get a random number.
|
||
|
*/
|
||
|
constexpr std::uint32_t NextInt() {
|
||
|
std::uint32_t tempOutput = state[4] + state[5];
|
||
|
bool startCycleIf = (state[4] + state[5] < state[5]) || (state[4] + state[5] < state[4]);
|
||
|
|
||
|
// start cycle?
|
||
|
std::uint32_t prevState = state[3];
|
||
|
state[4] = tempOutput;
|
||
|
tempOutput += startCycleIf + prevState;
|
||
|
|
||
|
std::uint32_t prevState2 = state[2];
|
||
|
state[3] = tempOutput;
|
||
|
tempOutput += (tempOutput < prevState) + prevState2;
|
||
|
|
||
|
prevState = state[1];
|
||
|
state[2] = tempOutput;
|
||
|
tempOutput += (tempOutput < prevState2) + prevState;
|
||
|
|
||
|
state[1] = tempOutput;
|
||
|
tempOutput += state[0] + (tempOutput < prevState);
|
||
|
|
||
|
// Set output "register" to the output of all the cycles combined,
|
||
|
// and add 1 to the final "register".
|
||
|
state[0] = tempOutput;
|
||
|
|
||
|
|
||
|
// Make sure the entire state is nonzero, including the output
|
||
|
// "register". This really should be cleaned up :(
|
||
|
|
||
|
state[5]++;
|
||
|
if (state[5] + 1 == 0) {
|
||
|
auto* ptr = &state[4];
|
||
|
*ptr++;
|
||
|
if (*ptr == 0) {
|
||
|
ptr = &state[3];
|
||
|
*ptr++;
|
||
|
if (*ptr == 0) {
|
||
|
ptr = &state[2];
|
||
|
*ptr++;
|
||
|
if (*ptr == 0) {
|
||
|
ptr = &state[1];
|
||
|
*ptr++;
|
||
|
if (*ptr == 0) {
|
||
|
tempOutput++;
|
||
|
state[0] = tempOutput;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return state[0];
|
||
|
}
|
||
|
|
||
|
|
||
|
/**
|
||
|
* The PRNG state, which I call "registers" for simplicity.
|
||
|
*
|
||
|
* Things I know:
|
||
|
* [0] is the output register
|
||
|
*/
|
||
|
std::uint32_t state[6];
|
||
|
};
|
||
|
|
||
|
|
||
|
#ifdef COMPILE_TEST
|
||
|
// The following is a simple driver program
|
||
|
// which just cycles the PRNG 1000 times.
|
||
|
// I use this in a very simple test one-liner I ran while simplifiying/cleaning up the PRNG:
|
||
|
//
|
||
|
// g++ -std=c++20 -DCOMPILE_TEST bxprng_new.cpp -o bxprng; ./bxprng >prelim; diff working prelim
|
||
|
//
|
||
|
// The "working" file, attached to this Gist, is the output of simply getting the decompiled code
|
||
|
// to even compile. (since I assume that would work without introducing bugs). If you see no output
|
||
|
// (besides gcc warnings) from this command, the test passed (prelim file is exactly the same as "working").
|
||
|
|
||
|
|
||
|
int main() {
|
||
|
bxPsuedoRng rng;
|
||
|
|
||
|
// Run 1000 cycles, printing out results as we go.
|
||
|
for(int i = 0; i < 1000; ++i) {
|
||
|
auto next = rng.NextInt();
|
||
|
std::printf("cycle %d: 0x%08x (decimal: %d / %u)\n", i + 1, next, next, next);
|
||
|
}
|
||
|
|
||
|
// The state after 1000 cycles should always match this, since
|
||
|
// we don't seed the PRNG.
|
||
|
assert(rng.state[0] == 0x9213d468);
|
||
|
assert(rng.state[1] == 0x04dfa254);
|
||
|
assert(rng.state[2] == 0xddf4b3e5);
|
||
|
assert(rng.state[3] == 0x90ef376d);
|
||
|
assert(rng.state[4] == 0x9e3cdd49);
|
||
|
assert(rng.state[5] == 0x6fdf3f4c);
|
||
|
}
|
||
|
|
||
|
#endif // COMPILE_TEST
|