move worker code into new source file

This commit is contained in:
Lily Tsuru 2023-08-22 04:30:34 -04:00
parent a0704ef240
commit 8e657dbdc6
8 changed files with 295 additions and 189 deletions

View File

@ -26,6 +26,7 @@ project_set_alternate_linker()
add_executable(swsf_bruteforce
src/asio_impl.cpp
src/main.cpp
src/worker.cpp
)
target_compile_definitions(swsf_bruteforce PRIVATE

36
README.md Normal file
View File

@ -0,0 +1,36 @@
# swsf_bruteforce
A performant, multithreaded, brute-forcer for scrambled cheat codes in Star Wars: Starfighter (JP) and Star Wars: Jedi Starfighter, written in modern C++20.
# Building
You need:
- A C++20 compiler (MSVC 2022, Clang, GCC)
- Boost (I tested with 1.81, should work fine with 1.82 onwards)
- CMake
## Windows
```
cmake -Bbuild -GNinja -DCMAKE_BUILD_TYPE=Release
cmake --build build
```
## Linux
```
$ cmake -Bbuild -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS="-march=native -mtune=native"
$ cmake --build build
`
# Usage
`./swsf_bruteforce -s [START_LENGTH] -l [LENGTH] -e 0xABCDABCD`
where:
`-s [START_LENGTH]` is the guessed start bound (greater than 2, less than end bound)
`-l [LENGTH]` is the guessed end bound (less than or equal to 8)
`0xABCDABCD` is the hash to use

28
src/ansi.hpp Normal file
View File

@ -0,0 +1,28 @@
#include <cstdint>
#include <format>
#include <string_view>
// see https://bisqwit.iki.fi/jutut/kuvat/programming_examples/cpp_thread_tutorial/ver05.cc
// this version of the code doesn't even need to worry about synchronization
namespace swbf::ansi {
inline std::uint32_t ln = 1;
inline auto Color(int n) {
return std::format("\33[38;5;{}m", n);
}
inline std::string_view Reset() {
return "\33[m";
}
inline auto Line(int l) {
int m = l - ln;
ln = l;
return "\r" + (m < 0 ? "\33[" + std::to_string(-m) + 'A' : std::string(m, '\n'));
}
inline std::string_view ResetLine() {
return "\33[2K";
}
} // namespace swbf::ansi

35
src/fprint.hpp Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include <iterator>
#include <cstdint>
#include <cstdio>
#include <format>
namespace swbf {
struct FputcIterator {
using iterator_category = std::output_iterator_tag;
using value_type = void;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = void;
FputcIterator(std::FILE* file) : file(file) {}
FputcIterator& operator*() { return *this; }
FputcIterator& operator++() { return *this; }
FputcIterator& operator++(int) { return *this; }
FputcIterator& operator=(const char& val) {
std::fputc(val, file);
return *this;
}
private:
std::FILE* file;
};
/// Poor Man's C++23
template<class ...Args>
inline void fprint(std::FILE* file, std::string_view format, Args&&... args) {
std::vformat_to(FputcIterator(file), format, std::make_format_args(args...));
}
} // namespace swbf

View File

@ -4,212 +4,37 @@
#include <format>
#include <thread>
#include "ansi.hpp"
#include "fprint.hpp"
#include "worker.hpp"
namespace asio = boost::asio;
constexpr static auto MAX_CODE_LENGTH = 8;
constexpr static auto THREAD_COUNT = 26; // this is a-z
void BruteMain() {
asio::thread_pool pool(swbf::THREAD_COUNT);
/// This is a constexpr-safe zero-allocation version of the algorithm
/// the "Scramble" ConsoleScript hash algorithm.
constexpr std::uint32_t SwsfScramble(std::string_view code) {
std::uint32_t tally {};
auto cycle = [&tally](char c) { tally = c + (tally * 5); };
cycle('c');
cycle('o');
cycle('d');
cycle('e');
cycle('_');
for(auto* p = code.data(); *p; ++p)
cycle(tolower(*p));
return tally;
}
struct FputcIterator {
using iterator_category = std::output_iterator_tag;
using value_type = void;
using difference_type = std::ptrdiff_t;
using pointer = void;
using reference = void;
FputcIterator(std::FILE* file) : file(file) {}
FputcIterator& operator*() { return *this; }
FputcIterator& operator++() { return *this; }
FputcIterator& operator++(int) { return *this; }
FputcIterator& operator=(const char& val) {
fputc(val, file);
return *this;
}
private:
std::FILE* file;
};
bool NextCode(std::string& buffer, size_t start) {
size_t len = buffer.length();
for(size_t i = len - 1; i >= start; --i) {
char c = buffer[i];
if(c < 'z') {
++buffer[i];
return true;
}
buffer[i] = 'a';
}
return false;
}
// see https://bisqwit.iki.fi/jutut/kuvat/programming_examples/cpp_thread_tutorial/ver05.cc
// this version of the code doesn't even need to worry about synchronization
namespace {
unsigned ln = 1;
auto Color(int n) {
return std::format("\33[38;5;{}m", n);
}
std::string_view Reset() {
return "\33[m";
}
auto Line(int l) {
int m = l - ln;
ln = l;
return "\r" + (m < 0 ? "\33[" + std::to_string(-m) + 'A' : std::string(m, '\n'));
}
std::string_view ResetLine() {
return "\33[2K";
}
} // namespace
/// This class implements the display of progress
struct ThreadInfoData {
ThreadInfoData() { codeString.resize(MAX_CODE_LENGTH); }
std::uint32_t threadIndex;
std::string codeString;
std::uint32_t codeHash;
bool done = false;
void DisplayProgress() {
if(!done)
std::format_to(FputcIterator(stdout), "{}Thread {:2}: Trying code {}{} {}({:08x}){}", Line(threadIndex), threadIndex, Color(172),
codeString, Color(166), codeHash, Reset());
else {
std::format_to(FputcIterator(stdout), "{}{}Thread {:2}: {}Finished!{}", Line(threadIndex), ResetLine(), threadIndex, Color(172), Reset());
}
std::fflush(stdout);
}
};
std::vector<ThreadInfoData> infoData(THREAD_COUNT);
/// This class actually implements the brunt of the logic
struct BruteforceWorker {
/// Options for the worker.
struct Options {
/// The target hash to find matches for.
std::uint32_t targetHash;
/// If `startLength` should be treated as the code length.
bool exact;
std::uint32_t startLength;
std::uint32_t endLength;
};
constexpr BruteforceWorker(unsigned tid, const Options& options) : options(options), threadIndex(tid) {}
/// Public-facing driver function - the thread pool runs this.
void Bruteforce(char prefix) {
if(options.exact) {
BruteforceForLength(prefix, options.startLength);
} else {
for(std::uint32_t i = options.startLength; i <= options.endLength; ++i)
BruteforceForLength(prefix, i);
}
DisplayData().done = true;
}
private:
void BruteforceForLength(char prefix, std::uint32_t length) {
DisplayData().codeString.resize(length);
test_buffer.resize(length, 'a');
test_buffer[0] = prefix;
// test all possible combinations
while(true) {
hash = SwsfScramble(test_buffer);
// There was a match!
if(hash == options.targetHash)
std::format_to(FputcIterator(stderr), "match: {} ({:08x})\n", test_buffer, hash);
CopyDisplayData();
if(!NextCode(test_buffer, 1))
break;
}
}
void CopyDisplayData() {
memcpy(DisplayData().codeString.data(), test_buffer.data(), test_buffer.length());
DisplayData().codeHash = hash;
}
constexpr ThreadInfoData& DisplayData() { return infoData[threadIndex]; }
Options options;
unsigned threadIndex;
std::string test_buffer;
std::uint32_t hash;
};
int main() {
asio::io_context ioc;
asio::thread_pool pool(THREAD_COUNT);
swbf::BruteforceWorker::Options options { .targetHash = 0x2c73af55, .exact = true, .startLength = 8 };
auto threadIndex = 0u;
BruteforceWorker::Options options {
.targetHash = 0x2c73af55,
.exact = true,
.startLength = 8
};
// post worker threads to run onto the thread pool & wait for them to complete
for(int i = 0; i < THREAD_COUNT; ++i)
for(int i = 0; i < swbf::THREAD_COUNT; ++i)
asio::post(pool, [&, tid = threadIndex++]() {
BruteforceWorker state(tid, options);
infoData[tid].threadIndex = tid;
swbf::BruteforceWorker state(tid, options);
state.Bruteforce('a' + tid);
});
bool done = false;
while(!done) {
int doneIndex = 0;
for(auto& d : infoData) {
if(d.done)
doneIndex++;
d.DisplayProgress();
}
if(doneIndex == THREAD_COUNT)
done = true;
done = swbf::DisplayThreadInfo();
std::this_thread::sleep_for(std::chrono::milliseconds(66));
}
pool.join(); // just in case!
}
int main() {
BruteMain();
return 0;
}

25
src/scramble.hpp Normal file
View File

@ -0,0 +1,25 @@
#include <cctype>
#include <cstdint>
#include <string_view>
namespace swbf {
/// This is a constexpr-safe zero-allocation version of the algorithm
/// the "Scramble" ConsoleScript hash uses.
constexpr std::uint32_t SwsfScramble(std::string_view code) {
std::uint32_t tally {};
auto cycle = [&tally](char c) { tally = c + (tally * 5); };
// "code_" is prepended and cycled to the input string
cycle('c');
cycle('o');
cycle('d');
cycle('e');
cycle('_');
// the input string is also made lowercase
for(auto c : code)
cycle(std::tolower(c));
return tally;
}
} // namespace swbf

109
src/worker.cpp Normal file
View File

@ -0,0 +1,109 @@
#include "worker.hpp"
#include <cstring>
#include <vector>
#include "ansi.hpp"
#include "fprint.hpp"
#include "scramble.hpp"
namespace swbf {
/// This class implements the display of progress
struct ThreadInfoData {
ThreadInfoData() { codeString.resize(MAX_CODE_LENGTH); }
std::uint32_t threadIndex;
std::string codeString;
std::uint32_t codeHash;
bool done = false;
void DisplayProgress() {
if(!done)
swbf::fprint(stdout, "{}Thread {:2}: Trying code {}{} {}({:08x}){}", swbf::ansi::Line(threadIndex), threadIndex,
swbf::ansi::Color(172), codeString, swbf::ansi::Color(166), codeHash, swbf::ansi::Reset());
else {
swbf::fprint(stdout, "{}{}Thread {:2}: {}Finished!{}", swbf::ansi::Line(threadIndex), swbf::ansi::ResetLine(), threadIndex,
swbf::ansi::Color(172), swbf::ansi::Reset());
}
std::fflush(stdout);
}
};
namespace {
std::vector<ThreadInfoData> infoData(THREAD_COUNT);
bool NextCode(std::string& buffer, size_t start) {
size_t len = buffer.length();
for(size_t i = len - 1; i >= start; --i) {
char c = buffer[i];
if(c < 'z') {
++buffer[i];
return true;
}
buffer[i] = 'a';
}
return false;
}
} // namespace
bool DisplayThreadInfo() {
int doneIndex = 0;
for(auto& d : infoData) {
if(d.done)
doneIndex++;
d.DisplayProgress();
}
if(doneIndex == THREAD_COUNT)
return true;
else
return false;
}
BruteforceWorker::BruteforceWorker(unsigned tid, const Options& options) : options(options), threadIndex(tid) {
DisplayData().threadIndex = tid;
}
/// Public-facing driver function - the thread pool runs this.
void BruteforceWorker::Bruteforce(char prefix) {
if(options.exact) {
BruteforceForLength(prefix, options.startLength);
} else {
for(std::uint32_t i = options.startLength; i <= options.endLength; ++i)
BruteforceForLength(prefix, i);
}
DisplayData().done = true;
}
void BruteforceWorker::BruteforceForLength(char prefix, std::uint32_t length) {
DisplayData().codeString.resize(length);
test_buffer.resize(length, 'a');
test_buffer[0] = prefix;
// test all possible combinations
while(true) {
hash = swbf::SwsfScramble(test_buffer);
// There was a match!
if(hash == options.targetHash)
swbf::fprint(stderr, "match: {} ({:08x})\n", test_buffer, hash);
CopyDisplayData();
if(!NextCode(test_buffer, 1))
break;
}
}
void BruteforceWorker::CopyDisplayData() {
std::memcpy(DisplayData().codeString.data(), test_buffer.data(), test_buffer.length());
DisplayData().codeHash = hash;
}
ThreadInfoData& BruteforceWorker::DisplayData() {
return infoData[threadIndex];
}
} // namespace swbf

47
src/worker.hpp Normal file
View File

@ -0,0 +1,47 @@
#pragma once
#include <cstdint>
#include <string>
namespace swbf {
constexpr static auto MAX_CODE_LENGTH = 8;
constexpr static auto THREAD_COUNT = 26; // this is a-z
/// private structure
struct ThreadInfoData;
/// returns true when all threads finished
bool DisplayThreadInfo();
/// Per-thread worker for brute-forcing
struct BruteforceWorker {
/// Options for the worker.
struct Options {
/// The target hash to find matches for.
std::uint32_t targetHash;
/// If `startLength` should be treated as the code length.
bool exact;
std::uint32_t startLength;
std::uint32_t endLength;
};
BruteforceWorker(unsigned tid, const Options& options);
/// Public-facing driver function - the thread pool runs this.
void Bruteforce(char prefix);
private:
void BruteforceForLength(char prefix, std::uint32_t length);
void CopyDisplayData();
ThreadInfoData& DisplayData();
Options options;
unsigned threadIndex;
std::string test_buffer;
std::uint32_t hash;
};
} // namespace swbf