diff --git a/CMakeLists.txt b/CMakeLists.txt index 49d7904..c87e15b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b218723 --- /dev/null +++ b/README.md @@ -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 + diff --git a/src/ansi.hpp b/src/ansi.hpp new file mode 100644 index 0000000..7d15719 --- /dev/null +++ b/src/ansi.hpp @@ -0,0 +1,28 @@ +#include +#include +#include + +// 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 diff --git a/src/fprint.hpp b/src/fprint.hpp new file mode 100644 index 0000000..1a24b66 --- /dev/null +++ b/src/fprint.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include +#include + +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 + 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 diff --git a/src/main.cpp b/src/main.cpp index 0bedce7..ba56558 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -4,212 +4,37 @@ #include #include +#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 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; } diff --git a/src/scramble.hpp b/src/scramble.hpp new file mode 100644 index 0000000..4a3736c --- /dev/null +++ b/src/scramble.hpp @@ -0,0 +1,25 @@ +#include +#include +#include +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 diff --git a/src/worker.cpp b/src/worker.cpp new file mode 100644 index 0000000..f077fd6 --- /dev/null +++ b/src/worker.cpp @@ -0,0 +1,109 @@ +#include "worker.hpp" + +#include +#include + +#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 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 diff --git a/src/worker.hpp b/src/worker.hpp new file mode 100644 index 0000000..4ac765a --- /dev/null +++ b/src/worker.hpp @@ -0,0 +1,47 @@ +#pragma once +#include +#include + +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