From 07612e0084200f07e67ab03da237a2d4cf994866 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 17:43:54 -0700 Subject: [PATCH 01/32] Replace png++ library with minimal libpng wrapper png++ 0.2.9 is unmaintained (2015) and broken with modern libc++. Since we only used it for simple "create image, set pixels, write PNG" operations, replace it with a ~150 line header-only wrapper around libpng directly. This removes the libs/png++-0.2.9 dependency. --- src/bridge.cpp | 26 ++----- src/fnt_test.cpp | 10 +-- src/gaf_test.cpp | 37 +++------- src/pcx_test.cpp | 16 ++-- src/rwe/util/png_write.h | 153 +++++++++++++++++++++++++++++++++++++++ src/texture_test.cpp | 14 ++-- src/tnt_test.cpp | 43 ++++------- 7 files changed, 201 insertions(+), 98 deletions(-) create mode 100644 src/rwe/util/png_write.h diff --git a/src/bridge.cpp b/src/bridge.cpp index 367732f55..74a1ad82c 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include #include @@ -113,18 +113,7 @@ void writeGetModeListSuccess(const std::vector>& modes) namespace rwe { - void loadPalette(std::istream& in, png::rgb_pixel* buffer) - { - for (unsigned int i = 0; i < 256; ++i) - { - in.read(reinterpret_cast(&(buffer[i].red)), 1); - in.read(reinterpret_cast(&(buffer[i].green)), 1); - in.read(reinterpret_cast(&(buffer[i].blue)), 1); - in.seekg(1, std::ios::cur); // skip alpha - } - } - - void extractMinimap(CompositeVirtualFileSystem& vfs, const png::rgb_pixel* palette, const std::string& source, const std::string& mapName, const std::string& outputPath) + void extractMinimap(CompositeVirtualFileSystem& vfs, const RgbPixel* palette, const std::string& source, const std::string& mapName, const std::string& outputPath) { auto tntData = vfs.readFileFromSource(source, "maps/" + mapName + ".tnt"); @@ -137,15 +126,14 @@ namespace rwe TntArchive tnt(&tntStream); auto minimap = tnt.readMinimap(); - png::image image(minimap.width, minimap.height); - for (png::uint_32 y = 0; y < image.get_height(); ++y) + PngImage image(minimap.width, minimap.height); + for (uint32_t y = 0; y < minimap.height; ++y) { - for (png::uint_32 x = 0; x < image.get_width(); ++x) + for (uint32_t x = 0; x < minimap.width; ++x) { auto b = static_cast(minimap.data[(y * minimap.width) + x]); assert(b >= 0 && b < 256); - auto px = palette[b]; - image[y][x] = px; + image.at(x, y) = palette[b]; } } @@ -173,7 +161,7 @@ std::optional getMinimap(rwe::CompositeVirtualFileSystem& vfs, cons } boost::interprocess::bufferstream paletteBuffer(paletteBytes->data(), paletteBytes->size()); - png::rgb_pixel palette[256]; + rwe::RgbPixel palette[256]; rwe::loadPalette(paletteBuffer, palette); auto output = fs::temp_directory_path(); diff --git a/src/fnt_test.cpp b/src/fnt_test.cpp index c86d9f63d..fbc940425 100644 --- a/src/fnt_test.cpp +++ b/src/fnt_test.cpp @@ -2,9 +2,9 @@ #include #include #include -#include #include #include +#include #include @@ -14,7 +14,7 @@ void renderFontFile(std::istream& in, std::ostream& out) std::vector v(512); - png::image image(256, 512); + rwe::PngImage image(256, 512); for (unsigned int i = 0; i < 256; ++i) { auto charX = (i % 16) * 16; @@ -34,12 +34,12 @@ void renderFontFile(std::istream& in, std::ostream& out) } auto val = (static_cast(v[j / 8u]) >> (7u - (j % 8u))) & 1u; - auto pxVal = val ? 255 : 0; - image[charY + dy][charX + dx] = png::rgb_pixel(pxVal, pxVal, pxVal); + auto pxVal = static_cast(val ? 255 : 0); + image.at(charX + dx, charY + dy) = rwe::RgbPixel{pxVal, pxVal, pxVal}; } } - image.write_stream(out); + image.writeStream(out); } int main(int argc, char* argv[]) diff --git a/src/gaf_test.cpp b/src/gaf_test.cpp index 214f0b921..da5f9bdeb 100644 --- a/src/gaf_test.cpp +++ b/src/gaf_test.cpp @@ -3,26 +3,12 @@ #include #include #include -#include #include +#include #include namespace fs = boost::filesystem; -void loadPalette(const std::string& filename, png::rgb_pixel* buffer) -{ - std::ifstream in(filename, std::ios::binary); - - for (unsigned int i = 0; i < 256; ++i) - { - in.read(reinterpret_cast(&(buffer[i].red)), 1); - in.read(reinterpret_cast(&(buffer[i].green)), 1); - in.read(reinterpret_cast(&(buffer[i].blue)), 1); - in.seekg(1, std::ios::cur); // skip alpha - } -} - - int listCommand(const std::string& filename) { std::cout << "GAF archive: " << filename << std::endl; @@ -95,14 +81,14 @@ class InspectAdapter : public rwe::GafReaderAdapter class GafAdapter : public rwe::GafReaderAdapter { private: - png::rgb_pixel* palette; + rwe::RgbPixel* palette; std::size_t frameCount; std::unique_ptr currentFrame; rwe::GafFrameData currentFrameHeader; fs::path destPath; public: - explicit GafAdapter(png::rgb_pixel* palette, const std::string& destPath) : palette(palette), frameCount(0), currentFrame(), destPath(destPath) {} + explicit GafAdapter(rwe::RgbPixel* palette, const std::string& destPath) : palette(palette), frameCount(0), currentFrame(), destPath(destPath) {} void beginFrame(const rwe::GafFrameEntry& entry, const rwe::GafFrameData& header) override { std::cout << "Beginning frame " << frameCount << std::endl; @@ -141,15 +127,14 @@ class GafAdapter : public rwe::GafReaderAdapter void endFrame() override { - png::image image(currentFrameHeader.width, currentFrameHeader.height); - for (png::uint_32 y = 0; y < image.get_height(); ++y) + rwe::PngImage image(currentFrameHeader.width, currentFrameHeader.height); + for (uint32_t y = 0; y < currentFrameHeader.height; ++y) { - for (png::uint_32 x = 0; x < image.get_width(); ++x) + for (uint32_t x = 0; x < currentFrameHeader.width; ++x) { auto b = static_cast(currentFrame[(y * currentFrameHeader.width) + x]); assert(b >= 0 && b < 256); - auto px = palette[b]; - image[y][x] = px; + image.at(x, y) = palette[b]; } } @@ -165,8 +150,8 @@ int extractAllCommand(const std::string& palettePath, const std::string& gafPath { std::cout << "Palette file: " << palettePath << std::endl; - png::rgb_pixel palette[256]; - loadPalette(palettePath, palette); + rwe::RgbPixel palette[256]; + rwe::loadPalette(palettePath, palette); std::cout << "GAF archive: " << gafPath << std::endl; std::ifstream file(gafPath, std::ios::binary); @@ -201,8 +186,8 @@ int extractCommand(const std::string& palettePath, const std::string& gafPath, c { std::cout << "Palette file: " << palettePath << std::endl; - png::rgb_pixel palette[256]; - loadPalette(palettePath, palette); + rwe::RgbPixel palette[256]; + rwe::loadPalette(palettePath, palette); std::cout << "GAF archive: " << gafPath << std::endl; std::ifstream file(gafPath, std::ios::binary); diff --git a/src/pcx_test.cpp b/src/pcx_test.cpp index e4cdc78a9..ef1bf9922 100644 --- a/src/pcx_test.cpp +++ b/src/pcx_test.cpp @@ -1,14 +1,8 @@ #include -#include #include +#include #include -class PaletteReadingException : public std::runtime_error -{ -public: - explicit PaletteReadingException(const char* message) : runtime_error(message) {} -}; - int convert(const std::string& inFile, const std::string& outFile) { std::ifstream in(inFile, std::ios::binary | std::ios::ate); @@ -28,14 +22,14 @@ int convert(const std::string& inFile, const std::string& outFile) auto width = decoder.getWidth(); auto height = decoder.getHeight(); - png::image image(width, height); - for (png::uint_32 y = 0; y < height; ++y) + rwe::PngImage image(width, height); + for (uint32_t y = 0; y < height; ++y) { - for (png::uint_32 x = 0; x < width; ++x) + for (uint32_t x = 0; x < width; ++x) { auto b = static_cast(decodedData[(y * width) + x]); auto px = palette[b]; - image[y][x] = png::rgb_pixel(px.red, px.green, px.blue); + image.at(x, y) = rwe::RgbPixel{px.red, px.green, px.blue}; } } diff --git a/src/rwe/util/png_write.h b/src/rwe/util/png_write.h new file mode 100644 index 000000000..0d250c68f --- /dev/null +++ b/src/rwe/util/png_write.h @@ -0,0 +1,153 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +namespace rwe +{ + struct RgbPixel + { + uint8_t r; + uint8_t g; + uint8_t b; + }; + + class PngImage + { + public: + uint32_t width; + uint32_t height; + std::vector pixels; + + PngImage(uint32_t width, uint32_t height) + : width(width), height(height), pixels(width * height) + { + } + + RgbPixel& at(uint32_t x, uint32_t y) + { + return pixels[y * width + x]; + } + + void write(const std::string& filename) const + { + FILE* fp = fopen(filename.c_str(), "wb"); + if (!fp) + { + throw std::runtime_error("Failed to open file for writing: " + filename); + } + + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png) + { + fclose(fp); + throw std::runtime_error("Failed to create PNG write struct"); + } + + png_infop info = png_create_info_struct(png); + if (!info) + { + png_destroy_write_struct(&png, nullptr); + fclose(fp); + throw std::runtime_error("Failed to create PNG info struct"); + } + + if (setjmp(png_jmpbuf(png))) + { + png_destroy_write_struct(&png, &info); + fclose(fp); + throw std::runtime_error("Error during PNG write"); + } + + png_init_io(png, fp); + writeRows(png, info); + png_destroy_write_struct(&png, &info); + fclose(fp); + } + + void writeStream(std::ostream& out) const + { + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); + if (!png) + { + throw std::runtime_error("Failed to create PNG write struct"); + } + + png_infop info = png_create_info_struct(png); + if (!info) + { + png_destroy_write_struct(&png, nullptr); + throw std::runtime_error("Failed to create PNG info struct"); + } + + if (setjmp(png_jmpbuf(png))) + { + png_destroy_write_struct(&png, &info); + throw std::runtime_error("Error during PNG write"); + } + + png_set_write_fn( + png, + &out, + [](png_structp pngPtr, png_bytep data, png_size_t length) { + auto* stream = static_cast(png_get_io_ptr(pngPtr)); + stream->write(reinterpret_cast(data), length); + }, + nullptr); + + writeRows(png, info); + png_destroy_write_struct(&png, &info); + } + + private: + void writeRows(png_structp png, png_infop info) const + { + png_set_IHDR( + png, + info, + width, + height, + 8, + PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, + PNG_FILTER_TYPE_DEFAULT); + + png_write_info(png, info); + + for (uint32_t y = 0; y < height; ++y) + { + png_write_row(png, reinterpret_cast(&pixels[y * width])); + } + + png_write_end(png, nullptr); + } + }; + + inline void loadPalette(const std::string& filename, RgbPixel* buffer) + { + std::ifstream in(filename, std::ios::binary); + for (unsigned int i = 0; i < 256; ++i) + { + in.read(reinterpret_cast(&(buffer[i].r)), 1); + in.read(reinterpret_cast(&(buffer[i].g)), 1); + in.read(reinterpret_cast(&(buffer[i].b)), 1); + in.seekg(1, std::ios::cur); // skip alpha + } + } + + inline void loadPalette(std::istream& in, RgbPixel* buffer) + { + for (unsigned int i = 0; i < 256; ++i) + { + in.read(reinterpret_cast(&(buffer[i].r)), 1); + in.read(reinterpret_cast(&(buffer[i].g)), 1); + in.read(reinterpret_cast(&(buffer[i].b)), 1); + in.seekg(1, std::ios::cur); // skip alpha + } + } +} diff --git a/src/texture_test.cpp b/src/texture_test.cpp index 71cf052c6..5b957df26 100644 --- a/src/texture_test.cpp +++ b/src/texture_test.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include #include #include #include @@ -163,15 +163,15 @@ namespace rwe void dumpImage(const Grid& g, const std::string& outFile) { - auto width = static_cast(g.getWidth()); - auto height = static_cast(g.getHeight()); - png::image image(width, height); - for (png::uint_32 y = 0; y < height; ++y) + auto width = static_cast(g.getWidth()); + auto height = static_cast(g.getHeight()); + PngImage image(width, height); + for (uint32_t y = 0; y < height; ++y) { - for (png::uint_32 x = 0; x < width; ++x) + for (uint32_t x = 0; x < width; ++x) { Color px = g.get(x, y); - image[y][x] = png::rgb_pixel(px.r, px.g, px.b); + image.at(x, y) = RgbPixel{px.r, px.g, px.b}; } } diff --git a/src/tnt_test.cpp b/src/tnt_test.cpp index 5a3ee32d2..6a13c8b45 100644 --- a/src/tnt_test.cpp +++ b/src/tnt_test.cpp @@ -2,26 +2,13 @@ #include #include #include -#include #include #include +#include #include namespace fs = boost::filesystem; -void loadPalette(const std::string& filename, png::rgb_pixel* buffer) -{ - std::ifstream in(filename, std::ios::binary); - - for (unsigned int i = 0; i < 256; ++i) - { - in.read(reinterpret_cast(&(buffer[i].red)), 1); - in.read(reinterpret_cast(&(buffer[i].green)), 1); - in.read(reinterpret_cast(&(buffer[i].blue)), 1); - in.seekg(1, std::ios::cur); // skip alpha - } -} - int featuresCommand(const std::string& tntPath) { std::ifstream file(tntPath, std::ios::binary); @@ -42,8 +29,8 @@ int featuresCommand(const std::string& tntPath) int tilesCommand(const std::string& palettePath, const std::string& tntPath, const std::string& outputPath) { - png::rgb_pixel palette[256]; - loadPalette(palettePath, palette); + rwe::RgbPixel palette[256]; + rwe::loadPalette(palettePath, palette); std::ifstream file(tntPath, std::ios::binary); if (!file.is_open()) @@ -58,15 +45,13 @@ int tilesCommand(const std::string& palettePath, const std::string& tntPath, con int i = 0; tnt.readTiles([&palette, &i, &outPath](const char* tileData) { - png::image image(32, 32); - for (png::uint_32 y = 0; y < image.get_height(); ++y) + rwe::PngImage image(32, 32); + for (uint32_t y = 0; y < 32; ++y) { - for (png::uint_32 x = 0; x < image.get_width(); ++x) + for (uint32_t x = 0; x < 32; ++x) { auto b = static_cast(tileData[(y * 32) + x]); - assert(b >= 0 && b < 256); - auto px = palette[b]; - image[y][x] = px; + image.at(x, y) = palette[b]; } } @@ -102,8 +87,8 @@ int infoCommand(const std::string& tntPath) int minimapCommand(const std::string& palettePath, const std::string& tntPath, const std::string& outputPath) { - png::rgb_pixel palette[256]; - loadPalette(palettePath, palette); + rwe::RgbPixel palette[256]; + rwe::loadPalette(palettePath, palette); std::ifstream file(tntPath, std::ios::binary); if (!file.is_open()) @@ -115,15 +100,13 @@ int minimapCommand(const std::string& palettePath, const std::string& tntPath, c rwe::TntArchive tnt(&file); auto minimap = tnt.readMinimap(); - png::image image(minimap.width, minimap.height); - for (png::uint_32 y = 0; y < image.get_height(); ++y) + rwe::PngImage image(minimap.width, minimap.height); + for (uint32_t y = 0; y < minimap.height; ++y) { - for (png::uint_32 x = 0; x < image.get_width(); ++x) + for (uint32_t x = 0; x < minimap.width; ++x) { auto b = static_cast(minimap.data[(y * minimap.width) + x]); - assert(b >= 0 && b < 256); - auto px = palette[b]; - image[y][x] = px; + image.at(x, y) = palette[b]; } } From 41711d61433db529e673225c1c7f3e6a999760c8 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 17:43:54 -0700 Subject: [PATCH 02/32] Fix build for modern boost/protobuf/clang, use devbox dependencies - Add devbox.json with all C++ build dependencies - CMakeLists.txt: fall back to find_package(Protobuf) when the vendored protobuf build is not present, remove Boost static lib requirement, handle GLEW target name differences across platforms - Update Boost.Asio usage for 1.87+: io_service -> io_context, resolver::query removal, expires_from_now -> expires_after, io_context.post -> boost::asio::post - Update protobuf usage: ByteSize() -> ByteSizeLong() - Fix unique_ptr -> shared_ptr conversion for newer libc++ strictness - Fix png_write.h: add missing include, fix const_cast - Add CLAUDE.md and update README with devbox build instructions --- CLAUDE.md | 79 ++++++++++++++ CMakeLists.txt | 46 +++++--- README.md | 17 ++- devbox.json | 29 +++++ devbox.lock | 164 ++++++++++++++++++++++++++++ src/main.cpp | 4 +- src/rwe/LoadingNetworkService.cpp | 9 +- src/rwe/LoadingNetworkService.h | 2 +- src/rwe/LoadingScene.cpp | 2 +- src/rwe/MainMenuScene.cpp | 2 +- src/rwe/game/GameNetworkService.cpp | 12 +- src/rwe/game/GameNetworkService.h | 2 +- src/rwe/util/png_write.h | 3 +- 13 files changed, 339 insertions(+), 32 deletions(-) create mode 100644 CLAUDE.md create mode 100644 devbox.json create mode 100644 devbox.lock diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..c0e07f3e6 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Robot War Engine (RWE) is an open-source real-time strategy game engine with high compatibility for Total Annihilation data files. It consists of a C++17 core engine and a TypeScript/Electron launcher application. Active priority is simplifying dependencies and improving new developer onboarding. + +## Build Commands + +### C++ Engine (from repo root) + +```bash +# First time setup (submodules + protobuf) +git submodule update --init --recursive +cd libs && ./build-protobuf.sh && cd .. + +# Build (Linux/macOS) +mkdir build && cd build +cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 +make -j$(nproc) + +# Run unit tests +./build/rwe_test + +# Run a single test by name (Catch2 syntax) +./build/rwe_test "test name pattern" +./build/rwe_test "[tag]" +``` + +### Launcher (from `launcher/` directory) + +```bash +npm ci +npm run tsc # Type check +npm test # Jest tests +npm run lint # ESLint +npm run server # Webpack dev server (hot reload) +npm start # Launch Electron app (needs RWE_HOME env var) +npm run master-server # Local multiplayer master server +npm run package # Package for distribution +``` + +## Architecture + +### Core Engine (`src/rwe/`) + +The engine is built as a static library `librwe` linked by multiple executables (`rwe`, `rwe_bridge`, `rwe_test`, and various format test tools). + +Key subsystems: + +- **sim/** - Deterministic game simulation (units, weapons, projectiles, terrain, resources). Uses fixed-point math types (`SimScalar`, `SimVector`, `SimAngle`) for cross-platform determinism. +- **scene/** - Scene state machine: MainMenuScene → LoadingScene → GameScene. +- **render/** - OpenGL 3.0+ rendering pipeline with GLSL shaders (in `shaders/`). +- **cob/** - Virtual machine executing Total Annihilation's COB unit behavior scripts. CobThread runs concurrent script coroutines within CobExecutionContext. +- **io/** - Parsers for TA file formats: HPI (archives), GAF (sprites), TDF (config), 3DO (models), COB (scripts), FBI (units), TNT (terrain), PCX (images), OTA (maps), GUI (layouts). +- **vfs/** - Virtual file system abstracting over HPI archives and directories. +- **pathfinding/** - A* pathfinding with octile distance on grids. +- **proto/** - Protocol buffer networking (defined in `proto/network.proto`). +- **geometry/** / **math/** - Linear algebra and spatial primitives. +- **collections/** - Custom data structures (MinHeap, VectorMap). + +### Launcher (`launcher/src/`) + +Electron app with React/Redux for the multiplayer lobby. Communicates with the engine via `rwe_bridge` (JSON IPC). Contains `launcher/`, `master-server/`, `game-server/`, and `common/` modules. + +## Code Conventions + +- All C++ code is in the `rwe::` namespace +- Formatting enforced by `.clang-format`: Allman brace style, 4-space indent, no column limit, C++17 standard +- Test files live alongside source: `src/rwe/[subsystem]/[Component].test.cpp` +- Strong typing via opaque ID types: `UnitId`, `PlayerId`, `ProjectileId` (see `OpaqueId`) +- Variant-based state machines for unit behavior and navigation goals +- Error handling uses `Result` types rather than exceptions +- Version derived from git tags (format: `v#.#.#`) + +## CI + +GitHub Actions runs Linux (gcc-12, clang-15) and Windows (MSVC 2022, MinGW64) builds in both Debug and Release configurations. diff --git a/CMakeLists.txt b/CMakeLists.txt index 643a91182..09bfeb943 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -131,18 +131,23 @@ if(MSVC) set(VORBISFILE_DLL "${CMAKE_SOURCE_DIR}/libs/_msvc/extras/libvorbisfile-3.dll") set(WEBP_DLL "${CMAKE_SOURCE_DIR}/libs/_msvc/extras/libwebp-7.dll") else() - set(Protobuf_LIBRARY "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/lib/libprotobuf.a") - set(Protobuf_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/include") - if(WIN32) - set(Protobuf_PROTOC_EXECUTABLE "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/bin/protoc.exe") + if(EXISTS "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/lib/libprotobuf.a") + set(Protobuf_LIBRARY "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/lib/libprotobuf.a") + set(Protobuf_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/include") + if(WIN32) + set(Protobuf_PROTOC_EXECUTABLE "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/bin/protoc.exe") + else() + set(Protobuf_PROTOC_EXECUTABLE "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/bin/protoc") + endif() else() - set(Protobuf_PROTOC_EXECUTABLE "${CMAKE_SOURCE_DIR}/libs/_protobuf-install/bin/protoc") + find_package(Protobuf REQUIRED) + set(Protobuf_LIBRARY protobuf::libprotobuf) + set(Protobuf_INCLUDE_DIR ${Protobuf_INCLUDE_DIRS}) endif() find_package(GLEW REQUIRED) find_windows_dll(GLEW_DLL "glew32.dll") - set(Boost_USE_STATIC_LIBS ON) set(Boost_NO_BOOST_CMAKE ON) find_package(Boost 1.54.0 REQUIRED COMPONENTS filesystem program_options) @@ -645,8 +650,13 @@ target_copy_file(librwe ${GLEW_DLL}) if(MSVC) target_link_libraries(librwe ${GLEW_LIBRARIES}) target_include_directories(librwe PUBLIC ${GLEW_INCLUDE_DIRS}) -else() +elseif(TARGET GLEW::glew) + target_link_libraries(librwe GLEW::glew) +elseif(TARGET GLEW::GLEW) target_link_libraries(librwe GLEW::GLEW) +else() + target_link_libraries(librwe ${GLEW_LIBRARIES}) + target_include_directories(librwe PUBLIC ${GLEW_INCLUDE_DIRS}) endif() target_copy_file(librwe ${SDL2_DLL}) @@ -704,7 +714,21 @@ target_copy_file(librwe ${VORBIS_DLL}) target_copy_file(librwe ${VORBISFILE_DLL}) target_copy_file(librwe ${WEBP_DLL}) -target_link_libraries(librwe ${Protobuf_LIBRARIES}) +if(TARGET protobuf::libprotobuf) + message(STATUS "Using protobuf::libprotobuf imported target") + target_link_libraries(librwe protobuf::libprotobuf) + # protobuf v32+ depends on abseil; link via pkg-config to propagate to consumers of this static lib + find_package(PkgConfig) + if(PkgConfig_FOUND) + pkg_check_modules(ABSL IMPORTED_TARGET absl_log_internal_check_op) + if(ABSL_FOUND) + target_link_libraries(librwe PkgConfig::ABSL) + endif() + endif() +else() + message(STATUS "Using Protobuf_LIBRARIES: ${Protobuf_LIBRARIES}") + target_link_libraries(librwe ${Protobuf_LIBRARIES}) +endif() target_include_directories(librwe PUBLIC ${Protobuf_INCLUDE_DIRS}) target_include_directories(librwe PUBLIC ${CMAKE_CURRENT_BINARY_DIR}) # for generated headers @@ -729,7 +753,6 @@ add_executable(gaf_test src/gaf_test.cpp) target_link_libraries(gaf_test librwe) target_link_libraries(gaf_test ${PNG_LIBRARIES}) target_include_directories(gaf_test PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(gaf_test PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(gaf_test PRIVATE __STDC_LIB_EXT1__=1) endif() @@ -738,7 +761,6 @@ add_executable(pcx_test src/pcx_test.cpp) target_link_libraries(pcx_test librwe) target_link_libraries(pcx_test ${PNG_LIBRARIES}) target_include_directories(pcx_test PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(pcx_test PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(pcx_test PRIVATE __STDC_LIB_EXT1__=1) endif() @@ -747,7 +769,6 @@ add_executable(tnt_test src/tnt_test.cpp) target_link_libraries(tnt_test librwe) target_link_libraries(tnt_test ${PNG_LIBRARIES}) target_include_directories(tnt_test PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(tnt_test PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(tnt_test PRIVATE __STDC_LIB_EXT1__=1) endif() @@ -762,7 +783,6 @@ add_executable(fnt_test src/fnt_test.cpp) target_link_libraries(fnt_test librwe) target_link_libraries(fnt_test ${PNG_LIBRARIES}) target_include_directories(fnt_test PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(fnt_test PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(fnt_test PRIVATE __STDC_LIB_EXT1__=1) endif() @@ -771,7 +791,6 @@ add_executable(texture_test src/texture_test.cpp) target_link_libraries(texture_test librwe) target_link_libraries(texture_test ${PNG_LIBRARIES}) target_include_directories(texture_test PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(texture_test PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(texture_test PRIVATE __STDC_LIB_EXT1__=1) endif() @@ -781,7 +800,6 @@ target_link_libraries(rwe_bridge PRIVATE librwe) target_link_libraries(rwe_bridge PRIVATE nlohmann_json::nlohmann_json) target_link_libraries(rwe_bridge PRIVATE ${PNG_LIBRARIES}) target_include_directories(rwe_bridge PUBLIC ${PNG_INCLUDE_DIRS}) -target_include_directories(rwe_bridge PUBLIC libs/png++-0.2.9) if(WIN32) target_compile_definitions(rwe_bridge PRIVATE __STDC_LIB_EXT1__=1) endif() diff --git a/README.md b/README.md index da29f6083..b0b467f4d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,22 @@ First fetch the source code: cd rwe git submodule update --init --recursive -Then follow the instructions for your platform. +### Devbox (Recommended - Linux/macOS) + +The easiest way to get a working build environment is with [Devbox](https://www.jetify.com/devbox), +which uses Nix to provide all dependencies automatically (including protobuf). + + curl -fsSL https://get.jetify.com/devbox | bash # install devbox (one-time) + devbox shell # enter the dev environment + mkdir build && cd build + cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_POLICY_VERSION_MINIMUM=3.5 + make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) + +Run the tests: + + ./rwe_test + +No manual dependency installation or protobuf compilation step required. ### Windows with Visual Studio diff --git a/devbox.json b/devbox.json new file mode 100644 index 000000000..14fa45bc2 --- /dev/null +++ b/devbox.json @@ -0,0 +1,29 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json", + "packages": [ + "cmake", + "boost", + "protobuf", + "SDL2", + "SDL2_image", + "SDL2_mixer", + "glew", + "libpng", + "zlib", + "pkg-config", + "git" + ], + "shell": { + "init_hook": [ + "echo 'RWE devbox shell ready.'" + ], + "scripts": { + "build": [ + "mkdir -p build && cd build && cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug && make -j$(sysctl -n hw.ncpu 2>/dev/null || nproc)" + ], + "test": [ + "./build/rwe_test" + ] + } + } +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 000000000..c0f620311 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,164 @@ +{ + "lockfile_version": "1", + "packages": { + "SDL2": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#SDL2", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/b6c8nbfs0awmdblgjaajsrvwd0m47r3p-sdl2-compat-2.32.62", + "default": true + } + ] + } + } + }, + "SDL2_image": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#SDL2_image", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/9kkyzjl728ijx8bvy95vvj0b0c2c8zp6-SDL2_image-2.8.8", + "default": true + } + ] + } + } + }, + "SDL2_mixer": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#SDL2_mixer", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/n0ij8v60ck36rni9gfia25cgn0sjf4ia-SDL2_mixer-2.8.1", + "default": true + } + ] + } + } + }, + "boost": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#boost", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/yvlw2q07alcpqsyqk4wxf3llchri5zbd-boost-1.87.0", + "default": true + } + ] + } + } + }, + "cmake": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#cmake", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/1cyc0yckxw16x6mma6jbcp70c7nv8i8i-cmake-4.1.2", + "default": true + } + ] + } + } + }, + "git": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#git", + "source": "nixpkg" + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "last_modified": "2026-02-19T06:30:45Z", + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?lastModified=1771482645&narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D" + }, + "glew": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#glew", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/48z1blr66jpdkycxanw1jqxh4g1xygai-glew-2.2.0-bin", + "default": true + } + ] + } + } + }, + "libpng": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#libpng", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "name": "man", + "path": "/nix/store/np3ii7yhkmgfl4d6rhmgdkb2lsq04lyk-libpng-apng-1.6.53-man", + "default": true + }, + { + "path": "/nix/store/zmz6mlp67gxiw51l3q8gv6x68lm1nanp-libpng-apng-1.6.53", + "default": true + } + ] + } + } + }, + "pkg-config": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#pkg-config", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "name": "man", + "path": "/nix/store/m3xvrpz0p7nnx6ck748ryyhvj50hjjz5-pkg-config-wrapper-0.29.2-man", + "default": true + }, + { + "path": "/nix/store/hxzn7zygy0lxw4s89i86wsa5k0nhwjwk-pkg-config-wrapper-0.29.2", + "default": true + } + ] + } + } + }, + "protobuf": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#protobuf", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/hks7kpdlgx8v4n75yfq3lla03myyfhf2-protobuf-32.1", + "default": true + } + ] + } + } + }, + "zlib": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#zlib", + "source": "nixpkg", + "systems": { + "x86_64-darwin": { + "outputs": [ + { + "path": "/nix/store/x76b36x85dy7q9bqnj5m5i1dzqsrahl9-zlib-1.3.1", + "default": true + } + ] + } + } + } + } +} diff --git a/src/main.cpp b/src/main.cpp index 22c8bd795..5599a666c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -341,7 +341,7 @@ namespace rwe &allSoundTdf, AudioService::LoopToken(), *gameParameters); - sceneManager.setNextScene(std::move(scene)); + sceneManager.setNextScene(std::shared_ptr(std::move(scene))); } else { @@ -351,7 +351,7 @@ namespace rwe &allSoundTdf, viewport.width(), viewport.height()); - sceneManager.setNextScene(std::move(scene)); + sceneManager.setNextScene(std::shared_ptr(std::move(scene))); } logger.info("Entering main loop"); diff --git a/src/rwe/LoadingNetworkService.cpp b/src/rwe/LoadingNetworkService.cpp index 02021fd0a..2a847889e 100644 --- a/src/rwe/LoadingNetworkService.cpp +++ b/src/rwe/LoadingNetworkService.cpp @@ -25,7 +25,8 @@ namespace rwe std::scoped_lock lock(mutex); // boost guarantees that resolve returns non-empty - remoteEndpoints.emplace_back(playerIndex, *resolver.resolve(boost::asio::ip::udp::resolver::query(host, port)), Status::Loading); + auto results = resolver.resolve(host, port); + remoteEndpoints.emplace_back(playerIndex, results.begin()->endpoint(), Status::Loading); } void LoadingNetworkService::setDoneLoading() @@ -158,7 +159,7 @@ namespace rwe } } - auto messageSize = outerMessage.ByteSize(); + auto messageSize = outerMessage.ByteSizeLong(); if (messageSize > getSize(sendBuffer) - 4) { throw std::runtime_error("Message to be sent was bigger than buffer size"); @@ -173,7 +174,7 @@ namespace rwe for (const auto& p : remoteEndpoints) { - spdlog::get("rwe")->debug("Sending notification to {0} {1}, size {2}", p.endpoint.address().to_string(), p.endpoint.port(), outerMessage.ByteSize()); + spdlog::get("rwe")->debug("Sending notification to {0} {1}, size {2}", p.endpoint.address().to_string(), p.endpoint.port(), outerMessage.ByteSizeLong()); socket.send_to(boost::asio::buffer(sendBuffer.data(), messageSize + 4), p.endpoint); } } @@ -181,7 +182,7 @@ namespace rwe void LoadingNetworkService::notifyStatusLoop() { notifyStatus(); - notifyTimer.expires_from_now(std::chrono::milliseconds(100)); + notifyTimer.expires_after(std::chrono::milliseconds(100)); notifyTimer.async_wait([this](const boost::system::error_code& error) { if (error) { diff --git a/src/rwe/LoadingNetworkService.h b/src/rwe/LoadingNetworkService.h index 4d78d89cc..c840b3fb1 100644 --- a/src/rwe/LoadingNetworkService.h +++ b/src/rwe/LoadingNetworkService.h @@ -40,7 +40,7 @@ namespace rwe std::vector remoteEndpoints; // state owned by the worker thread - boost::asio::io_service ioContext; + boost::asio::io_context ioContext; boost::asio::ip::udp::resolver resolver; boost::asio::ip::udp::socket socket; std::array sendBuffer; diff --git a/src/rwe/LoadingScene.cpp b/src/rwe/LoadingScene.cpp index 7c5cc4ae4..e9f916cd0 100644 --- a/src/rwe/LoadingScene.cpp +++ b/src/rwe/LoadingScene.cpp @@ -130,7 +130,7 @@ namespace rwe } networkService.start(gameParameters.localNetworkPort); - sceneContext.sceneManager->setNextScene(createGameScene(gameParameters.mapName, gameParameters.schemaIndex)); + sceneContext.sceneManager->setNextScene(std::shared_ptr(createGameScene(gameParameters.mapName, gameParameters.schemaIndex))); // wait for other players before starting networkService.setDoneLoading(); diff --git a/src/rwe/MainMenuScene.cpp b/src/rwe/MainMenuScene.cpp index 6ade2cd58..f495e1aa6 100644 --- a/src/rwe/MainMenuScene.cpp +++ b/src/rwe/MainMenuScene.cpp @@ -713,7 +713,7 @@ namespace rwe std::move(bgm), params); - sceneContext.sceneManager->setNextScene(std::move(scene)); + sceneContext.sceneManager->setNextScene(std::shared_ptr(std::move(scene))); } Point MainMenuScene::toScaledCoordinates(int x, int y) const diff --git a/src/rwe/game/GameNetworkService.cpp b/src/rwe/game/GameNetworkService.cpp index 8a5f3c145..677e58e20 100644 --- a/src/rwe/game/GameNetworkService.cpp +++ b/src/rwe/game/GameNetworkService.cpp @@ -43,7 +43,7 @@ namespace rwe void GameNetworkService::submitCommands(SceneTime currentSceneTime, const GameNetworkService::CommandSet& commands) { - ioContext.post([this, currentSceneTime, commands]() { + boost::asio::post(ioContext,[this, currentSceneTime, commands]() { this->currentSceneTime = currentSceneTime; for (auto& e : endpoints) { @@ -54,7 +54,7 @@ namespace rwe void GameNetworkService::submitGameHash(GameHash hash) { - ioContext.post([this, hash]() { + boost::asio::post(ioContext,[this, hash]() { for (auto& e : endpoints) { e.hashSendBuffer.push_back(hash); @@ -65,7 +65,7 @@ namespace rwe SceneTime GameNetworkService::estimateAvergeSceneTime(SceneTime localSceneTime) { std::promise result; - ioContext.post([this, localSceneTime, &result]() { + boost::asio::post(ioContext,[this, localSceneTime, &result]() { auto time = getTimestamp(); auto otherTimes = choose(endpoints, [](const auto& e) { return e.lastKnownSceneTime; }); @@ -79,7 +79,7 @@ namespace rwe float GameNetworkService::getMaxAverageRttMillis() { std::promise result; - ioContext.post([this, &result]() { + boost::asio::post(ioContext,[this, &result]() { auto maxRtt = 0.0f; for (const auto& e : endpoints) { @@ -171,7 +171,7 @@ namespace rwe void GameNetworkService::sendLoop() { sendToAll(); - sendTimer.expires_from_now(std::chrono::milliseconds(100)); + sendTimer.expires_after(std::chrono::milliseconds(100)); sendTimer.async_wait([this](const boost::system::error_code& error) { if (error) { @@ -203,7 +203,7 @@ namespace rwe } auto message = createProtoMessage(packetId, localPlayerId, currentSceneTime, endpoint.nextCommandToSend, endpoint.nextCommandToReceive, endpoint.nextHashToSend, endpoint.nextHashToReceive, delay, endpoint.sendBuffer, endpoint.hashSendBuffer); - auto messageSize = message.ByteSize(); + auto messageSize = message.ByteSizeLong(); if (messageSize > getSize(sendBuffer) - 4) { throw std::runtime_error("Message to be sent was bigger than buffer size"); diff --git a/src/rwe/game/GameNetworkService.h b/src/rwe/game/GameNetworkService.h index 4da5ae7d6..07a77da3b 100644 --- a/src/rwe/game/GameNetworkService.h +++ b/src/rwe/game/GameNetworkService.h @@ -82,7 +82,7 @@ namespace rwe std::thread networkThread; - boost::asio::io_service ioContext; + boost::asio::io_context ioContext; boost::asio::ip::udp::resolver resolver; boost::asio::ip::udp::socket socket; boost::asio::steady_timer sendTimer; diff --git a/src/rwe/util/png_write.h b/src/rwe/util/png_write.h index 0d250c68f..cc498d40c 100644 --- a/src/rwe/util/png_write.h +++ b/src/rwe/util/png_write.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include @@ -121,7 +122,7 @@ namespace rwe for (uint32_t y = 0; y < height; ++y) { - png_write_row(png, reinterpret_cast(&pixels[y * width])); + png_write_row(png, reinterpret_cast(const_cast(&pixels[y * width]))); } png_write_end(png, nullptr); From 798b9fd3dc03d70edfe2c24cd2d3e5883904c9fc Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 17:43:55 -0700 Subject: [PATCH 03/32] Upgrade GLSL shaders from version 130 to 150 for macOS compatibility macOS only supports GLSL 150+ (OpenGL 3.2 Core profile), not GLSL 130. The shaders already used modern syntax so only the version directive needed updating. Co-Authored-By: Claude Opus 4.6 --- shaders/basicColor.frag | 2 +- shaders/basicColor.vert | 2 +- shaders/basicTexture.frag | 2 +- shaders/basicTexture.vert | 2 +- shaders/flashEffect.frag | 2 +- shaders/flashEffect.vert | 2 +- shaders/mapTerrain.frag | 2 +- shaders/mapTerrain.vert | 2 +- shaders/unitBuild.frag | 2 +- shaders/unitBuild.vert | 2 +- shaders/unitShadow.frag | 2 +- shaders/unitShadow.vert | 2 +- shaders/unitTexture.frag | 2 +- shaders/unitTexture.vert | 2 +- shaders/worldPost.frag | 2 +- shaders/worldPost.vert | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/shaders/basicColor.frag b/shaders/basicColor.frag index d9c9f4ffc..0bffb6a79 100644 --- a/shaders/basicColor.frag +++ b/shaders/basicColor.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec3 fragColor; out vec4 outColor; diff --git a/shaders/basicColor.vert b/shaders/basicColor.vert index f2db8c400..6c3a8b044 100644 --- a/shaders/basicColor.vert +++ b/shaders/basicColor.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; diff --git a/shaders/basicTexture.frag b/shaders/basicTexture.frag index a27abfc9c..9cdaf7ebc 100644 --- a/shaders/basicTexture.frag +++ b/shaders/basicTexture.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; out vec4 outColor; diff --git a/shaders/basicTexture.vert b/shaders/basicTexture.vert index ab601e970..222cbb62d 100644 --- a/shaders/basicTexture.vert +++ b/shaders/basicTexture.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; diff --git a/shaders/flashEffect.frag b/shaders/flashEffect.frag index e1f2c159d..5ac4c36fe 100644 --- a/shaders/flashEffect.frag +++ b/shaders/flashEffect.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; out vec4 outColor; diff --git a/shaders/flashEffect.vert b/shaders/flashEffect.vert index ab601e970..222cbb62d 100644 --- a/shaders/flashEffect.vert +++ b/shaders/flashEffect.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; diff --git a/shaders/mapTerrain.frag b/shaders/mapTerrain.frag index 0330cd4c5..0858d30f3 100644 --- a/shaders/mapTerrain.frag +++ b/shaders/mapTerrain.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec3 fragTexCoord; out vec4 outColor; diff --git a/shaders/mapTerrain.vert b/shaders/mapTerrain.vert index 34d13ff74..392d7fe94 100644 --- a/shaders/mapTerrain.vert +++ b/shaders/mapTerrain.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; diff --git a/shaders/unitBuild.frag b/shaders/unitBuild.frag index 25c6f2931..309f8fd6b 100644 --- a/shaders/unitBuild.frag +++ b/shaders/unitBuild.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; in float height; diff --git a/shaders/unitBuild.vert b/shaders/unitBuild.vert index 1237ac4c6..07fa00860 100644 --- a/shaders/unitBuild.vert +++ b/shaders/unitBuild.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; uniform mat4 modelMatrix; diff --git a/shaders/unitShadow.frag b/shaders/unitShadow.frag index af37405e2..83342f8b0 100644 --- a/shaders/unitShadow.frag +++ b/shaders/unitShadow.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; in float height; diff --git a/shaders/unitShadow.vert b/shaders/unitShadow.vert index ec09eeb3f..0d7f2ed91 100644 --- a/shaders/unitShadow.vert +++ b/shaders/unitShadow.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 vpMatrix; uniform mat4 modelMatrix; diff --git a/shaders/unitTexture.frag b/shaders/unitTexture.frag index 6d8144468..4d7f3d427 100644 --- a/shaders/unitTexture.frag +++ b/shaders/unitTexture.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; in float height; diff --git a/shaders/unitTexture.vert b/shaders/unitTexture.vert index 1237ac4c6..07fa00860 100644 --- a/shaders/unitTexture.vert +++ b/shaders/unitTexture.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 uniform mat4 mvpMatrix; uniform mat4 modelMatrix; diff --git a/shaders/worldPost.frag b/shaders/worldPost.frag index c1178c026..2464a2124 100644 --- a/shaders/worldPost.frag +++ b/shaders/worldPost.frag @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec2 fragTexCoord; out vec4 outColor; diff --git a/shaders/worldPost.vert b/shaders/worldPost.vert index e472d57fa..dd7fd2b3b 100644 --- a/shaders/worldPost.vert +++ b/shaders/worldPost.vert @@ -1,4 +1,4 @@ -#version 130 +#version 150 in vec3 position; in vec2 texCoord; From 28f202af1dbf4b7d7e5e9a569dfe45fe02851ad5 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 18:54:54 -0700 Subject: [PATCH 04/32] Add missing #include across 31 source files Modern GCC/libstdc++ no longer transitively includes through other standard library headers, causing build failures for std::find, std::find_if, std::sort, std::clamp, etc. --- src/bridge.cpp | 1 + src/rwe/BoxTreeSplit.h | 1 + src/rwe/LoadingScene.cpp | 1 + src/rwe/LoadingScene_util.cpp | 1 + src/rwe/MainMenuModel.cpp | 1 + src/rwe/MainMenuScene.cpp | 1 + src/rwe/TextureService.cpp | 1 + src/rwe/UiRenderService.h | 1 + src/rwe/atlas_util.cpp | 1 + src/rwe/cob/CobEnvironment.cpp | 1 + src/rwe/collections/MinHeap.test.cpp | 1 + src/rwe/collections/VectorMap.h | 1 + src/rwe/collections/VectorMap.test.cpp | 1 + src/rwe/game/GameNetworkService.cpp | 1 + src/rwe/game/PlayerCommandService.cpp | 1 + src/rwe/geometry/BoundingBox3x.h | 1 + src/rwe/grid/Point.cpp | 1 + src/rwe/io/hpi/hpi_util.cpp | 1 + src/rwe/io/tnt/TntArchive.cpp | 1 + src/rwe/math/Vector3f.cpp | 1 + src/rwe/pathfinding/AStarPathFinder.h | 1 + src/rwe/sim/GameSimulation.cpp | 1 + src/rwe/sim/UnitBehaviorService_util.cpp | 1 + src/rwe/ui/UiFactory.cpp | 1 + src/rwe/ui/UiListBox.cpp | 1 + src/rwe/ui/UiPanel.cpp | 1 + src/rwe/ui/UiPanel.h | 1 + src/rwe/ui/UiScrollBar.cpp | 1 + src/rwe/ui/UiSurface.cpp | 1 + src/rwe/vertex_height.cpp | 1 + src/rwe/vfs/DirectoryFileSystem.cpp | 1 + 31 files changed, 31 insertions(+) diff --git a/src/bridge.cpp b/src/bridge.cpp index 74a1ad82c..4d10215ff 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/rwe/BoxTreeSplit.h b/src/rwe/BoxTreeSplit.h index 8f0ca9853..3ea442810 100644 --- a/src/rwe/BoxTreeSplit.h +++ b/src/rwe/BoxTreeSplit.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/src/rwe/LoadingScene.cpp b/src/rwe/LoadingScene.cpp index e9f916cd0..d633783ba 100644 --- a/src/rwe/LoadingScene.cpp +++ b/src/rwe/LoadingScene.cpp @@ -1,4 +1,5 @@ #include "LoadingScene.h" +#include #include #include #include diff --git a/src/rwe/LoadingScene_util.cpp b/src/rwe/LoadingScene_util.cpp index 05b0070a1..d6181c1a5 100644 --- a/src/rwe/LoadingScene_util.cpp +++ b/src/rwe/LoadingScene_util.cpp @@ -1,4 +1,5 @@ #include "LoadingScene_util.h" +#include #include #include diff --git a/src/rwe/MainMenuModel.cpp b/src/rwe/MainMenuModel.cpp index 5ff4b6534..72dd53fe9 100644 --- a/src/rwe/MainMenuModel.cpp +++ b/src/rwe/MainMenuModel.cpp @@ -1,4 +1,5 @@ #include "MainMenuModel.h" +#include namespace rwe { diff --git a/src/rwe/MainMenuScene.cpp b/src/rwe/MainMenuScene.cpp index f495e1aa6..98b1798ad 100644 --- a/src/rwe/MainMenuScene.cpp +++ b/src/rwe/MainMenuScene.cpp @@ -1,4 +1,5 @@ #include "MainMenuScene.h" +#include #include #include #include diff --git a/src/rwe/TextureService.cpp b/src/rwe/TextureService.cpp index ff77ffa34..3c2d18947 100644 --- a/src/rwe/TextureService.cpp +++ b/src/rwe/TextureService.cpp @@ -1,4 +1,5 @@ #include "TextureService.h" +#include #include #include #include diff --git a/src/rwe/UiRenderService.h b/src/rwe/UiRenderService.h index f1d032fc5..c10248be2 100644 --- a/src/rwe/UiRenderService.h +++ b/src/rwe/UiRenderService.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/src/rwe/atlas_util.cpp b/src/rwe/atlas_util.cpp index 07d8a9d0d..8dd01e1a1 100644 --- a/src/rwe/atlas_util.cpp +++ b/src/rwe/atlas_util.cpp @@ -1,4 +1,5 @@ #include "atlas_util.h" +#include #include #include #include diff --git a/src/rwe/cob/CobEnvironment.cpp b/src/rwe/cob/CobEnvironment.cpp index 3b1827615..30d6add39 100644 --- a/src/rwe/cob/CobEnvironment.cpp +++ b/src/rwe/cob/CobEnvironment.cpp @@ -1,4 +1,5 @@ #include "CobEnvironment.h" +#include #include namespace rwe diff --git a/src/rwe/collections/MinHeap.test.cpp b/src/rwe/collections/MinHeap.test.cpp index bc391b445..97b0c489b 100644 --- a/src/rwe/collections/MinHeap.test.cpp +++ b/src/rwe/collections/MinHeap.test.cpp @@ -1,4 +1,5 @@ #include +#include namespace std { diff --git a/src/rwe/collections/VectorMap.h b/src/rwe/collections/VectorMap.h index 08523907d..d063d3dab 100644 --- a/src/rwe/collections/VectorMap.h +++ b/src/rwe/collections/VectorMap.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/src/rwe/collections/VectorMap.test.cpp b/src/rwe/collections/VectorMap.test.cpp index a6fdf14ab..6ba047988 100644 --- a/src/rwe/collections/VectorMap.test.cpp +++ b/src/rwe/collections/VectorMap.test.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include diff --git a/src/rwe/game/GameNetworkService.cpp b/src/rwe/game/GameNetworkService.cpp index 677e58e20..1bdd6eef8 100644 --- a/src/rwe/game/GameNetworkService.cpp +++ b/src/rwe/game/GameNetworkService.cpp @@ -1,4 +1,5 @@ #include "GameNetworkService.h" +#include #include #include #include diff --git a/src/rwe/game/PlayerCommandService.cpp b/src/rwe/game/PlayerCommandService.cpp index 0ef713414..63313fe1e 100644 --- a/src/rwe/game/PlayerCommandService.cpp +++ b/src/rwe/game/PlayerCommandService.cpp @@ -1,4 +1,5 @@ #include "PlayerCommandService.h" +#include namespace rwe { diff --git a/src/rwe/geometry/BoundingBox3x.h b/src/rwe/geometry/BoundingBox3x.h index 09c048660..017fc8798 100644 --- a/src/rwe/geometry/BoundingBox3x.h +++ b/src/rwe/geometry/BoundingBox3x.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace rwe { diff --git a/src/rwe/grid/Point.cpp b/src/rwe/grid/Point.cpp index d8dbb5bde..0ffc0957f 100644 --- a/src/rwe/grid/Point.cpp +++ b/src/rwe/grid/Point.cpp @@ -1,4 +1,5 @@ #include "Point.h" +#include namespace rwe { diff --git a/src/rwe/io/hpi/hpi_util.cpp b/src/rwe/io/hpi/hpi_util.cpp index 7d3d5aa7f..ac5b58c05 100644 --- a/src/rwe/io/hpi/hpi_util.cpp +++ b/src/rwe/io/hpi/hpi_util.cpp @@ -1,4 +1,5 @@ #include "hpi_util.h" +#include #include #include diff --git a/src/rwe/io/tnt/TntArchive.cpp b/src/rwe/io/tnt/TntArchive.cpp index 5c4923141..22a3031c2 100644 --- a/src/rwe/io/tnt/TntArchive.cpp +++ b/src/rwe/io/tnt/TntArchive.cpp @@ -1,4 +1,5 @@ #include "TntArchive.h" +#include #include #include diff --git a/src/rwe/math/Vector3f.cpp b/src/rwe/math/Vector3f.cpp index 57b537f28..300666ce0 100644 --- a/src/rwe/math/Vector3f.cpp +++ b/src/rwe/math/Vector3f.cpp @@ -1,4 +1,5 @@ #include "Vector3f.h" +#include #include diff --git a/src/rwe/pathfinding/AStarPathFinder.h b/src/rwe/pathfinding/AStarPathFinder.h index 9f9c4f101..c82bdb18a 100644 --- a/src/rwe/pathfinding/AStarPathFinder.h +++ b/src/rwe/pathfinding/AStarPathFinder.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/src/rwe/sim/GameSimulation.cpp b/src/rwe/sim/GameSimulation.cpp index c900baffe..aa7b48109 100644 --- a/src/rwe/sim/GameSimulation.cpp +++ b/src/rwe/sim/GameSimulation.cpp @@ -1,4 +1,5 @@ #include "GameSimulation.h" +#include #include #include #include diff --git a/src/rwe/sim/UnitBehaviorService_util.cpp b/src/rwe/sim/UnitBehaviorService_util.cpp index f01d8fdd6..f60806f2b 100644 --- a/src/rwe/sim/UnitBehaviorService_util.cpp +++ b/src/rwe/sim/UnitBehaviorService_util.cpp @@ -1,4 +1,5 @@ #include "UnitBehaviorService_util.h" +#include #include diff --git a/src/rwe/ui/UiFactory.cpp b/src/rwe/ui/UiFactory.cpp index 4a2100f56..af1624474 100644 --- a/src/rwe/ui/UiFactory.cpp +++ b/src/rwe/ui/UiFactory.cpp @@ -1,4 +1,5 @@ #include "UiFactory.h" +#include #include #include diff --git a/src/rwe/ui/UiListBox.cpp b/src/rwe/ui/UiListBox.cpp index 860034b4c..62a199a26 100644 --- a/src/rwe/ui/UiListBox.cpp +++ b/src/rwe/ui/UiListBox.cpp @@ -1,4 +1,5 @@ #include "UiListBox.h" +#include namespace rwe { diff --git a/src/rwe/ui/UiPanel.cpp b/src/rwe/ui/UiPanel.cpp index 9bb55753e..d5900406c 100644 --- a/src/rwe/ui/UiPanel.cpp +++ b/src/rwe/ui/UiPanel.cpp @@ -1,4 +1,5 @@ #include "UiPanel.h" +#include #include namespace rwe diff --git a/src/rwe/ui/UiPanel.h b/src/rwe/ui/UiPanel.h index eb36ccb1c..6ecd02334 100644 --- a/src/rwe/ui/UiPanel.h +++ b/src/rwe/ui/UiPanel.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include diff --git a/src/rwe/ui/UiScrollBar.cpp b/src/rwe/ui/UiScrollBar.cpp index d5bfcf3d0..728ce53a0 100644 --- a/src/rwe/ui/UiScrollBar.cpp +++ b/src/rwe/ui/UiScrollBar.cpp @@ -1,4 +1,5 @@ #include "UiScrollBar.h" +#include namespace rwe { diff --git a/src/rwe/ui/UiSurface.cpp b/src/rwe/ui/UiSurface.cpp index aee27573e..9bfd8b9c2 100644 --- a/src/rwe/ui/UiSurface.cpp +++ b/src/rwe/ui/UiSurface.cpp @@ -1,4 +1,5 @@ #include "UiSurface.h" +#include namespace rwe { diff --git a/src/rwe/vertex_height.cpp b/src/rwe/vertex_height.cpp index dba18b0f2..b2398b9f4 100644 --- a/src/rwe/vertex_height.cpp +++ b/src/rwe/vertex_height.cpp @@ -1,4 +1,5 @@ #include "vertex_height.h" +#include namespace rwe { diff --git a/src/rwe/vfs/DirectoryFileSystem.cpp b/src/rwe/vfs/DirectoryFileSystem.cpp index b29b58fcc..16659d77a 100644 --- a/src/rwe/vfs/DirectoryFileSystem.cpp +++ b/src/rwe/vfs/DirectoryFileSystem.cpp @@ -1,4 +1,5 @@ #include "DirectoryFileSystem.h" +#include #include #include From 82ed3fabb2c8197dc86bd7cd9e2e77885fb23570 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 18:58:58 -0700 Subject: [PATCH 05/32] Detect repo path change in protobuf build script Add path-change detection to build-protobuf.sh so moving the repo automatically triggers a clean rebuild instead of failing with stale libtool paths. --- devbox.json | 3 ++- devbox.lock | 4 ++++ libs/build-protobuf.sh | 15 +++++++++++++++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/devbox.json b/devbox.json index 14fa45bc2..983307b98 100644 --- a/devbox.json +++ b/devbox.json @@ -11,7 +11,8 @@ "libpng", "zlib", "pkg-config", - "git" + "git", + "libX11.dev" ], "shell": { "init_hook": [ diff --git a/devbox.lock b/devbox.lock index c0f620311..d9809953a 100644 --- a/devbox.lock +++ b/devbox.lock @@ -94,6 +94,10 @@ } } }, + "libX11.dev": { + "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#libX11.dev", + "source": "nixpkg" + }, "libpng": { "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#libpng", "source": "nixpkg", diff --git a/libs/build-protobuf.sh b/libs/build-protobuf.sh index a3cd00724..916d7d14e 100755 --- a/libs/build-protobuf.sh +++ b/libs/build-protobuf.sh @@ -3,17 +3,31 @@ set -euo pipefail install_dir="${PWD}/_protobuf-install" +path_marker="$install_dir/build_path" pushd protobuf source_protobuf_version="$(git rev-parse HEAD)" built_protobuf_version="" +built_path="" if [ -f "$install_dir/done" ]; then built_protobuf_version="$(cat "$install_dir/done")" fi +if [ -f "$path_marker" ]; then + built_path="$(cat "$path_marker")" +fi + +if [ "$built_path" != "$install_dir" ]; then + echo "repo path has changed (was: ${built_path:-}, now: $install_dir)" + echo "cleaning protobuf source tree" + git clean -fdx + rm -rf "$install_dir" + built_protobuf_version="" +fi + if [ "$source_protobuf_version" != "$built_protobuf_version" ]; then echo "built protobuf is a different version than source" echo "source protobuf: $source_protobuf_version" @@ -25,6 +39,7 @@ if [ "$source_protobuf_version" != "$built_protobuf_version" ]; then make -j`nproc` make install echo "$source_protobuf_version" > "$install_dir/done" + echo "$install_dir" > "$path_marker" echo "finished building protobuf: $source_protobuf_version" else echo "built protobuf matches source protobuf, skipping protobuf build" From ac3f5df7c894f34848e1fdb0fd7aa0ef1692d7ec Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Fri, 20 Mar 2026 19:42:18 -0700 Subject: [PATCH 06/32] Fix DiscreteRect::bottom() returning y + width instead of y + height The bug had no current callers but would produce wrong bounds for any non-square rectangle. --- src/rwe/grid/DiscreteRect.h | 2 +- src/rwe/grid/DiscreteRect.test.cpp | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/rwe/grid/DiscreteRect.h b/src/rwe/grid/DiscreteRect.h index a4933e3a6..22a1e6707 100644 --- a/src/rwe/grid/DiscreteRect.h +++ b/src/rwe/grid/DiscreteRect.h @@ -49,7 +49,7 @@ namespace rwe } int bottom() const { - return y + width; + return y + height; } /** diff --git a/src/rwe/grid/DiscreteRect.test.cpp b/src/rwe/grid/DiscreteRect.test.cpp index dcb0e99c5..6532531c7 100644 --- a/src/rwe/grid/DiscreteRect.test.cpp +++ b/src/rwe/grid/DiscreteRect.test.cpp @@ -289,6 +289,15 @@ namespace rwe } } + SECTION(".bottom") + { + SECTION("returns y + height for non-square rects") + { + DiscreteRect r(1, 2, 3, 5); + REQUIRE(r.bottom() == 7); + } + } + SECTION(".translate") { SECTION("translates the rectangle") From 406821c68ee2ed8b427b0ea514dd92523d8e78e3 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 12:40:09 -0700 Subject: [PATCH 07/32] Generate run.sh for Linux GPU driver discovery, update README CMake now generates a run.sh wrapper on Linux that prepends system lib paths so the Nix-built binary can find the GPU driver. Updated README with consolidated Linux/macOS build instructions and devbox setup. --- CMakeLists.txt | 11 ++++++++ README.md | 76 ++++++++++++++++++++++++++++---------------------- 2 files changed, 53 insertions(+), 34 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 09bfeb943..f821231a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -743,6 +743,17 @@ add_custom_command(TARGET librwe PRE_BUILD add_executable(rwe src/main.cpp) target_link_libraries(rwe librwe) +if(UNIX AND NOT APPLE) + file(WRITE "${CMAKE_BINARY_DIR}/run.sh" "#!/bin/bash +# The devbox-built binary links against Nix's libglvnd, which can't find +# the system GPU driver (e.g. Mesa). Prepend system lib paths so it's +# discovered. Not needed on macOS, which uses Apple's OpenGL framework +# directly rather than libglvnd. +LD_LIBRARY_PATH=/usr/lib:/usr/lib64:/usr/lib/x86_64-linux-gnu:\$LD_LIBRARY_PATH exec ./rwe \"\$@\" +") + file(CHMOD "${CMAKE_BINARY_DIR}/run.sh" PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) +endif() + add_executable(hpi_test src/hpi_test.cpp) target_link_libraries(hpi_test librwe) diff --git a/README.md b/README.md index b0b467f4d..887ed2b06 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,14 @@ To find the files, click "Environment: RWE\_COMPILER=MSYS; Configuration: Releas RWE is currently only available on Windows, however the code is also built and tested on Linux. Official Linux binaries will be available when the project reaches a stable version. +MacOS should work via the devbox build below, tho it's not regularly tested. Source code is hosted on Github: https://github.com/MHeasell/rwe -## How to Install - +## How to Install +Windows: 1. Create the folder `%AppData%/RWE/Data` and copy your TA data files to it (.hpi, .ufo, rev31.gp3, etc.) 2. Run rwe.exe (if you used the installer, RWE will be in your start menu items) @@ -67,23 +68,6 @@ First fetch the source code: cd rwe git submodule update --init --recursive -### Devbox (Recommended - Linux/macOS) - -The easiest way to get a working build environment is with [Devbox](https://www.jetify.com/devbox), -which uses Nix to provide all dependencies automatically (including protobuf). - - curl -fsSL https://get.jetify.com/devbox | bash # install devbox (one-time) - devbox shell # enter the dev environment - mkdir build && cd build - cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_POLICY_VERSION_MINIMUM=3.5 - make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) - -Run the tests: - - ./rwe_test - -No manual dependency installation or protobuf compilation step required. - ### Windows with Visual Studio Get the RWE MSVC libraries bundle. @@ -149,8 +133,45 @@ Install Visual Studio Code, and open it. Under extensions, search for C/C++, and File > Open Folder, choose the root directory of the RWE repository (where CMakeLists.txt is). At the bottom of the VS Code window you should see CMake: [Debug]: Ready and probably No Kit Selected. Click No Kit Selected to choose which compiler to use. VS Code should auto-detect compilers on your machine, so if none are listed here, install Visual Studio or MSYS2 first and try again. Once a toolset is selected, CMake will configure itself for the project, with its output in the OUTPUT window. When that's done, you should be able to build by clicking the Build button there on the bottom or hit F7. -### Ubuntu - +### Linux/macOS +You can build without TA game assets, but to run the game rwe will need to know where they are. +rwe looks in $HOME/.rwe/Data by default. +> After building, you can also override it at runtime, e.g. `./rwe --data-path "$HOME/src/TA/Total Annihilation"` + +To copy TA data files (.hpi, .ufo, .ccx, rev31.gp3, etc.) in the default dir: +```bash +mkdir -p $HOME/.rwe/Data +cp /path/to/totala/*.hpi $HOME/.rwe/Data +cp /path/to/totala/*.ufo $HOME/.rwe/Data +cp /path/to/totala/*.ccx $HOME/.rwe/Data +cp /path/to/totala/*.gpf $HOME/.rwe/Data +cp /path/to/totala/*.gp3 $HOME/.rwe/Data +``` +Or, symlink: `ln -s "/path/to/totala/" ~/.rwe/Data` + +#### Devbox +The easiest way to get a working build environment is with [Devbox](https://www.jetify.com/devbox), +which uses Nix to provide dependencies automatically. +```bash +curl -fsSL https://get.jetify.com/devbox | bash # installs devbox (one-time) +# From the rwe repo base dir: +devbox shell # Enters the dev environment - uses devbox.json to pull in dependencies. May take a while the first time. +mkdir build && cd build +cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 +make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) # -j isn't necessary, but builds with multiple threads and will reduce build time +./rwe_test # run tests, of course +# run the game- this should work for MacOS, tho in Linux the devbox/nix build may have a quirk that gives you OpenGL related errors on launch +./rwe +# If you see "Could not get EGL display" or other OpenGL related errors on launch, try this instead of `./rwe` +# This script just runs rwe with LD_LIBRARY_PATH set to a good guess of which dirs your video drivers exist in. +./run.sh +``` +```-DCMAKE_EXPORT_COMPILE_COMMANDS=1``` is optional. It generates `compile_commands.json` which some VS Code plugins like clangd can read in order to automatically configure themselves for the project, to give the linter/tools like go-to-definition the same view of the code the compiler has. + + +#### Ubuntu +> Note: these steps may be a bit out of date, but the Devbox build above should work fine on Ubuntu. +> If you want a native build environment, take a look at devbox.json to get an idea of the dependencies required. Install the necessary packages: sudo add-apt-repository ppa:ubuntu-toolchain-r/test @@ -178,7 +199,6 @@ Here's how you might install CMake: export PATH=$(pwd)/cmake-3.8.2-Linux-x86_64/bin:$PATH Compile protobuf: - cd /path/to/rwe cd libs ./build-protobuf.sh @@ -195,18 +215,6 @@ Now build the code: The -DCMAKE_EXPORT_COMPILE_COMMANDS=1 is optional. It generates compile_commands.json which some VS Code plugins like clangd can read in order to automatically configure themselves for the project. Note if LLVM/clang is installed, export CC=clang CXX=clang++ should also work. -Install some TA data files (.hpi, .ufo, .ccx, rev31.gp3, etc.) -to your local data directory: - - mkdir -p $HOME/.rwe/Data - cp /path/to/totala/*.hpi $HOME/.rwe/Data - cp /path/to/totala/*.ufo $HOME/.rwe/Data - cp /path/to/totala/*.ccx $HOME/.rwe/Data - cp /path/to/totala/*.gpf $HOME/.rwe/Data - cp /path/to/totala/*.gp3 $HOME/.rwe/Data - -Alternatively you can symlink `.rwe/Data` to your TA directory. - Finally, launch RWE from the top-level project directory: cd /path/to/rwe From 780a5196c25cca3f8c1f32a39401946dc942dbee Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 13:36:51 -0700 Subject: [PATCH 08/32] Replace boost::filesystem with std::filesystem Drop the Boost.Filesystem dependency in favor of the C++17 standard library equivalent. Add missing #include in RadiansAngle.cpp and gaf_test.cpp where it was previously provided transitively via Boost headers. --- src/bridge.cpp | 4 ++-- src/gaf_test.cpp | 5 +++-- src/hpi_test.cpp | 4 ++-- src/main.cpp | 4 ++-- src/rwe/RadiansAngle.cpp | 1 + src/rwe/util.cpp | 10 +++++----- src/rwe/util.h | 6 +++--- src/rwe/vfs/CompositeVirtualFileSystem.cpp | 8 ++++---- src/rwe/vfs/CompositeVirtualFileSystem.h | 6 +++--- src/rwe/vfs/DirectoryFileSystem.cpp | 10 +++++----- src/rwe/vfs/DirectoryFileSystem.h | 6 +++--- src/tnt_test.cpp | 4 ++-- 12 files changed, 35 insertions(+), 33 deletions(-) diff --git a/src/bridge.cpp b/src/bridge.cpp index 4d10215ff..77115dfa3 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -1,7 +1,7 @@ #include #include -#include #include +#include #include #include #include @@ -16,7 +16,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; using json = nlohmann::json; diff --git a/src/gaf_test.cpp b/src/gaf_test.cpp index da5f9bdeb..f880f096f 100644 --- a/src/gaf_test.cpp +++ b/src/gaf_test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -7,7 +8,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; int listCommand(const std::string& filename) { diff --git a/src/hpi_test.cpp b/src/hpi_test.cpp index 0f2292d7f..92719d211 100644 --- a/src/hpi_test.cpp +++ b/src/hpi_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -6,7 +6,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; std::string schemeName(rwe::HpiArchive::File::CompressionScheme scheme) { diff --git a/src/main.cpp b/src/main.cpp index 5599a666c..4620837ac 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,6 @@ #include -#include #include +#include #include #include #include @@ -31,7 +31,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; namespace po = boost::program_options; namespace rwe diff --git a/src/rwe/RadiansAngle.cpp b/src/rwe/RadiansAngle.cpp index 931871707..4ed2a8386 100644 --- a/src/rwe/RadiansAngle.cpp +++ b/src/rwe/RadiansAngle.cpp @@ -1,4 +1,5 @@ #include "RadiansAngle.h" +#include #include #include diff --git a/src/rwe/util.cpp b/src/rwe/util.cpp index 1ba184966..9a297fbe8 100644 --- a/src/rwe/util.cpp +++ b/src/rwe/util.cpp @@ -5,7 +5,7 @@ namespace rwe { #ifdef RWE_PLATFORM_WINDOWS - std::optional getLocalDataPath() + std::optional getLocalDataPath() { auto appData = std::getenv("APPDATA"); if (appData == nullptr) @@ -13,7 +13,7 @@ namespace rwe return std::nullopt; } - boost::filesystem::path path(appData); + std::filesystem::path path(appData); path /= "RWE"; return path; @@ -21,7 +21,7 @@ namespace rwe #endif #ifdef RWE_PLATFORM_LINUX - std::optional getLocalDataPath() + std::optional getLocalDataPath() { auto home = std::getenv("HOME"); if (home == nullptr) @@ -29,14 +29,14 @@ namespace rwe return std::nullopt; } - boost::filesystem::path path(home); + std::filesystem::path path(home); path /= ".rwe"; return path; } #endif - std::optional getSearchPath() + std::optional getSearchPath() { auto path = getLocalDataPath(); if (!path) diff --git a/src/rwe/util.h b/src/rwe/util.h index e4d3994da..a0f9db30f 100644 --- a/src/rwe/util.h +++ b/src/rwe/util.h @@ -1,12 +1,12 @@ #pragma once -#include +#include #include namespace rwe { - std::optional getLocalDataPath(); - std::optional getSearchPath(); + std::optional getLocalDataPath(); + std::optional getSearchPath(); float toleranceToRadians(unsigned int angle); } diff --git a/src/rwe/vfs/CompositeVirtualFileSystem.cpp b/src/rwe/vfs/CompositeVirtualFileSystem.cpp index 67e9ba3ab..e5180e994 100644 --- a/src/rwe/vfs/CompositeVirtualFileSystem.cpp +++ b/src/rwe/vfs/CompositeVirtualFileSystem.cpp @@ -1,12 +1,12 @@ #include "CompositeVirtualFileSystem.h" -#include +#include #include #include #include #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; namespace rwe { @@ -144,7 +144,7 @@ namespace rwe } } - void addToVfs(CompositeVirtualFileSystem& vfs, const boost::filesystem::path& searchPath) + void addToVfs(CompositeVirtualFileSystem& vfs, const std::filesystem::path& searchPath) { std::vector hpiExtensions{".hpi", ".ufo", ".ccx", ".gpf", ".gp3"}; @@ -157,7 +157,7 @@ namespace rwe } } - CompositeVirtualFileSystem constructVfs(const boost::filesystem::path& searchPath) + CompositeVirtualFileSystem constructVfs(const std::filesystem::path& searchPath) { CompositeVirtualFileSystem vfs; addToVfs(vfs, searchPath); diff --git a/src/rwe/vfs/CompositeVirtualFileSystem.h b/src/rwe/vfs/CompositeVirtualFileSystem.h index d46a7d73c..b404f80d7 100644 --- a/src/rwe/vfs/CompositeVirtualFileSystem.h +++ b/src/rwe/vfs/CompositeVirtualFileSystem.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include #include @@ -38,6 +38,6 @@ namespace rwe }; - void addToVfs(CompositeVirtualFileSystem& vfs, const boost::filesystem::path& searchPath); - CompositeVirtualFileSystem constructVfs(const boost::filesystem::path& searchPath); + void addToVfs(CompositeVirtualFileSystem& vfs, const std::filesystem::path& searchPath); + CompositeVirtualFileSystem constructVfs(const std::filesystem::path& searchPath); } diff --git a/src/rwe/vfs/DirectoryFileSystem.cpp b/src/rwe/vfs/DirectoryFileSystem.cpp index 16659d77a..4ea316c18 100644 --- a/src/rwe/vfs/DirectoryFileSystem.cpp +++ b/src/rwe/vfs/DirectoryFileSystem.cpp @@ -5,7 +5,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; namespace rwe { @@ -21,8 +21,8 @@ namespace rwe * If we naively try to follow file references on a case-sensitive filesystem * we may fail to find the file we wanted. */ - std::optional findPathCaseInsensitive( - const boost::filesystem::path& root, const boost::filesystem::path& path) + std::optional findPathCaseInsensitive( + const std::filesystem::path& root, const std::filesystem::path& path) { fs::path basePath; @@ -53,7 +53,7 @@ namespace rwe { } - DirectoryFileSystem::DirectoryFileSystem(const boost::filesystem::path& path) + DirectoryFileSystem::DirectoryFileSystem(const std::filesystem::path& path) : path(path), pathString(this->path.string()) { } @@ -142,7 +142,7 @@ namespace rwe for (; it != end; ++it) { const auto& e = *it; - if (e.status().type() == fs::file_type::directory_file) + if (e.status().type() == fs::file_type::directory) { // recurse into directory auto innerDirectoryName = e.path().filename(); diff --git a/src/rwe/vfs/DirectoryFileSystem.h b/src/rwe/vfs/DirectoryFileSystem.h index 618369185..74bbf56e6 100644 --- a/src/rwe/vfs/DirectoryFileSystem.h +++ b/src/rwe/vfs/DirectoryFileSystem.h @@ -2,19 +2,19 @@ #include -#include +#include namespace rwe { class DirectoryFileSystem final : public LeafVirtualFileSystem { private: - boost::filesystem::path path; + std::filesystem::path path; std::string pathString; public: explicit DirectoryFileSystem(const std::string& path); - explicit DirectoryFileSystem(const boost::filesystem::path& path); + explicit DirectoryFileSystem(const std::filesystem::path& path); public: const std::string& getPath() const override; diff --git a/src/tnt_test.cpp b/src/tnt_test.cpp index 6a13c8b45..f43ec81dd 100644 --- a/src/tnt_test.cpp +++ b/src/tnt_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -7,7 +7,7 @@ #include #include -namespace fs = boost::filesystem; +namespace fs = std::filesystem; int featuresCommand(const std::string& tntPath) { From 2d9b597c4f78b27f05d727c9721fa0083a9126e6 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 13:42:11 -0700 Subject: [PATCH 09/32] Replace boost::crc_32_type with standalone CRC32 implementation Use a standard table-driven CRC32 with polynomial 0xEDB88320, identical output to Boost.CRC and zlib. Add tests with known reference values to verify correctness. --- src/rwe/network_util.cpp | 37 ++++++++++++++++++++++++++++++----- src/rwe/network_util.test.cpp | 21 ++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/src/rwe/network_util.cpp b/src/rwe/network_util.cpp index dbbaf76ec..1ee28a6a7 100644 --- a/src/rwe/network_util.cpp +++ b/src/rwe/network_util.cpp @@ -1,8 +1,33 @@ #include "network_util.h" -#include +#include namespace rwe { + // CRC32 lookup table (polynomial 0xEDB88320, same as Boost.CRC / zlib) + static uint32_t makeCrc32Entry(uint32_t index) + { + uint32_t crc = index; + for (int j = 0; j < 8; ++j) + { + crc = (crc >> 1) ^ (0xEDB88320u & (-(crc & 1u))); + } + return crc; + } + + struct Crc32Table + { + uint32_t entries[256]; + Crc32Table() + { + for (uint32_t i = 0; i < 256; ++i) + { + entries[i] = makeCrc32Entry(i); + } + } + }; + + static const Crc32Table crc32Table; + float ema(float val, float average, float alpha) { return (alpha * val) + ((1.0f - alpha) * average); @@ -23,10 +48,12 @@ namespace rwe } unsigned int computeCrc(const char* buffer, unsigned int size) { - // throw in a CRC to verify the message - boost::crc_32_type crc; - crc.process_bytes(buffer, size); - return crc.checksum(); + uint32_t crc = 0xFFFFFFFFu; + for (unsigned int i = 0; i < size; ++i) + { + crc = crc32Table.entries[(crc ^ static_cast(buffer[i])) & 0xFFu] ^ (crc >> 8u); + } + return crc ^ 0xFFFFFFFFu; } } diff --git a/src/rwe/network_util.test.cpp b/src/rwe/network_util.test.cpp index 6a48f28c8..24962fcad 100644 --- a/src/rwe/network_util.test.cpp +++ b/src/rwe/network_util.test.cpp @@ -23,4 +23,25 @@ namespace rwe RC_ASSERT(result == i); }); } + + TEST_CASE("computeCrc") + { + SECTION("empty input") + { + REQUIRE(computeCrc("", 0) == 0x00000000u); + } + + SECTION("known CRC32 values") + { + // CRC32 of "123456789" is 0xCBF43926 + const char* input = "123456789"; + REQUIRE(computeCrc(input, 9) == 0xCBF43926u); + } + + SECTION("single byte") + { + // CRC32 of "a" is 0xE8B7BE43 + REQUIRE(computeCrc("a", 1) == 0xE8B7BE43u); + } + } } From 6c9b2f4725a030f7e03a2a0e4513314883df9755 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 13:53:12 -0700 Subject: [PATCH 10/32] Replace boost::hash_combine and boost::hash with std::hash and inline helper Add rwe::hashCombine in util/hash_combine.h, using the same algorithm as Boost (golden ratio constant 0x9e3779b9). Replace all boost::hash and boost::hash_combine usage in Point, DiscreteRect, GameMediaDatabase, and test files. Remove unused hash_value ADL function from Point. Add missing includes exposed by removing transitive Boost headers. --- src/rwe/collections/MinHeap.test.cpp | 9 +++++---- src/rwe/game/GameMediaDatabase.h | 7 +++---- src/rwe/grid/DiscreteRect.h | 11 ++++++----- src/rwe/grid/EightWayDirection.cpp | 3 +++ src/rwe/grid/Point.cpp | 5 ----- src/rwe/grid/Point.h | 12 ++++-------- src/rwe/util/hash_combine.h | 14 ++++++++++++++ src/texture_test.cpp | 6 ++++-- 8 files changed, 39 insertions(+), 28 deletions(-) create mode 100644 src/rwe/util/hash_combine.h diff --git a/src/rwe/collections/MinHeap.test.cpp b/src/rwe/collections/MinHeap.test.cpp index 97b0c489b..485281a3a 100644 --- a/src/rwe/collections/MinHeap.test.cpp +++ b/src/rwe/collections/MinHeap.test.cpp @@ -10,10 +10,8 @@ namespace std } } -#include #include #include -#include #include #include #include @@ -25,9 +23,12 @@ namespace std template struct hash> { - std::size_t operator()(const std::pair& f) const noexcept + std::size_t operator()(const std::pair& p) const noexcept { - return boost::hash>()(f); + std::size_t seed = 0; + rwe::hashCombine(seed, std::hash{}(p.first)); + rwe::hashCombine(seed, std::hash{}(p.second)); + return seed; } }; } diff --git a/src/rwe/game/GameMediaDatabase.h b/src/rwe/game/GameMediaDatabase.h index 84e350496..14e10bdfd 100644 --- a/src/rwe/game/GameMediaDatabase.h +++ b/src/rwe/game/GameMediaDatabase.h @@ -1,9 +1,8 @@ #pragma once -#include -#include #include #include +#include #include #include #include @@ -44,8 +43,8 @@ namespace rwe std::size_t operator()(const std::pair& key) const { std::size_t seed = 0; - boost::hash_combine(seed, toUpper(key.first)); - boost::hash_combine(seed, toUpper(key.second)); + hashCombine(seed, std::hash{}(toUpper(key.first))); + hashCombine(seed, std::hash{}(toUpper(key.second))); return seed; } }; diff --git a/src/rwe/grid/DiscreteRect.h b/src/rwe/grid/DiscreteRect.h index 22a1e6707..ae77d6f80 100644 --- a/src/rwe/grid/DiscreteRect.h +++ b/src/rwe/grid/DiscreteRect.h @@ -1,7 +1,8 @@ #pragma once -#include +#include #include +#include #include #include @@ -111,10 +112,10 @@ namespace std std::size_t operator()(const rwe::DiscreteRect& r) const noexcept { std::size_t seed = 0; - boost::hash_combine(seed, r.x); - boost::hash_combine(seed, r.y); - boost::hash_combine(seed, r.width); - boost::hash_combine(seed, r.height); + rwe::hashCombine(seed, std::hash{}(r.x)); + rwe::hashCombine(seed, std::hash{}(r.y)); + rwe::hashCombine(seed, std::hash{}(r.width)); + rwe::hashCombine(seed, std::hash{}(r.height)); return seed; } }; diff --git a/src/rwe/grid/EightWayDirection.cpp b/src/rwe/grid/EightWayDirection.cpp index e8d6d51c0..6c35c36b7 100644 --- a/src/rwe/grid/EightWayDirection.cpp +++ b/src/rwe/grid/EightWayDirection.cpp @@ -1,5 +1,8 @@ #include "EightWayDirection.h" +#include +#include #include +#include namespace rwe { diff --git a/src/rwe/grid/Point.cpp b/src/rwe/grid/Point.cpp index 0ffc0957f..0c1763590 100644 --- a/src/rwe/grid/Point.cpp +++ b/src/rwe/grid/Point.cpp @@ -41,11 +41,6 @@ namespace rwe return *this; } - std::size_t hash_value(const Point& p) - { - return std::hash()(p); - } - int Point::maxSingleDimensionDistance(const Point& rhs) const { auto delta = *this - rhs; diff --git a/src/rwe/grid/Point.h b/src/rwe/grid/Point.h index 891c7e14f..b2cef47ca 100644 --- a/src/rwe/grid/Point.h +++ b/src/rwe/grid/Point.h @@ -1,6 +1,7 @@ #pragma once -#include +#include +#include namespace rwe { @@ -37,14 +38,9 @@ namespace std std::size_t operator()(const rwe::Point& v) const noexcept { std::size_t seed = 0; - boost::hash_combine(seed, v.x); - boost::hash_combine(seed, v.y); + rwe::hashCombine(seed, std::hash{}(v.x)); + rwe::hashCombine(seed, std::hash{}(v.y)); return seed; } }; } - -namespace rwe -{ - std::size_t hash_value(const Point& p); -} diff --git a/src/rwe/util/hash_combine.h b/src/rwe/util/hash_combine.h new file mode 100644 index 000000000..9dee56d67 --- /dev/null +++ b/src/rwe/util/hash_combine.h @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace rwe +{ + // boost::hash_combine algorithm. The magic constant is the golden + // ratio's fixed-point representation (2^32 / phi), which spreads + // sequential inputs across the hash space to minimize collisions. + inline void hashCombine(std::size_t& seed, std::size_t value) + { + seed ^= value + 0x9e3779b9 + (seed << 6) + (seed >> 2); + } +} diff --git a/src/texture_test.cpp b/src/texture_test.cpp index 5b957df26..2c2e91941 100644 --- a/src/texture_test.cpp +++ b/src/texture_test.cpp @@ -1,4 +1,3 @@ -#include #include #include #include @@ -26,7 +25,10 @@ namespace std { std::size_t operator()(const rwe::FrameId& f) const noexcept { - return boost::hash()(f); + std::size_t seed = 0; + rwe::hashCombine(seed, std::hash{}(f.first)); + rwe::hashCombine(seed, std::hash{}(f.second)); + return seed; } }; } From 1eaab2697123d336376d9c3c0f9d9728bc4df195 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 14:04:34 -0700 Subject: [PATCH 11/32] Replace boost::interprocess::bufferstream with SpanStream Add rwe::SpanStream, a lightweight std::istream wrapper over a const char* buffer with full seek support. Replaces all boost::interprocess::bufferstream usage across the codebase. Add tests covering sequential reads, seeking, boundary conditions, and binary data. --- src/bridge.cpp | 6 +- src/rwe/LoadingScene.cpp | 4 +- src/rwe/LoadingScene_util.cpp | 4 +- src/rwe/MeshService.cpp | 6 +- src/rwe/TextureService.cpp | 8 +-- src/rwe/atlas_util.cpp | 6 +- src/rwe/util/SpanStream.h | 74 +++++++++++++++++++++ src/rwe/util/SpanStream.test.cpp | 108 +++++++++++++++++++++++++++++++ src/texture_test.cpp | 4 +- 9 files changed, 201 insertions(+), 19 deletions(-) create mode 100644 src/rwe/util/SpanStream.h create mode 100644 src/rwe/util/SpanStream.test.cpp diff --git a/src/bridge.cpp b/src/bridge.cpp index 77115dfa3..9d3db6e64 100644 --- a/src/bridge.cpp +++ b/src/bridge.cpp @@ -1,6 +1,6 @@ #include #include -#include +#include #include #include #include @@ -123,7 +123,7 @@ namespace rwe throw std::runtime_error("map tnt not found!"); } - boost::interprocess::bufferstream tntStream(tntData->data(), tntData->size()); + rwe::SpanStream tntStream(tntData->data(), tntData->size()); TntArchive tnt(&tntStream); auto minimap = tnt.readMinimap(); @@ -161,7 +161,7 @@ std::optional getMinimap(rwe::CompositeVirtualFileSystem& vfs, cons return std::nullopt; } - boost::interprocess::bufferstream paletteBuffer(paletteBytes->data(), paletteBytes->size()); + rwe::SpanStream paletteBuffer(paletteBytes->data(), paletteBytes->size()); rwe::RgbPixel palette[256]; rwe::loadPalette(paletteBuffer, palette); diff --git a/src/rwe/LoadingScene.cpp b/src/rwe/LoadingScene.cpp index d633783ba..a49d8ca0b 100644 --- a/src/rwe/LoadingScene.cpp +++ b/src/rwe/LoadingScene.cpp @@ -1,6 +1,6 @@ #include "LoadingScene.h" #include -#include +#include #include #include #include @@ -333,7 +333,7 @@ namespace rwe throw std::runtime_error("Failed to load map bytes"); } - boost::interprocess::bufferstream tntStream(tntBytes->data(), tntBytes->size()); + rwe::SpanStream tntStream(tntBytes->data(), tntBytes->size()); TntArchive tnt(&tntStream); auto tileTextures = getTileTextures(tnt); diff --git a/src/rwe/LoadingScene_util.cpp b/src/rwe/LoadingScene_util.cpp index d6181c1a5..c24b758ed 100644 --- a/src/rwe/LoadingScene_util.cpp +++ b/src/rwe/LoadingScene_util.cpp @@ -1,7 +1,7 @@ #include "LoadingScene_util.h" #include -#include +#include #include namespace rwe @@ -31,7 +31,7 @@ namespace rwe throw std::runtime_error("File in listing could not be read: " + scriptName); } - boost::interprocess::bufferstream s(bytes->data(), bytes->size()); + rwe::SpanStream s(bytes->data(), bytes->size()); auto cob = parseCob(s); auto scriptNameWithoutExtension = scriptName.substr(0, scriptName.size() - 4); diff --git a/src/rwe/MeshService.cpp b/src/rwe/MeshService.cpp index 6ae0b5900..ce57c11e0 100644 --- a/src/rwe/MeshService.cpp +++ b/src/rwe/MeshService.cpp @@ -1,5 +1,5 @@ #include "MeshService.h" -#include +#include #include #include #include @@ -30,7 +30,7 @@ namespace rwe throw std::runtime_error("Failed to load object bytes: " + name); } - boost::interprocess::bufferstream s(bytes->data(), bytes->size()); + rwe::SpanStream s(bytes->data(), bytes->size()); auto objects = parse3doObjects(s, s.tellg()); assert(objects.size() == 1); auto selectionMesh = selectionMeshFrom3do(*graphics, objects.front()); @@ -53,7 +53,7 @@ namespace rwe throw std::runtime_error("Failed to load object bytes: " + name); } - boost::interprocess::bufferstream s(bytes->data(), bytes->size()); + rwe::SpanStream s(bytes->data(), bytes->size()); auto objects = parse3doObjects(s, s.tellg()); assert(objects.size() == 1); diff --git a/src/rwe/TextureService.cpp b/src/rwe/TextureService.cpp index 3c2d18947..694c0d09f 100644 --- a/src/rwe/TextureService.cpp +++ b/src/rwe/TextureService.cpp @@ -1,6 +1,6 @@ #include "TextureService.h" #include -#include +#include #include #include #include @@ -111,7 +111,7 @@ namespace rwe return std::nullopt; } - boost::interprocess::bufferstream gafStream(gafBytes->data(), gafBytes->size()); + rwe::SpanStream gafStream(gafBytes->data(), gafBytes->size()); GafArchive gafArchive(&gafStream); auto gafEntry = gafArchive.findEntry(normEntryName); @@ -240,7 +240,7 @@ namespace rwe throw std::runtime_error("map tnt not found!"); } - boost::interprocess::bufferstream tntStream(tntData->data(), tntData->size()); + rwe::SpanStream tntStream(tntData->data(), tntData->size()); TntArchive tnt(&tntStream); auto minimap = tnt.readMinimap(); @@ -275,7 +275,7 @@ namespace rwe throw std::runtime_error("font not found!"); } - boost::interprocess::bufferstream fntStream(fntBytes->data(), fntBytes->size()); + rwe::SpanStream fntStream(fntBytes->data(), fntBytes->size()); FntArchive fnt(&fntStream); auto series = std::make_shared(); diff --git a/src/rwe/atlas_util.cpp b/src/rwe/atlas_util.cpp index 8dd01e1a1..7c8af841e 100644 --- a/src/rwe/atlas_util.cpp +++ b/src/rwe/atlas_util.cpp @@ -1,6 +1,6 @@ #include "atlas_util.h" #include -#include +#include #include #include #include @@ -95,7 +95,7 @@ namespace rwe throw std::runtime_error("textures/LOGOS.GAF could not be read"); } - boost::interprocess::bufferstream stream(bytes->data(), bytes->size()); + rwe::SpanStream stream(bytes->data(), bytes->size()); GafArchive gaf(&stream); std::vector>> entries; @@ -192,7 +192,7 @@ namespace rwe throw std::runtime_error("File in listing could not be read: " + gafName); } - boost::interprocess::bufferstream stream(bytes->data(), bytes->size()); + rwe::SpanStream stream(bytes->data(), bytes->size()); GafArchive gaf(&stream); diff --git a/src/rwe/util/SpanStream.h b/src/rwe/util/SpanStream.h new file mode 100644 index 000000000..eb5c37959 --- /dev/null +++ b/src/rwe/util/SpanStream.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +namespace rwe +{ + class SpanStreamBuf : public std::streambuf + { + public: + SpanStreamBuf() = default; + + SpanStreamBuf(const char* data, std::size_t size) + { + reset(data, size); + } + + void reset(const char* data, std::size_t size) + { + auto p = const_cast(data); + setg(p, p, p + size); + } + + protected: + std::streampos seekoff(std::streamoff off, std::ios_base::seekdir dir, std::ios_base::openmode which) override + { + if (!(which & std::ios_base::in)) + { + return std::streampos(-1); + } + + char* newPos; + switch (dir) + { + case std::ios_base::beg: + newPos = eback() + off; + break; + case std::ios_base::cur: + newPos = gptr() + off; + break; + case std::ios_base::end: + newPos = egptr() + off; + break; + default: + return std::streampos(-1); + } + + if (newPos < eback() || newPos > egptr()) + { + return std::streampos(-1); + } + + setg(eback(), newPos, egptr()); + return std::streampos(newPos - eback()); + } + + std::streampos seekpos(std::streampos pos, std::ios_base::openmode which) override + { + return seekoff(std::streamoff(pos), std::ios_base::beg, which); + } + }; + + class SpanStream : public std::istream + { + private: + SpanStreamBuf buf; + + public: + SpanStream(const char* data, std::size_t size) + : std::istream(nullptr), buf(data, size) + { + rdbuf(&buf); + } + }; +} diff --git a/src/rwe/util/SpanStream.test.cpp b/src/rwe/util/SpanStream.test.cpp new file mode 100644 index 000000000..efd27664f --- /dev/null +++ b/src/rwe/util/SpanStream.test.cpp @@ -0,0 +1,108 @@ +#include +#include +#include + +namespace rwe +{ + TEST_CASE("SpanStream") + { + const char data[] = "Hello, world!"; + auto size = sizeof(data) - 1; // exclude null terminator + + SECTION("reads data sequentially") + { + SpanStream stream(data, size); + char buf[5]; + stream.read(buf, 5); + REQUIRE(!stream.fail()); + REQUIRE(std::string(buf, 5) == "Hello"); + } + + SECTION("reports failure when reading past end") + { + SpanStream stream(data, size); + char buf[20]; + stream.read(buf, 20); + REQUIRE(stream.fail()); + } + + SECTION("tellg reports current position") + { + SpanStream stream(data, size); + REQUIRE(stream.tellg() == 0); + char buf[5]; + stream.read(buf, 5); + REQUIRE(stream.tellg() == 5); + } + + SECTION("seekg to absolute position") + { + SpanStream stream(data, size); + stream.seekg(7); + REQUIRE(!stream.fail()); + char buf[5]; + stream.read(buf, 5); + REQUIRE(!stream.fail()); + REQUIRE(std::string(buf, 5) == "world"); + } + + SECTION("seekg relative to current position") + { + SpanStream stream(data, size); + char buf[5]; + stream.read(buf, 5); + stream.seekg(2, std::ios_base::cur); + REQUIRE(!stream.fail()); + stream.read(buf, 5); + REQUIRE(!stream.fail()); + REQUIRE(std::string(buf, 5) == "world"); + } + + SECTION("seekg relative to end") + { + SpanStream stream(data, size); + stream.seekg(-6, std::ios_base::end); + REQUIRE(!stream.fail()); + char buf[5]; + stream.read(buf, 5); + REQUIRE(!stream.fail()); + REQUIRE(std::string(buf, 5) == "orld!"); + } + + SECTION("seekg to beginning after reading") + { + SpanStream stream(data, size); + char buf[5]; + stream.read(buf, 5); + stream.seekg(0); + REQUIRE(!stream.fail()); + stream.read(buf, 5); + REQUIRE(std::string(buf, 5) == "Hello"); + } + + SECTION("seekg past end fails") + { + SpanStream stream(data, size); + stream.seekg(100); + REQUIRE(stream.fail()); + } + + SECTION("seekg before beginning fails") + { + SpanStream stream(data, size); + stream.seekg(-1); + REQUIRE(stream.fail()); + } + + SECTION("works with binary data containing null bytes") + { + const char binData[] = {'\x00', '\x01', '\x02', '\x03', '\x04'}; + SpanStream stream(binData, 5); + stream.seekg(2); + char val; + stream.read(&val, 1); + REQUIRE(!stream.fail()); + REQUIRE(val == '\x02'); + } + } +} diff --git a/src/texture_test.cpp b/src/texture_test.cpp index 2c2e91941..d1854063c 100644 --- a/src/texture_test.cpp +++ b/src/texture_test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include @@ -115,7 +115,7 @@ namespace rwe throw std::runtime_error("File in listing could not be read: " + gafName); } - boost::interprocess::bufferstream stream(bytes->data(), bytes->size()); + rwe::SpanStream stream(bytes->data(), bytes->size()); GafArchive gaf(&stream); for (const auto& e : gaf.entries()) From 2fad5172ac81f609ca55749e390f050a32d8edc8 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Sat, 21 Mar 2026 14:23:16 -0700 Subject: [PATCH 12/32] Replace boost::program_options with simple OpaqueArgs parser Also fixes a bug where --dir-features was mapped to pathMapping.downloads instead of pathMapping.features. --- CMakeLists.txt | 1 + src/main.cpp | 134 ++++++++----------- src/rwe/util/OpaqueArgs.h | 186 +++++++++++++++++++++++++++ src/rwe/util/OpaqueArgs.test.cpp | 212 +++++++++++++++++++++++++++++++ 4 files changed, 451 insertions(+), 82 deletions(-) create mode 100644 src/rwe/util/OpaqueArgs.h create mode 100644 src/rwe/util/OpaqueArgs.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f821231a7..16ea974a9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -854,6 +854,7 @@ set(TEST_FILES src/rwe/sim/SimVector.test.cpp src/rwe/sim/UnitState_util.test.cpp src/rwe/sim/util.test.cpp + src/rwe/util/OpaqueArgs.test.cpp src/rwe/util/Result.test.cpp src/rwe/util/rwe_string.test.cpp ) diff --git a/src/main.cpp b/src/main.cpp index 4620837ac..3ce003641 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,4 @@ #include -#include #include #include #include @@ -27,12 +26,12 @@ #include #include #include +#include #include #include #include namespace fs = std::filesystem; -namespace po = boost::program_options; namespace rwe { @@ -520,78 +519,49 @@ int main(int argc, char* argv[]) fs::path imGuiIniFilePath(*localDataPath); imGuiIniFilePath /= "imgui.ini"; - po::options_description desc("Allowed options"); - - // clang-format off - desc.add_options() - ("help", "produce help message") - ("log", po::value(), "Sets the log output file path") - ("state-log", po::value(), "Sets the output file for sim-state logs. This is a desync debugging feature.") - ("width", po::value()->default_value(800), "Sets the window width in pixels") - ("height", po::value()->default_value(600), "Sets the window height in pixels") - ("fullscreen", po::bool_switch(), "Starts the application in fullscreen mode") - ("interface-mode", po::value()->default_value("left-click"), "left-click or right-click") - ("data-path", po::value>(), "Sets the location(s) to search for game data") - ("map", po::value(), "If given, launches straight into a game on the given map") - ("port", po::value()->default_value("1337"), "Network port to bind to") - ("player", po::value>(), "type;side;color") - ("dir-ai", po::value()->default_value("ai"), "AI directory name") - ("dir-anims", po::value()->default_value("anims"), "anims directory name") - ("dir-bitmaps", po::value()->default_value("bitmaps"), "bitmaps directory name") - ("dir-camps", po::value()->default_value("camps"), "campaigns directory name") - ("dir-downloads", po::value()->default_value("downloads"), "downloads directory name") - ("dir-features", po::value()->default_value("features"), "features directory name") - ("dir-fonts", po::value()->default_value("fonts"), "fonts directory name") - ("dir-gamedata", po::value()->default_value("gamedata"), "gamedata directory name") - ("dir-guis", po::value()->default_value("guis"), "GUIs directory name") - ("dir-maps", po::value()->default_value("maps"), "maps directory name") - ("dir-objects3d", po::value()->default_value("objects3d"), "3D objects directory name") - ("dir-palettes", po::value()->default_value("palettes"), "palettes directory name") - ("dir-scripts", po::value()->default_value("scripts"), "scripts directory name") - ("dir-sounds", po::value()->default_value("sounds"), "sounds directory name") - ("dir-textures", po::value()->default_value("textures"), "textures directory name") - ("dir-unitpics", po::value()->default_value("unitpics"), "unitpics directory name") - ("dir-units", po::value()->default_value("units"), "units directory name") - ("dir-weapons", po::value()->default_value("weapons"), "weapons directory name"); - // clang-format on - - po::variables_map vm; - po::store(po::parse_command_line(argc, argv, desc), vm); - { - std::ifstream configFileStream(configFilePath.string(), std::ios::binary); - if (configFileStream.is_open()) - { - po::store(po::parse_config_file(configFileStream, desc), vm); - } - } - po::notify(vm); - - if (vm.count("help")) - { - std::cout << desc << std::endl; + rwe::OpaqueArgs args; + args.parse(argc, argv); + args.parseConfig(configFilePath.string()); + + if (args.isHelpRequested()) + { + std::cout << "Usage: rwe [options]\n" + << " --help Show this message\n" + << " --log Log output file path\n" + << " --state-log Sim-state log file (desync debugging)\n" + << " --width Window width (default: 800)\n" + << " --height Window height (default: 600)\n" + << " --fullscreen Start in fullscreen mode\n" + << " --interface-mode left-click or right-click (default: left-click)\n" + << " --data-path Game data search path (repeatable)\n" + << " --map Launch directly into a game on this map\n" + << " --port Network port (default: 1337)\n" + << " --player Player spec: name;type;side;color (repeatable)\n" + << " --dir- Override directory name for a data category\n" + << std::endl; return 0; } - auto logger = vm.count("log") ? createLogger(fs::path(vm["log"].as())) : createLoggerInDir(*localDataPath); + auto logger = args.contains("log") ? createLogger(fs::path(args.getString("log"))) : createLoggerInDir(*localDataPath); logger->set_level(spdlog::level::debug); logger->flush_on(spdlog::level::debug); // always flush try { rwe::GlobalConfig config; - config.leftClickInterfaceMode = vm["interface-mode"].as() != "right-click"; + config.leftClickInterfaceMode = args.getString("interface-mode", "left-click") != "right-click"; std::optional gameParameters; - if (vm.count("map")) + if (args.contains("map")) { - const auto& mapName = vm["map"].as(); - const auto& players = vm["player"].as>(); + const auto& mapName = args.getString("map"); + const auto& players = args.getMulti("player"); gameParameters = rwe::GameParameters{mapName, 0}; - if (vm.count("state-log")) + if (args.contains("state-log")) { - gameParameters->stateLogFile = vm["state-log"].as(); + gameParameters->stateLogFile = args.getString("state-log"); } - gameParameters->localNetworkPort = vm["port"].as(); + gameParameters->localNetworkPort = args.getString("port", "1337"); unsigned int playerIndex = 0; if (players.size() > 10) { @@ -606,38 +576,38 @@ int main(int argc, char* argv[]) std::vector gameDataPaths; - if (vm.count("data-path")) + auto dataPaths = args.getMulti("data-path"); + if (!dataPaths.empty()) { - const auto& paths = vm["data-path"].as>(); - gameDataPaths.insert(gameDataPaths.end(), paths.begin(), paths.end()); + gameDataPaths.insert(gameDataPaths.end(), dataPaths.begin(), dataPaths.end()); } else { gameDataPaths.emplace_back(*localDataPath) /= "Data"; } - auto screenWidth = vm["width"].as(); - auto screenHeight = vm["height"].as(); - auto fullscreen = vm["fullscreen"].as(); + auto screenWidth = args.getUint("width", 800); + auto screenHeight = args.getUint("height", 600); + auto fullscreen = args.getBool("fullscreen"); auto pathMapping = constructDefaultPathMapping(); - pathMapping.ai = vm["dir-ai"].as(); - pathMapping.anims = vm["dir-anims"].as(); - pathMapping.bitmaps = vm["dir-bitmaps"].as(); - pathMapping.camps = vm["dir-camps"].as(); - pathMapping.downloads = vm["dir-features"].as(); - pathMapping.fonts = vm["dir-fonts"].as(); - pathMapping.gamedata = vm["dir-gamedata"].as(); - pathMapping.guis = vm["dir-guis"].as(); - pathMapping.maps = vm["dir-maps"].as(); - pathMapping.objects3d = vm["dir-objects3d"].as(); - pathMapping.palettes = vm["dir-palettes"].as(); - pathMapping.scripts = vm["dir-scripts"].as(); - pathMapping.sounds = vm["dir-sounds"].as(); - pathMapping.textures = vm["dir-textures"].as(); - pathMapping.unitpics = vm["dir-unitpics"].as(); - pathMapping.units = vm["dir-units"].as(); - pathMapping.weapons = vm["dir-weapons"].as(); + pathMapping.ai = args.getString("dir-ai", "ai"); + pathMapping.anims = args.getString("dir-anims", "anims"); + pathMapping.bitmaps = args.getString("dir-bitmaps", "bitmaps"); + pathMapping.camps = args.getString("dir-camps", "camps"); + pathMapping.downloads = args.getString("dir-downloads", "downloads"); + pathMapping.fonts = args.getString("dir-fonts", "fonts"); + pathMapping.gamedata = args.getString("dir-gamedata", "gamedata"); + pathMapping.guis = args.getString("dir-guis", "guis"); + pathMapping.maps = args.getString("dir-maps", "maps"); + pathMapping.objects3d = args.getString("dir-objects3d", "objects3d"); + pathMapping.palettes = args.getString("dir-palettes", "palettes"); + pathMapping.scripts = args.getString("dir-scripts", "scripts"); + pathMapping.sounds = args.getString("dir-sounds", "sounds"); + pathMapping.textures = args.getString("dir-textures", "textures"); + pathMapping.unitpics = args.getString("dir-unitpics", "unitpics"); + pathMapping.units = args.getString("dir-units", "units"); + pathMapping.weapons = args.getString("dir-weapons", "weapons"); return rwe::run(*logger, gameDataPaths, pathMapping, gameParameters, screenWidth, screenHeight, fullscreen, imGuiIniFilePath.string(), config); } diff --git a/src/rwe/util/OpaqueArgs.h b/src/rwe/util/OpaqueArgs.h new file mode 100644 index 000000000..2d8131109 --- /dev/null +++ b/src/rwe/util/OpaqueArgs.h @@ -0,0 +1,186 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace rwe +{ + // Simple command-line and config-file argument parser. + // Supports --key value, --key=value, --flag (bool), and multi-value keys. + // Config files use key=value format, one per line; # comments are ignored. + // Command-line args take precedence over config file values. + class OpaqueArgs + { + private: + std::map> values; + bool helpRequested{false}; + + public: + void parse(int argc, char* argv[]) + { + for (int i = 1; i < argc; ++i) + { + std::string arg(argv[i]); + if (arg.size() < 3 || arg[0] != '-' || arg[1] != '-') + { + throw std::runtime_error("Unexpected argument: " + arg); + } + arg = arg.substr(2); + + if (arg == "help") + { + helpRequested = true; + continue; + } + + // --key=value form + auto eqPos = arg.find('='); + if (eqPos != std::string::npos) + { + auto key = arg.substr(0, eqPos); + auto val = arg.substr(eqPos + 1); + values[key].push_back(val); + continue; + } + + // --flag (no next arg or next arg starts with --) + if (i + 1 >= argc || (argv[i + 1][0] == '-' && argv[i + 1][1] == '-')) + { + values[arg].emplace_back("true"); + continue; + } + + // --key value form + ++i; + values[arg].push_back(argv[i]); + } + } + + void parseConfig(const std::string& filePath) + { + std::ifstream file(filePath); + if (!file.is_open()) + { + return; + } + parseConfig(file); + } + + void parseConfig(std::istream& stream) + { + std::string line; + while (std::getline(stream, line)) + { + // trim leading whitespace + auto start = line.find_first_not_of(" \t\r\n"); + if (start == std::string::npos) + { + continue; + } + line = line.substr(start); + + // skip comments and empty lines + if (line.empty() || line[0] == '#') + { + continue; + } + + auto eqPos = line.find('='); + if (eqPos == std::string::npos) + { + throw std::runtime_error("Invalid config line (no '='): " + line); + } + + auto key = line.substr(0, eqPos); + auto val = line.substr(eqPos + 1); + + // trim trailing whitespace from key + auto keyEnd = key.find_last_not_of(" \t"); + if (keyEnd != std::string::npos) + { + key = key.substr(0, keyEnd + 1); + } + + // trim leading whitespace from value + auto valStart = val.find_first_not_of(" \t"); + if (valStart != std::string::npos) + { + val = val.substr(valStart); + } + else + { + val = ""; + } + + // Only set from config if not already set by command line + if (values.find(key) == values.end()) + { + values[key].push_back(val); + } + } + } + + bool isHelpRequested() const + { + return helpRequested; + } + + bool contains(const std::string& key) const + { + return values.find(key) != values.end(); + } + + std::string getString(const std::string& key, const std::string& defaultValue) const + { + auto it = values.find(key); + if (it == values.end() || it->second.empty()) + { + return defaultValue; + } + return it->second.back(); + } + + std::string getString(const std::string& key) const + { + auto it = values.find(key); + if (it == values.end() || it->second.empty()) + { + throw std::runtime_error("Missing required argument: --" + key); + } + return it->second.back(); + } + + unsigned int getUint(const std::string& key, unsigned int defaultValue) const + { + auto it = values.find(key); + if (it == values.end() || it->second.empty()) + { + return defaultValue; + } + return static_cast(std::stoul(it->second.back())); + } + + bool getBool(const std::string& key) const + { + auto it = values.find(key); + if (it == values.end() || it->second.empty()) + { + return false; + } + return it->second.back() == "true" || it->second.back() == "1"; + } + + std::vector getMulti(const std::string& key) const + { + auto it = values.find(key); + if (it == values.end()) + { + return {}; + } + return it->second; + } + }; +} diff --git a/src/rwe/util/OpaqueArgs.test.cpp b/src/rwe/util/OpaqueArgs.test.cpp new file mode 100644 index 000000000..a10e01d43 --- /dev/null +++ b/src/rwe/util/OpaqueArgs.test.cpp @@ -0,0 +1,212 @@ +#include +#include +#include + +namespace rwe +{ + // Helper to build argc/argv from a vector of strings. + // The first element should be the program name. + struct ArgBuilder + { + std::vector storage; + std::vector ptrs; + + ArgBuilder(std::initializer_list args) : storage(args) + { + for (auto& s : storage) + { + ptrs.push_back(s.data()); + } + } + + int argc() const { return static_cast(ptrs.size()); } + char** argv() { return ptrs.data(); } + }; + + TEST_CASE("OpaqueArgs") + { + SECTION("parses --key value pairs") + { + ArgBuilder args{"rwe", "--width", "1024", "--height", "768"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getUint("width", 800) == 1024); + REQUIRE(oa.getUint("height", 600) == 768); + } + + SECTION("parses --key=value pairs") + { + ArgBuilder args{"rwe", "--width=1024", "--height=768"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getUint("width", 800) == 1024); + REQUIRE(oa.getUint("height", 600) == 768); + } + + SECTION("returns defaults for missing keys") + { + ArgBuilder args{"rwe"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getUint("width", 800) == 800); + REQUIRE(oa.getString("port", "1337") == "1337"); + REQUIRE(oa.getBool("fullscreen") == false); + } + + SECTION("parses bool flags") + { + ArgBuilder args{"rwe", "--fullscreen"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getBool("fullscreen") == true); + } + + SECTION("bool flag followed by another flag") + { + ArgBuilder args{"rwe", "--fullscreen", "--width", "1024"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getBool("fullscreen") == true); + REQUIRE(oa.getUint("width", 800) == 1024); + } + + SECTION("multi-value args accumulate") + { + ArgBuilder args{"rwe", "--data-path", "/foo", "--data-path", "/bar"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + auto paths = oa.getMulti("data-path"); + REQUIRE(paths.size() == 2); + REQUIRE(paths[0] == "/foo"); + REQUIRE(paths[1] == "/bar"); + } + + SECTION("--help sets help flag") + { + ArgBuilder args{"rwe", "--help"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.isHelpRequested()); + } + + SECTION("getString throws on missing required key") + { + ArgBuilder args{"rwe"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE_THROWS_AS(oa.getString("map"), std::runtime_error); + } + + SECTION("contains returns true for present keys") + { + ArgBuilder args{"rwe", "--map", "coast"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.contains("map")); + REQUIRE(!oa.contains("log")); + } + + SECTION("throws on unexpected argument without --") + { + ArgBuilder args{"rwe", "bogus"}; + OpaqueArgs oa; + REQUIRE_THROWS_AS(oa.parse(args.argc(), args.argv()), std::runtime_error); + } + + SECTION("parses config file") + { + std::istringstream config( + "width=1024\n" + "height=768\n" + "fullscreen=true\n"); + + OpaqueArgs oa; + ArgBuilder args{"rwe"}; + oa.parse(args.argc(), args.argv()); + oa.parseConfig(config); + REQUIRE(oa.getUint("width", 800) == 1024); + REQUIRE(oa.getUint("height", 600) == 768); + REQUIRE(oa.getBool("fullscreen") == true); + } + + SECTION("config file skips comments and blank lines") + { + std::istringstream config( + "# this is a comment\n" + "\n" + "width=1024\n" + " \n" + "# another comment\n"); + + OpaqueArgs oa; + ArgBuilder args{"rwe"}; + oa.parse(args.argc(), args.argv()); + oa.parseConfig(config); + REQUIRE(oa.getUint("width", 800) == 1024); + } + + SECTION("command line takes precedence over config file") + { + std::istringstream config("width=640\nheight=480\n"); + + ArgBuilder args{"rwe", "--width", "1024"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + oa.parseConfig(config); + REQUIRE(oa.getUint("width", 800) == 1024); + REQUIRE(oa.getUint("height", 600) == 480); + } + + SECTION("config file trims whitespace around key and value") + { + std::istringstream config(" width = 1024 \n"); + + OpaqueArgs oa; + ArgBuilder args{"rwe"}; + oa.parse(args.argc(), args.argv()); + oa.parseConfig(config); + REQUIRE(oa.getUint("width", 800) == 1024); + } + + SECTION("config file throws on line without =") + { + std::istringstream config("badline\n"); + + OpaqueArgs oa; + ArgBuilder args{"rwe"}; + oa.parse(args.argc(), args.argv()); + REQUIRE_THROWS_AS(oa.parseConfig(config), std::runtime_error); + } + + SECTION("player args accumulate via multi") + { + ArgBuilder args{"rwe", "--map", "coast", + "--player", "Player1;human;arm;blue", + "--player", "Player2;computer;core;red"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + auto players = oa.getMulti("player"); + REQUIRE(players.size() == 2); + REQUIRE(players[0] == "Player1;human;arm;blue"); + REQUIRE(players[1] == "Player2;computer;core;red"); + } + + SECTION("getString with default returns last value for multi-value key") + { + ArgBuilder args{"rwe", "--interface-mode", "left-click", "--interface-mode", "right-click"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getString("interface-mode", "left-click") == "right-click"); + } + + SECTION("all dir-* options work") + { + ArgBuilder args{"rwe", "--dir-ai", "myai", "--dir-maps", "mymaps"}; + OpaqueArgs oa; + oa.parse(args.argc(), args.argv()); + REQUIRE(oa.getString("dir-ai", "ai") == "myai"); + REQUIRE(oa.getString("dir-maps", "maps") == "mymaps"); + REQUIRE(oa.getString("dir-sounds", "sounds") == "sounds"); + } + } +} From 71c4d2b2fb0fb235389641ea039b716ce1dad346 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:08:22 -0700 Subject: [PATCH 13/32] Replace spdlog with SimpleLogger, upgrade to C++20 Replace the vendored spdlog 0.14.0 (which was incompatible with C++20) with a minimal SimpleLogger using LOG_XXX << stream macros. Log level is controlled at compile time via RWE_LOG_LEVEL. Also upgrades the C++ standard from C++17 to C++20 (gcc/clang and MSVC). --- CMakeLists.txt | 12 +- src/main.cpp | 82 ++++++----- src/rwe/LoadingNetworkService.cpp | 34 ++--- src/rwe/MainMenuScene.cpp | 10 +- src/rwe/game/GameNetworkService.cpp | 48 +++---- src/rwe/game/GameScene.cpp | 6 +- src/rwe/pathfinding/AStarPathFinder.h | 6 +- src/rwe/util/SimpleLogger.h | 200 ++++++++++++++++++++++++++ src/rwe/util/SimpleLogger.test.cpp | 171 ++++++++++++++++++++++ 9 files changed, 470 insertions(+), 99 deletions(-) create mode 100644 src/rwe/util/SimpleLogger.h create mode 100644 src/rwe/util/SimpleLogger.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 16ea974a9..ca1c12129 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,10 @@ cmake_minimum_required(VERSION 3.11) +# Generate compile_commands.json for clangd-based editor integration +# (VS Code, Zed, etc.). The .clangd file in the repo root points +# editors to look for it in the build/ directory. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Type of build" FORCE) message(STATUS "No build type specified, using default: ${CMAKE_BUILD_TYPE}") @@ -74,7 +79,7 @@ endfunction() set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/Modules") if(NOT MSVC) - set(CMAKE_CXX_STANDARD 17) + set(CMAKE_CXX_STANDARD 20) endif() find_package(OpenGL REQUIRED) @@ -625,8 +630,7 @@ endif() add_library(librwe STATIC ${SOURCE_FILES} ${PROTO_SOURCE_FILES} ${PROTO_HEADER_FILES}) set_target_properties(librwe PROPERTIES PREFIX "") if(MSVC) - target_compile_options(librwe PUBLIC "/std:c++17" "/EHsc") - add_definitions(-D_SILENCE_ALL_CXX17_DEPRECATION_WARNINGS) + target_compile_options(librwe PUBLIC "/std:c++20" "/EHsc") else() target_compile_options(librwe PUBLIC "-Wall" "-Wextra") endif() @@ -642,7 +646,6 @@ if(WIN32) endif() target_include_directories(librwe PUBLIC "libs/utfcpp/source") -target_include_directories(librwe PUBLIC "libs/spdlog/include") target_link_libraries(librwe OpenGL::GL) @@ -856,6 +859,7 @@ set(TEST_FILES src/rwe/sim/util.test.cpp src/rwe/util/OpaqueArgs.test.cpp src/rwe/util/Result.test.cpp + src/rwe/util/SimpleLogger.test.cpp src/rwe/util/rwe_string.test.cpp ) diff --git a/src/main.cpp b/src/main.cpp index 3ce003641..dbbb841f0 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -28,8 +28,8 @@ #include #include #include +#include #include -#include namespace fs = std::filesystem; @@ -78,13 +78,12 @@ namespace rwe } } - Result createOpenGlContext(SdlContext* sdlContext, SDL_Window* window, spdlog::logger& logger, const OpenGlVersionInfo& requiredVersion) + Result createOpenGlContext(SdlContext* sdlContext, SDL_Window* window, const OpenGlVersionInfo& requiredVersion) { - logger.info( - "Requesting OpenGL version {0}.{1}, {2} profile", - requiredVersion.version.majorVersion, - requiredVersion.version.minorVersion, - getOpenGlProfileName(requiredVersion.profile)); + LOG_INFO << "Requesting OpenGL version " + << requiredVersion.version.majorVersion << "." + << requiredVersion.version.minorVersion << ", " + << getOpenGlProfileName(requiredVersion.profile) << " profile"; if (sdlContext->glSetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, requiredVersion.version.majorVersion) != 0) { @@ -119,14 +118,14 @@ namespace rwe return Ok(std::move(glContext)); }; - int run(spdlog::logger& logger, const std::vector& searchPath, const PathMapping& pathMapping, const std::optional& gameParameters, unsigned int desiredWindowWidth, unsigned int desiredWindowHeight, bool fullscreen, const std::string& imGuiIniPath, GlobalConfig& globalConfig) + int run(const std::vector& searchPath, const PathMapping& pathMapping, const std::optional& gameParameters, unsigned int desiredWindowWidth, unsigned int desiredWindowHeight, bool fullscreen, const std::string& imGuiIniPath, GlobalConfig& globalConfig) { - logger.info(ProjectNameVersion); - logger.info("Current directory: {0}", fs::current_path().string()); + LOG_INFO << ProjectNameVersion; + LOG_INFO << "Current directory: " << fs::current_path().string(); TimeService timeService(getTimestamp()); - logger.info("Initializing SDL"); + LOG_INFO << "Initializing SDL"; SdlContextManager sdlManager; // Set a reasonable number of audio channels @@ -190,13 +189,13 @@ namespace rwe sdlContext->getWindowSize(window.get(), &windowWidth, &windowHeight); Viewport viewport(0, 0, windowWidth, windowHeight); - logger.info("Initializing OpenGL context"); + LOG_INFO << "Initializing OpenGL context"; - auto glContextResult = createOpenGlContext(sdlContext, window.get(), logger, OpenGlVersionInfo(3, 2, OpenGlProfile::Core)); + auto glContextResult = createOpenGlContext(sdlContext, window.get(), OpenGlVersionInfo(3, 2, OpenGlProfile::Core)); if (!glContextResult) { - logger.error("Failed to create preferred OpenGL context: {0}", glContextResult.getErr()); - glContextResult = createOpenGlContext(sdlContext, window.get(), logger, OpenGlVersionInfo(3, 0, OpenGlProfile::Compatibility)); + LOG_ERROR << "Failed to create preferred OpenGL context: " << glContextResult.getErr(); + glContextResult = createOpenGlContext(sdlContext, window.get(), OpenGlVersionInfo(3, 0, OpenGlProfile::Compatibility)); if (!glContextResult) { throw std::runtime_error(glContextResult.getErr()); @@ -212,29 +211,29 @@ namespace rwe doGlewInit(); // log opengl context info - logger.info("OpenGL version: {0}", glGetString(GL_VERSION)); - logger.info("OpenGL vendor: {0}", glGetString(GL_VENDOR)); - logger.info("OpenGL renderer: {0}", glGetString(GL_RENDERER)); - logger.info("OpenGL shading language version: {0}", glGetString(GL_SHADING_LANGUAGE_VERSION)); - logger.debug("OpenGL extensions:"); + LOG_INFO << "OpenGL version: " << glGetString(GL_VERSION); + LOG_INFO << "OpenGL vendor: " << glGetString(GL_VENDOR); + LOG_INFO << "OpenGL renderer: " << glGetString(GL_RENDERER); + LOG_INFO << "OpenGL shading language version: " << glGetString(GL_SHADING_LANGUAGE_VERSION); + LOG_DEBUG << "OpenGL extensions:"; int openGlExtensionCount; glGetIntegerv(GL_NUM_EXTENSIONS, &openGlExtensionCount); for (int i = 0; i < openGlExtensionCount; ++i) { - logger.debug(" {0}", glGetStringi(GL_EXTENSIONS, i)); + LOG_DEBUG << " " << glGetStringi(GL_EXTENSIONS, i); } - logger.info("Initializing Dear ImGui"); + LOG_INFO << "Initializing Dear ImGui"; ImGuiContext imGuiContext(imGuiIniPath, window.get(), glContext.get()); - logger.info("Initializing virtual file system"); + LOG_INFO << "Initializing virtual file system"; CompositeVirtualFileSystem vfs; for (const auto& path : searchPath) { addToVfs(vfs, path.string()); } - logger.info("Loading palette"); + LOG_INFO << "Loading palette"; auto paletteBytes = vfs.readFile("palettes/PALETTE.PAL"); if (!paletteBytes) { @@ -247,7 +246,7 @@ namespace rwe throw std::runtime_error("Couldn't read palette"); } - logger.info("Loading GUI palette"); + LOG_INFO << "Loading GUI palette"; auto guiPaletteBytes = vfs.readFile("palettes/GUIPAL.PAL"); if (!guiPaletteBytes) { @@ -260,7 +259,7 @@ namespace rwe throw std::runtime_error("Couldn't read GUI palette"); } - logger.info("Initializing services"); + LOG_INFO << "Initializing services"; GraphicsContext graphics; graphics.enableCulling(); graphics.enableBlending(); @@ -272,7 +271,7 @@ namespace rwe AudioService audioService(sdlContext, sdlManager.getSdlMixerContext(), &vfs); // load sound definitions - logger.info("Loading global sound definitions"); + LOG_INFO << "Loading global sound definitions"; auto allSoundBytes = vfs.readFile("gamedata/ALLSOUND.TDF"); if (!allSoundBytes) { @@ -282,7 +281,7 @@ namespace rwe std::string allSoundString(allSoundBytes->data(), allSoundBytes->size()); auto allSoundTdf = parseTdfFromString(allSoundString); - logger.info("Loading cursors"); + LOG_INFO << "Loading cursors"; Cursors cursors; cursors[*CursorType::Normal] = textureService.getGafEntry("anims/CURSORS.GAF", "cursornormal"); cursors[*CursorType::Select] = textureService.getGafEntry("anims/CURSORS.GAF", "cursorselect"); @@ -298,7 +297,7 @@ namespace rwe SceneManager sceneManager(sdlContext, window.get(), &graphics, &timeService, &imGuiContext, &cursor, &globalConfig, UiRenderService(&graphics, &shaders, &viewport), &viewport); - logger.info("Loading side data"); + LOG_INFO << "Loading side data"; auto sideDataBytes = vfs.readFile("gamedata/SIDEDATA.TDF"); if (!sideDataBytes) { @@ -334,7 +333,7 @@ namespace rwe if (gameParameters) { - logger.info("Launching into game on map: {0}", gameParameters->mapName); + LOG_INFO << "Launching into game on map: " << gameParameters->mapName; auto scene = std::make_unique( sceneContext, &allSoundTdf, @@ -344,7 +343,7 @@ namespace rwe } else { - logger.info("Launching into the main menu"); + LOG_INFO << "Launching into the main menu"; auto scene = std::make_unique( sceneContext, &allSoundTdf, @@ -353,10 +352,10 @@ namespace rwe sceneManager.setNextScene(std::shared_ptr(std::move(scene))); } - logger.info("Entering main loop"); + LOG_INFO << "Entering main loop"; sceneManager.execute(); - logger.info("Finished main loop, exiting"); + LOG_INFO << "Finished main loop, exiting"; return 0; } @@ -444,12 +443,12 @@ namespace rwe } } -auto createLogger(const fs::path& logFile) +std::shared_ptr createLogger(const fs::path& logFile) { - return spdlog::basic_logger_mt("rwe", logFile.string(), true); + return std::make_shared(logFile.string(), true); } -auto createLoggerInDir(const fs::path& logDir) +std::shared_ptr createLoggerInDir(const fs::path& logDir) { for (int i = 0; i < 3; ++i) { @@ -465,9 +464,9 @@ auto createLoggerInDir(const fs::path& logDir) try { - return spdlog::basic_logger_mt("rwe", logPath.string(), true); + return std::make_shared(logPath.string(), true); } - catch (const spdlog::spdlog_ex& ex) + catch (const std::exception&) { } } @@ -543,8 +542,7 @@ int main(int argc, char* argv[]) } auto logger = args.contains("log") ? createLogger(fs::path(args.getString("log"))) : createLoggerInDir(*localDataPath); - logger->set_level(spdlog::level::debug); - logger->flush_on(spdlog::level::debug); // always flush + rwe::setGlobalLogger(logger); try { @@ -609,11 +607,11 @@ int main(int argc, char* argv[]) pathMapping.units = args.getString("dir-units", "units"); pathMapping.weapons = args.getString("dir-weapons", "weapons"); - return rwe::run(*logger, gameDataPaths, pathMapping, gameParameters, screenWidth, screenHeight, fullscreen, imGuiIniFilePath.string(), config); + return rwe::run(gameDataPaths, pathMapping, gameParameters, screenWidth, screenHeight, fullscreen, imGuiIniFilePath.string(), config); } catch (const std::exception& e) { - logger->critical(e.what()); + LOG_CRITICAL << e.what(); throw; } } diff --git a/src/rwe/LoadingNetworkService.cpp b/src/rwe/LoadingNetworkService.cpp index 2a847889e..aa871f4ad 100644 --- a/src/rwe/LoadingNetworkService.cpp +++ b/src/rwe/LoadingNetworkService.cpp @@ -2,7 +2,7 @@ #include #include #include -#include +#include namespace rwe { @@ -59,7 +59,7 @@ namespace rwe { try { - spdlog::get("rwe")->debug("Opening listen socket on port {}", port); + LOG_DEBUG << "Opening listen socket on port " << port; auto endpoint = boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v6(), std::stoi(port)); socket.open(endpoint.protocol()); socket.bind(endpoint); @@ -72,7 +72,7 @@ namespace rwe } catch (const std::exception& e) { - spdlog::get("rwe")->error("Network thread died with error: {0}", e.what()); + LOG_ERROR << "Network thread died with error: " << e.what(); } } @@ -80,25 +80,25 @@ namespace rwe { if (error) { - spdlog::get("rwe")->error("Received network error from {0} port {1}: {2}", currentRemoteEndpoint.address().to_string(), currentRemoteEndpoint.port(), error.message()); + LOG_ERROR << "Received network error from " << currentRemoteEndpoint.address().to_string() << " port " << currentRemoteEndpoint.port() << ": " << error.message(); return; } std::scoped_lock lock(mutex); - spdlog::get("rwe")->debug("Received network message from {0} {1}, size {2}", currentRemoteEndpoint.address().to_string(), currentRemoteEndpoint.port(), bytesTransferred); + LOG_DEBUG << "Received network message from " << currentRemoteEndpoint.address().to_string() << " " << currentRemoteEndpoint.port() << ", size " << bytesTransferred; auto it = std::find_if(remoteEndpoints.begin(), remoteEndpoints.end(), [this](const auto& p) { return p.endpoint == currentRemoteEndpoint; }); if (it == remoteEndpoints.end()) { // message from some unknown address, ignore - spdlog::get("rwe")->debug("Sender was unknown, aborting"); + LOG_DEBUG << "Sender was unknown, aborting"; return; } - spdlog::get("rwe")->debug("Sender was recognised"); + LOG_DEBUG << "Sender was recognised"; if (bytesTransferred < 4) { - spdlog::get("rwe")->error("Received message is too short, ignoring"); + LOG_ERROR << "Received message is too short, ignoring"; return; } @@ -106,7 +106,7 @@ namespace rwe auto computedCrc = computeCrc(receiveBuffer.data(), bytesTransferred - 4); if (receivedCrc != computedCrc) { - spdlog::get("rwe")->error("Message CRC incorrect, ignoring"); + LOG_ERROR << "Message CRC incorrect, ignoring"; return; } @@ -115,7 +115,7 @@ namespace rwe if (!message.has_loading_status()) { - spdlog::get("rwe")->debug("Sender is already in game!"); + LOG_DEBUG << "Sender is already in game!"; it->status = Status::Ready; return; } @@ -123,11 +123,11 @@ namespace rwe switch (message.loading_status().status()) { case proto::LoadingStatusMessage_Status_Loading: - spdlog::get("rwe")->debug("Sender is loading"); + LOG_DEBUG << "Sender is loading"; it->status = Status::Loading; break; case proto::LoadingStatusMessage_Status_Ready: - spdlog::get("rwe")->debug("Sender is ready"); + LOG_DEBUG << "Sender is ready"; it->status = Status::Ready; break; default: @@ -138,7 +138,7 @@ namespace rwe void LoadingNetworkService::notifyStatus() { std::scoped_lock lock(mutex); - spdlog::get("rwe")->debug("Notifying peers about loading status"); + LOG_DEBUG << "Notifying peers about loading status"; proto::NetworkMessage outerMessage; @@ -147,11 +147,11 @@ namespace rwe switch (loadingStatus) { case Status::Loading: - spdlog::get("rwe")->debug("we are loading"); + LOG_DEBUG << "we are loading"; innerMessage.set_status(proto::LoadingStatusMessage_Status_Loading); break; case Status::Ready: - spdlog::get("rwe")->debug("we are ready"); + LOG_DEBUG << "we are ready"; innerMessage.set_status(proto::LoadingStatusMessage_Status_Ready); break; default: @@ -174,7 +174,7 @@ namespace rwe for (const auto& p : remoteEndpoints) { - spdlog::get("rwe")->debug("Sending notification to {0} {1}, size {2}", p.endpoint.address().to_string(), p.endpoint.port(), outerMessage.ByteSizeLong()); + LOG_DEBUG << "Sending notification to " << p.endpoint.address().to_string() << " " << p.endpoint.port() << ", size " << outerMessage.ByteSizeLong(); socket.send_to(boost::asio::buffer(sendBuffer.data(), messageSize + 4), p.endpoint); } } @@ -186,7 +186,7 @@ namespace rwe notifyTimer.async_wait([this](const boost::system::error_code& error) { if (error) { - spdlog::get("rwe")->error("Received error from network notify timer: {0}", error.message()); + LOG_ERROR << "Received error from network notify timer: " << error.message(); return; } notifyStatusLoop(); diff --git a/src/rwe/MainMenuScene.cpp b/src/rwe/MainMenuScene.cpp index 98b1798ad..a4f9f76ce 100644 --- a/src/rwe/MainMenuScene.cpp +++ b/src/rwe/MainMenuScene.cpp @@ -188,12 +188,18 @@ namespace rwe sceneContext.sceneManager->requestExit(); } - std::optional matchesPlayer(const std::string& format, const std::string& input) + std::optional matchesPlayer(const std::string& pattern, const std::string& input) { // FIXME: this should probably be a regex match instead of this crude brute-force search for (int i = 0; i < 10; ++i) { - if (input == fmt::format(format, i)) + std::string candidate = pattern; + auto pos = candidate.find("{0}"); + if (pos != std::string::npos) + { + candidate.replace(pos, 3, std::to_string(i)); + } + if (input == candidate) { return i; } diff --git a/src/rwe/game/GameNetworkService.cpp b/src/rwe/game/GameNetworkService.cpp index 1bdd6eef8..93c9fd0c1 100644 --- a/src/rwe/game/GameNetworkService.cpp +++ b/src/rwe/game/GameNetworkService.cpp @@ -8,7 +8,7 @@ #include #include #include -#include +#include #include namespace rwe @@ -112,7 +112,7 @@ namespace rwe } catch (const std::exception& e) { - spdlog::get("rwe")->error("Network thread died with error: {0}", e.what()); + LOG_ERROR << "Network thread died with error: " << e.what(); } } @@ -176,7 +176,7 @@ namespace rwe sendTimer.async_wait([this](const boost::system::error_code& error) { if (error) { - spdlog::get("rwe")->error("Boost error while waiting on timer: {}", error.message()); + LOG_ERROR << "Boost error while waiting on timer: " << error.message(); return; } @@ -195,7 +195,7 @@ namespace rwe void GameNetworkService::send(GameNetworkService::EndpointInfo& endpoint) { auto packetId = uniform_dist(gen); - spdlog::get("rwe")->debug("Sending packet ID {} to endpoint: {}:{}", packetId, endpoint.endpoint.address().to_string(), endpoint.endpoint.port()); + LOG_DEBUG << "Sending packet ID " << packetId << " to endpoint: " << endpoint.endpoint.address().to_string() << ":" << endpoint.endpoint.port(); std::chrono::milliseconds delay(0); auto sendTime = getTimestamp(); if (endpoint.lastReceiveTime) @@ -230,21 +230,21 @@ namespace rwe { if (error) { - spdlog::get("rwe")->error("Boost error on receive: {}", error.message()); + LOG_ERROR << "Boost error on receive: " << error.message(); return; } auto receiveTime = getTimestamp(); - spdlog::get("rwe")->debug("Received {} bytes from endpoint: {}:{}", receivedBytes, currentRemoteEndpoint.address().to_string(), currentRemoteEndpoint.port()); + LOG_DEBUG << "Received " << receivedBytes << " bytes from endpoint: " << currentRemoteEndpoint.address().to_string() << ":" << currentRemoteEndpoint.port(); if (receivedBytes == receiveBuffer.size()) { - spdlog::get("rwe")->warn("Received {} bytes, which filled the entire message buffer!!", receivedBytes); + LOG_WARN << "Received " << receivedBytes << " bytes, which filled the entire message buffer!!"; } if (receivedBytes < 4) { - spdlog::get("rwe")->error("Received message is too short, ignoring", receivedBytes); + LOG_ERROR << "Received message is too short (" << receivedBytes << " bytes), ignoring"; return; } @@ -252,7 +252,7 @@ namespace rwe if (endpointIt == endpoints.end()) { // message was from some unknown address, ignore it - spdlog::get("rwe")->debug("Unknown address, ignoring"); + LOG_DEBUG << "Unknown address, ignoring"; return; } @@ -260,7 +260,7 @@ namespace rwe auto computedCrc = computeCrc(receiveBuffer.data(), receivedBytes - 4); if (receivedCrc != computedCrc) { - spdlog::get("rwe")->error("Message CRC incorrect, ignoring"); + LOG_ERROR << "Message CRC incorrect, ignoring"; return; } @@ -269,7 +269,7 @@ namespace rwe if (!outerMessage.has_game_update()) { // message wasn't a game update, ignore it - spdlog::get("rwe")->debug("Not game update, ignoring"); + LOG_DEBUG << "Not game update, ignoring"; return; } @@ -277,23 +277,19 @@ namespace rwe const auto& message = outerMessage.game_update(); - spdlog::get("rwe")->debug("Packet received with ID {}", message.packet_id()); + LOG_DEBUG << "Packet received with ID " << message.packet_id(); if (message.player_id() != endpoint.playerId.value) { - spdlog::get("rwe")->error("Player {} endpoint sent wrong player ID: {}", endpoint.playerId.value, message.player_id()); + LOG_ERROR << "Player " << endpoint.playerId.value << " endpoint sent wrong player ID: " << message.player_id(); return; } - spdlog::get("rwe")->debug("Received ack to {0} and {1} commands starting at {2}", message.next_command_set_to_receive(), message.command_set_size(), message.next_command_set_to_send()); + LOG_DEBUG << "Received ack to " << message.next_command_set_to_receive() << " and " << message.command_set_size() << " commands starting at " << message.next_command_set_to_send(); SequenceNumber newNextCommandToSend(message.next_command_set_to_receive()); if (newNextCommandToSend.value > endpoint.nextCommandToSend.value + endpoint.sendBuffer.size()) { - spdlog::get("rwe")->error( - "Remote acked up to {0}, but we are at {1} and command buffer contains {2} elements", - newNextCommandToSend.value, - endpoint.nextCommandToSend.value, - endpoint.sendBuffer.size()); + LOG_ERROR << "Remote acked up to " << newNextCommandToSend.value << ", but we are at " << endpoint.nextCommandToSend.value << " and command buffer contains " << endpoint.sendBuffer.size() << " elements"; } while (newNextCommandToSend > endpoint.nextCommandToSend && !endpoint.sendBuffer.empty()) { @@ -313,19 +309,19 @@ namespace rwe roundTripTime = roundTripTime > ackDelay ? roundTripTime - ackDelay : std::chrono::milliseconds(0); auto rttMillis = std::chrono::duration_cast(roundTripTime).count(); endpoint.averageRoundTripTime = ema(rttMillis, endpoint.averageRoundTripTime, 0.1f); - spdlog::get("rwe")->debug("Average RTT: {0}ms", endpoint.averageRoundTripTime); + LOG_DEBUG << "Average RTT: " << endpoint.averageRoundTripTime << "ms"; } auto extraFrames = static_cast((endpoint.averageRoundTripTime / 2.0f) * SimTicksPerSecond / 1000.0f); endpoint.lastKnownSceneTime = std::make_pair(SceneTime(message.current_scene_time() + extraFrames), receiveTime); - spdlog::get("rwe")->debug("Estimated peer scene time: {0}", endpoint.lastKnownSceneTime->first.value); + LOG_DEBUG << "Estimated peer scene time: " << endpoint.lastKnownSceneTime->first.value; SequenceNumber firstCommandNumber(message.next_command_set_to_send()); if (firstCommandNumber > endpoint.nextCommandToReceive) { // message starts with commands too far in the future, ignore it. // FIXME: this should probably be an error as it shouldn't ever happen - spdlog::get("rwe")->error("First command number in message was too high! Expecting no more than {0}, received {1}", endpoint.nextCommandToReceive.value, firstCommandNumber.value); + LOG_ERROR << "First command number in message was too high! Expecting no more than " << endpoint.nextCommandToReceive.value << ", received " << firstCommandNumber.value; return; } @@ -347,11 +343,7 @@ namespace rwe GameTime newNextHashToSend(message.next_game_hash_to_receive()); if (newNextHashToSend > endpoint.nextHashToSend + GameTime(endpoint.hashSendBuffer.size())) { - spdlog::get("rwe")->error( - "Remote acked up to {0}, but we are at {1} and hash buffer contains {2} elements", - newNextHashToSend.value, - endpoint.nextHashToSend.value, - endpoint.hashSendBuffer.size()); + LOG_ERROR << "Remote acked up to " << newNextHashToSend.value << ", but we are at " << endpoint.nextHashToSend.value << " and hash buffer contains " << endpoint.hashSendBuffer.size() << " elements"; } while (newNextHashToSend > endpoint.nextHashToSend && !endpoint.hashSendBuffer.empty()) { @@ -364,7 +356,7 @@ namespace rwe { // message starts with hashes too far in the future, ignore it. // FIXME: this should probably be an error as it shouldn't ever happen - spdlog::get("rwe")->error("First game hash time in message was too high! Expecting no more than {0}, received {1}", endpoint.nextHashToReceive.value, firstGameHashTime.value); + LOG_ERROR << "First game hash time in message was too high! Expecting no more than " << endpoint.nextHashToReceive.value << ", received " << firstGameHashTime.value; return; } diff --git a/src/rwe/game/GameScene.cpp b/src/rwe/game/GameScene.cpp index 9b9528afc..f757512a7 100644 --- a/src/rwe/game/GameScene.cpp +++ b/src/rwe/game/GameScene.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include namespace rwe { @@ -1983,7 +1983,7 @@ namespace rwe auto bufferedCommandCount = playerCommandService->bufferedCommandCount(localPlayerId); - spdlog::get("rwe")->debug("Buffer levels (real/target) {0}/{1}", bufferedCommandCount, targetCommandBufferSize); + LOG_DEBUG << "Buffer levels (real/target) " << bufferedCommandCount << "/" << targetCommandBufferSize; // If we have too many commands buffered, // defer submitting commands this frame @@ -2335,7 +2335,7 @@ namespace rwe auto playerCommands = playerCommandService->tryPopCommands(); if (!playerCommands) { - spdlog::get("rwe")->error("Blocked waiting for player commands"); + LOG_ERROR << "Blocked waiting for player commands"; return; } diff --git a/src/rwe/pathfinding/AStarPathFinder.h b/src/rwe/pathfinding/AStarPathFinder.h index c82bdb18a..c11856f32 100644 --- a/src/rwe/pathfinding/AStarPathFinder.h +++ b/src/rwe/pathfinding/AStarPathFinder.h @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include #include @@ -67,7 +67,7 @@ namespace rwe if (isGoal(current.vertex)) { - spdlog::get("rwe")->debug("Found goal after visiting {0} vertices", openListPopsPerformed); + LOG_DEBUG << "Found goal after visiting " << openListPopsPerformed << " vertices"; return AStarPathInfo{AStarPathType::Complete, walkPath(current), std::move(closedVertices)}; } @@ -89,7 +89,7 @@ namespace rwe } } - spdlog::get("rwe")->debug("Failed to find goal, visited {0} vertices", openListPopsPerformed); + LOG_DEBUG << "Failed to find goal, visited " << openListPopsPerformed << " vertices"; return AStarPathInfo{AStarPathType::Partial, walkPath(*(closestVertex->second)), std::move(closedVertices)}; } diff --git a/src/rwe/util/SimpleLogger.h b/src/rwe/util/SimpleLogger.h new file mode 100644 index 000000000..8cac2d00b --- /dev/null +++ b/src/rwe/util/SimpleLogger.h @@ -0,0 +1,200 @@ +#pragma once + +// Minimal logging replacement for spdlog. +// This project only used spdlog for basic file logging with timestamps, +// so a full logging framework was unnecessary overhead and was blocking +// the C++20 upgrade. If more advanced logging features are needed in the +// future (async logging, log rotation, multiple sinks, etc.), consider +// re-evaluating spdlog or another logging library. +// +// Usage: +// LOG_INFO << "Loaded " << count << " units from " << path; +// LOG_DEBUG << "Position: " << x << ", " << y; +// LOG_ERROR << "Failed to open file: " << filename; +// +// Log level is controlled at compile time via RWE_LOG_LEVEL. +// Set it in CMake (e.g. -DRWE_LOG_LEVEL=1) to strip lower levels +// from the binary entirely. Default is 0 (everything enabled). +// +// 0 = Debug (all messages) +// 1 = Info +// 2 = Warn +// 3 = Error +// 4 = Critical +// 5 = Off (no messages) + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Compile-time log level. Statements below this level are dead code +// and will be eliminated by the compiler. +#ifndef RWE_LOG_LEVEL +#define RWE_LOG_LEVEL 0 +#endif + +#define RWE_LEVEL_DEBUG 0 +#define RWE_LEVEL_INFO 1 +#define RWE_LEVEL_WARN 2 +#define RWE_LEVEL_ERROR 3 +#define RWE_LEVEL_CRITICAL 4 +#define RWE_LEVEL_OFF 5 + +namespace rwe +{ + enum class LogLevel + { + Debug, + Info, + Warn, + Error, + Critical + }; + + class SimpleLogger + { + private: + std::ofstream file; + std::mutex mutex; + + static const char* levelString(LogLevel lvl) + { + switch (lvl) + { + case LogLevel::Debug: return "debug"; + case LogLevel::Info: return "info"; + case LogLevel::Warn: return "warn"; + case LogLevel::Error: return "error"; + case LogLevel::Critical: return "critical"; + } + return "unknown"; + } + + static std::string timestamp() + { + auto now = std::chrono::system_clock::now(); + auto time = std::chrono::system_clock::to_time_t(now); + auto ms = std::chrono::duration_cast( + now.time_since_epoch()) % 1000; + + std::tm tm{}; +#ifdef _WIN32 + localtime_s(&tm, &time); +#else + localtime_r(&time, &tm); +#endif + + std::ostringstream oss; + oss << std::put_time(&tm, "%Y-%m-%d %H:%M:%S") + << '.' << std::setfill('0') << std::setw(3) << ms.count(); + return oss.str(); + } + + public: + SimpleLogger(const std::string& filePath, bool truncate) + : file(filePath, truncate ? std::ios::trunc : std::ios::app) + { + if (!file.is_open()) + { + throw std::runtime_error("Failed to open log file: " + filePath); + } + } + + void write(LogLevel msgLevel, const std::string& msg) + { + auto line = "[" + timestamp() + "] [" + levelString(msgLevel) + "] " + msg + "\n"; + std::lock_guard lock(mutex); + file << line; + file.flush(); + } + }; + + // RAII stream that accumulates a log message via operator<< + // and writes it to the logger when destroyed. + class LogStream + { + private: + SimpleLogger& logger; + LogLevel level; + std::ostringstream oss; + + public: + LogStream(SimpleLogger& logger, LogLevel level) + : logger(logger), level(level) {} + + ~LogStream() + { + logger.write(level, oss.str()); + } + + LogStream(const LogStream&) = delete; + LogStream& operator=(const LogStream&) = delete; + LogStream(LogStream&&) = default; + + template + LogStream& operator<<(const T& value) + { + oss << value; + return *this; + } + }; + + // Global logger instance, set once at startup. + inline std::shared_ptr& globalLogger() + { + static std::shared_ptr instance; + return instance; + } + + inline void setGlobalLogger(std::shared_ptr logger) + { + globalLogger() = std::move(logger); + } + + inline SimpleLogger& getLogger() + { + auto& logger = globalLogger(); + assert(logger && "Logger not initialized"); + return *logger; + } +} + +// Logging macros. Enabled levels expand to a plain LogStream expression. +// Disabled levels use if(true){}else to create a dead branch that the +// compiler eliminates entirely while still type-checking the << chain. +#if RWE_LOG_LEVEL <= RWE_LEVEL_DEBUG +#define LOG_DEBUG rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Debug) +#else +#define LOG_DEBUG if (true) {} else rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Debug) +#endif + +#if RWE_LOG_LEVEL <= RWE_LEVEL_INFO +#define LOG_INFO rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Info) +#else +#define LOG_INFO if (true) {} else rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Info) +#endif + +#if RWE_LOG_LEVEL <= RWE_LEVEL_WARN +#define LOG_WARN rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Warn) +#else +#define LOG_WARN if (true) {} else rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Warn) +#endif + +#if RWE_LOG_LEVEL <= RWE_LEVEL_ERROR +#define LOG_ERROR rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Error) +#else +#define LOG_ERROR if (true) {} else rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Error) +#endif + +#if RWE_LOG_LEVEL <= RWE_LEVEL_CRITICAL +#define LOG_CRITICAL rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Critical) +#else +#define LOG_CRITICAL if (true) {} else rwe::LogStream(rwe::getLogger(), rwe::LogLevel::Critical) +#endif diff --git a/src/rwe/util/SimpleLogger.test.cpp b/src/rwe/util/SimpleLogger.test.cpp new file mode 100644 index 000000000..33f713e3a --- /dev/null +++ b/src/rwe/util/SimpleLogger.test.cpp @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include + +namespace rwe +{ + static std::vector readLines(const std::string& path) + { + std::ifstream f(path); + std::vector lines; + std::string line; + while (std::getline(f, line)) + { + lines.push_back(line); + } + return lines; + } + + // Strip the timestamp prefix "[YYYY-MM-DD HH:MM:SS.mmm] " from a log line + // so we can assert on the rest deterministically. + static std::string stripTimestamp(const std::string& line) + { + auto pos = line.find("] "); + if (pos == std::string::npos) + { + return line; + } + return line.substr(pos + 2); + } + + static void initTestLogger(const std::string& path) + { + auto logger = std::make_shared(path, true); + setGlobalLogger(logger); + } + + TEST_CASE("SimpleLogger") + { + const std::string path = "/tmp/rwe_test_logger.log"; + + SECTION("writes log lines via macros") + { + initTestLogger(path); + + LOG_INFO << "Hello world"; + LOG_DEBUG << "Value is " << 42; + LOG_ERROR << "Error in " << "module" << ": " << "bad thing"; + LOG_WARN << "No args here"; + + // flush by replacing global logger + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 4); + REQUIRE(stripTimestamp(lines[0]) == "[info] Hello world"); + REQUIRE(stripTimestamp(lines[1]) == "[debug] Value is 42"); + REQUIRE(stripTimestamp(lines[2]) == "[error] Error in module: bad thing"); + REQUIRE(stripTimestamp(lines[3]) == "[warn] No args here"); + + std::remove(path.c_str()); + } + + SECTION("truncate mode overwrites existing file") + { + initTestLogger(path); + LOG_INFO << "first run"; + setGlobalLogger(nullptr); + + initTestLogger(path); + LOG_INFO << "second run"; + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 1); + REQUIRE(stripTimestamp(lines[0]) == "[info] second run"); + + std::remove(path.c_str()); + } + + SECTION("append mode preserves existing content") + { + { + auto logger = std::make_shared(path, true); + setGlobalLogger(logger); + LOG_INFO << "first run"; + } + { + auto logger = std::make_shared(path, false); + setGlobalLogger(logger); + LOG_INFO << "second run"; + } + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 2); + REQUIRE(stripTimestamp(lines[0]) == "[info] first run"); + REQUIRE(stripTimestamp(lines[1]) == "[info] second run"); + + std::remove(path.c_str()); + } + + SECTION("handles multiple stream insertions") + { + initTestLogger(path); + LOG_INFO << "alpha" << " then " << "beta"; + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 1); + REQUIRE(stripTimestamp(lines[0]) == "[info] alpha then beta"); + + std::remove(path.c_str()); + } + + SECTION("handles utf-8 content") + { + initTestLogger(path); + LOG_INFO << "Japanese: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"; + LOG_INFO << "Emoji: \xf0\x9f\x8e\xae\xf0\x9f\x8e\xaf"; + LOG_INFO << "Mixed: hello \xc3\xa9\xc3\xa0\xc3\xbc world"; + LOG_INFO << "Format with utf-8: " << "user" << " said \xc2\xab" << "\xc3\xa7" "a marche\xc2\xbb"; + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 4); + REQUIRE(stripTimestamp(lines[0]) == "[info] Japanese: \xe6\x97\xa5\xe6\x9c\xac\xe8\xaa\x9e"); + REQUIRE(stripTimestamp(lines[1]) == "[info] Emoji: \xf0\x9f\x8e\xae\xf0\x9f\x8e\xaf"); + REQUIRE(stripTimestamp(lines[2]) == "[info] Mixed: hello \xc3\xa9\xc3\xa0\xc3\xbc world"); + REQUIRE(stripTimestamp(lines[3]) == "[info] Format with utf-8: user said \xc2\xab\xc3\xa7" "a marche\xc2\xbb"); + + std::remove(path.c_str()); + } + + SECTION("timestamp format is correct") + { + initTestLogger(path); + LOG_INFO << "check timestamp"; + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 1); + // Verify timestamp matches [YYYY-MM-DD HH:MM:SS.mmm] pattern + REQUIRE(lines[0][0] == '['); + REQUIRE(lines[0][5] == '-'); + REQUIRE(lines[0][8] == '-'); + REQUIRE(lines[0][11] == ' '); + REQUIRE(lines[0][14] == ':'); + REQUIRE(lines[0][17] == ':'); + REQUIRE(lines[0][20] == '.'); + REQUIRE(lines[0][24] == ']'); + + std::remove(path.c_str()); + } + + SECTION("logs numeric types correctly") + { + initTestLogger(path); + LOG_INFO << "int=" << 42 << " float=" << 3.14 << " negative=" << -1; + setGlobalLogger(nullptr); + + auto lines = readLines(path); + REQUIRE(lines.size() == 1); + REQUIRE(stripTimestamp(lines[0]) == "[info] int=42 float=3.14 negative=-1"); + + std::remove(path.c_str()); + } + } +} From de4a095add486706856d91816d90a73055393b48 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:10:15 -0700 Subject: [PATCH 14/32] Clean up build instructions and add .clangd config --- .clangd | 2 ++ CLAUDE.md | 2 +- README.md | 8 +++----- 3 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 .clangd diff --git a/.clangd b/.clangd new file mode 100644 index 000000000..e95990a8f --- /dev/null +++ b/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + CompilationDatabase: build/ diff --git a/CLAUDE.md b/CLAUDE.md index c0e07f3e6..eeb195915 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,7 +17,7 @@ cd libs && ./build-protobuf.sh && cd .. # Build (Linux/macOS) mkdir build && cd build -cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 +cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug make -j$(nproc) # Run unit tests diff --git a/README.md b/README.md index 887ed2b06..284973a5c 100644 --- a/README.md +++ b/README.md @@ -157,16 +157,15 @@ curl -fsSL https://get.jetify.com/devbox | bash # installs devbox (one-time) # From the rwe repo base dir: devbox shell # Enters the dev environment - uses devbox.json to pull in dependencies. May take a while the first time. mkdir build && cd build -cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug -DCMAKE_EXPORT_COMPILE_COMMANDS=1 +cmake .. -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=Debug make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu) # -j isn't necessary, but builds with multiple threads and will reduce build time ./rwe_test # run tests, of course # run the game- this should work for MacOS, tho in Linux the devbox/nix build may have a quirk that gives you OpenGL related errors on launch ./rwe -# If you see "Could not get EGL display" or other OpenGL related errors on launch, try this instead of `./rwe` +# If you see "Could not get EGL display" or other OpenGL related errors on launch, try this instead of `./rwe` # This script just runs rwe with LD_LIBRARY_PATH set to a good guess of which dirs your video drivers exist in. ./run.sh ``` -```-DCMAKE_EXPORT_COMPILE_COMMANDS=1``` is optional. It generates `compile_commands.json` which some VS Code plugins like clangd can read in order to automatically configure themselves for the project, to give the linter/tools like go-to-definition the same view of the code the compiler has. #### Ubuntu @@ -209,10 +208,9 @@ Now build the code: mkdir build cd build export CC=gcc-7 CXX=g++-7 - cmake .. -G 'Unix Makefiles' -DCMAKE_EXPORT_COMPILE_COMMANDS=1 + cmake .. -G 'Unix Makefiles' make -The -DCMAKE_EXPORT_COMPILE_COMMANDS=1 is optional. It generates compile_commands.json which some VS Code plugins like clangd can read in order to automatically configure themselves for the project. Note if LLVM/clang is installed, export CC=clang CXX=clang++ should also work. Finally, launch RWE from the top-level project directory: From 428e6533cc2bc43a284106a3b8b0394364fccd03 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:17:54 -0700 Subject: [PATCH 15/32] Replace boost::adaptors with C++20 std::views and structured bindings --- src/rwe/game/GameNetworkService.cpp | 1 - src/rwe/game/GameScene.cpp | 17 ++++++++--------- src/rwe/game/GameScene.h | 4 +--- src/rwe/util/range_util.h | 8 ++++---- 4 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/rwe/game/GameNetworkService.cpp b/src/rwe/game/GameNetworkService.cpp index 93c9fd0c1..2443ff76c 100644 --- a/src/rwe/game/GameNetworkService.cpp +++ b/src/rwe/game/GameNetworkService.cpp @@ -1,6 +1,5 @@ #include "GameNetworkService.h" #include -#include #include #include #include diff --git a/src/rwe/game/GameScene.cpp b/src/rwe/game/GameScene.cpp index f757512a7..fd7d24371 100644 --- a/src/rwe/game/GameScene.cpp +++ b/src/rwe/game/GameScene.cpp @@ -1,6 +1,5 @@ #include "GameScene.h" #include -#include #include #include #include @@ -523,7 +522,7 @@ namespace rwe auto worldToMinimap = worldToMinimapMatrix(simulation.terrain, minimapRect); // draw minimap dots - for (const auto& unit : (simulation.units | boost::adaptors::map_values)) + for (const auto& [_, unit] : simulation.units) { auto minimapPos = worldToMinimap * simVectorToFloat(unit.position); minimapPos.x = std::floor(minimapPos.x); @@ -754,7 +753,7 @@ namespace rwe auto seaLevel = simulation.terrain.getSeaLevel(); UnitShadowMeshBatch unitShadowMeshBatch; - for (const auto& unit : (simulation.units | boost::adaptors::map_values)) + for (const auto& [_, unit] : simulation.units) { const auto& unitDefinition = simulation.unitDefinitions.at(unit.unitType); const auto& modelDefinition = simulation.unitModelDefinitions.at(unitDefinition.objectName); @@ -766,7 +765,7 @@ namespace rwe } drawUnitShadow(gameMediaDatabase, viewProjectionMatrix, unit, unitDefinition, modelDefinition, interpolationFraction, simScalarToFloat(groundHeight), unitTextureAtlas.get(), unitTeamTextureAtlases, unitShadowMeshBatch); } - for (const auto& feature : (simulation.features | boost::adaptors::map_values)) + for (const auto& [_, feature] : simulation.features) { const auto& position = feature.position; auto groundHeight = simulation.terrain.getHeightAt(position.x, position.z); @@ -782,13 +781,13 @@ namespace rwe sceneContext.graphics->enableDepthBuffer(); UnitMeshBatch unitMeshBatch; - for (const auto& unit : (simulation.units | boost::adaptors::map_values)) + for (const auto& [_, unit] : simulation.units) { const auto& unitDefinition = simulation.unitDefinitions.at(unit.unitType); const auto& unitModelDefinition = simulation.unitModelDefinitions.at(unitDefinition.objectName); drawUnit(gameMediaDatabase, viewProjectionMatrix, unit, unitDefinition, unitModelDefinition, getPlayer(unit.owner).color, interpolationFraction, unitTextureAtlas.get(), unitTeamTextureAtlases, unitMeshBatch); } - for (const auto& feature : (simulation.features | boost::adaptors::map_values)) + for (const auto& [_, feature] : simulation.features) { drawMeshFeature(simulation.unitModelDefinitions, gameMediaDatabase, viewProjectionMatrix, feature, unitTextureAtlas.get(), unitTeamTextureAtlases, unitMeshBatch); } @@ -820,7 +819,7 @@ namespace rwe sceneContext.graphics->disableDepthTest(); ColoredMeshBatch nanoLinesBatch; - for (const auto& unit : (simulation.units | boost::adaptors::map_values)) + for (const auto& [_, unit] : simulation.units) { if (auto nanolatheTarget = unit.getActiveNanolatheTarget()) { @@ -904,7 +903,7 @@ namespace rwe if (healthBarsVisible) { - for (const UnitState& unit : (simulation.units | boost::adaptors::map_values)) + for (const auto& [_, unit] : simulation.units) { if (!unit.isOwnedBy(localPlayerId)) { @@ -3325,7 +3324,7 @@ namespace rwe bool GameScene::matchesWithSidePrefix(const std::string& suffix, const std::string& value) const { - for (const auto& side : (*sceneContext.sideData | boost::adaptors::map_values)) + for (const auto& [_, side] : *sceneContext.sideData) { if (side.namePrefix + suffix == value) { diff --git a/src/rwe/game/GameScene.h b/src/rwe/game/GameScene.h index 07f1ce0c7..6bde0ff1e 100644 --- a/src/rwe/game/GameScene.h +++ b/src/rwe/game/GameScene.h @@ -1,7 +1,5 @@ #pragma once -#include -#include #include #include #include @@ -542,7 +540,7 @@ namespace rwe template std::optional> findWithSidePrefix(UiPanel& p, const std::string& name) { - for (const auto& side : (*sceneContext.sideData | boost::adaptors::map_values)) + for (const auto& [_, side] : *sceneContext.sideData) { auto control = p.find(side.namePrefix + name); if (control) diff --git a/src/rwe/util/range_util.h b/src/rwe/util/range_util.h index 2e76600ba..15f716bca 100644 --- a/src/rwe/util/range_util.h +++ b/src/rwe/util/range_util.h @@ -1,6 +1,6 @@ #pragma once -#include +#include namespace rwe { @@ -8,8 +8,8 @@ namespace rwe auto choose(Range r, Chooser c) { return r - | boost::adaptors::transformed(c) - | boost::adaptors::filtered([](const auto& e) { return e.has_value(); }) - | boost::adaptors::transformed([](const auto& e) { return *e; }); + | std::views::transform(c) + | std::views::filter([](const auto& e) { return e.has_value(); }) + | std::views::transform([](const auto& e) { return *e; }); } } From bbf43f94c29d27fc4b4593294a6d4d1e40e13d63 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:38:09 -0700 Subject: [PATCH 16/32] Replace boost::asio with standalone Asio, remove Boost dependency entirely --- .gitignore | 1 + .gitmodules | 6 +++--- CMakeLists.txt | 28 ++++------------------------ devbox.json | 1 - devbox.lock | 14 -------------- libs/asio | 1 + libs/spdlog | 1 - src/rwe/LoadingNetworkService.cpp | 14 +++++++------- src/rwe/LoadingNetworkService.h | 21 ++++++++++----------- src/rwe/game/GameNetworkService.cpp | 22 +++++++++++----------- src/rwe/game/GameNetworkService.h | 19 +++++++++---------- src/rwe/game/GameScene_util.cpp | 1 - 12 files changed, 46 insertions(+), 83 deletions(-) create mode 160000 libs/asio delete mode 160000 libs/spdlog diff --git a/.gitignore b/.gitignore index eb9ad245c..728b9efe1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ /libs/_msvc/ /libs/_protobuf-install/ +/.cache/ /.idea/ /.vs/ diff --git a/.gitmodules b/.gitmodules index 194516bf4..7a56e0097 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,3 @@ -[submodule "libs/spdlog"] - path = libs/spdlog - url = https://github.com/gabime/spdlog.git [submodule "libs/rapidcheck"] path = libs/rapidcheck url = https://github.com/MHeasell/rapidcheck.git @@ -19,3 +16,6 @@ [submodule "libs/utfcpp"] path = libs/utfcpp url = https://github.com/nemtrif/utfcpp.git +[submodule "libs/asio"] + path = libs/asio + url = https://github.com/chriskohlhoff/asio.git diff --git a/CMakeLists.txt b/CMakeLists.txt index ca1c12129..32ff1c815 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,7 +33,7 @@ project("Robot War Engine" VERSION ${CMAKE_MATCH_1}) enable_testing() set(RC_ENABLE_CATCH ON CACHE BOOL "Enables Catch support in RapidCheck" FORCE) -set(RC_ENABLE_BOOST ON CACHE BOOL "Enables Boost support in RapidCheck" FORCE) +set(RC_ENABLE_BOOST OFF CACHE BOOL "Enables Boost support in RapidCheck" FORCE) # rapidcheck relies on result_of, which is deprecated in favour of invoke_result. # MSVC hides it from their stdlib unless you supply this option. if(MSVC) @@ -97,15 +97,6 @@ if(MSVC) set(GLEW_LIBRARIES "${CMAKE_SOURCE_DIR}/libs/_msvc/glew-2.1.0/lib/Release/x64/glew32.lib") set(GLEW_INCLUDE_DIRS "libs/_msvc/glew-2.1.0/include") - set(Boost_INCLUDE_DIRS "libs/_msvc/boost_1_74_0") - if(CMAKE_BUILD_TYPE STREQUAL "Debug") - set(Boost_LIBRARIES "${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_filesystem-vc142-mt-gd-x64-1_74.lib;${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_system-vc142-mt-gd-x64-1_74.lib;${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_program_options-vc142-mt-gd-x64-1_74.lib") - else() - set(Boost_LIBRARIES "${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_filesystem-vc142-mt-x64-1_74.lib;${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_system-vc142-mt-x64-1_74.lib;${CMAKE_SOURCE_DIR}/libs/_msvc/boost_1_74_0/lib64-msvc-14.2/libboost_program_options-vc142-mt-x64-1_74.lib") - endif() - add_definitions(-DBOOST_DATE_TIME_NO_LIB) - add_definitions(-DBOOST_REGEX_NO_LIB) - set(SDL2_DLL "${CMAKE_SOURCE_DIR}/libs/_msvc/SDL2-2.0.7/lib/x64/SDL2.dll") set(SDL2_LIBRARY "${CMAKE_SOURCE_DIR}/libs/_msvc/SDL2-2.0.7/lib/x64/SDL2.lib;${CMAKE_SOURCE_DIR}/libs/_msvc/SDL2-2.0.7/lib/x64/SDL2main.lib") set(SDL2_INCLUDE_DIR "libs/_msvc/SDL2-2.0.7/include") @@ -153,9 +144,6 @@ else() find_package(GLEW REQUIRED) find_windows_dll(GLEW_DLL "glew32.dll") - set(Boost_NO_BOOST_CMAKE ON) - find_package(Boost 1.54.0 REQUIRED COMPONENTS filesystem program_options) - find_package(SDL2 REQUIRED) find_windows_dll(SDL2_DLL "SDL2.dll") find_package(SDL2_image REQUIRED) @@ -204,13 +192,6 @@ else() find_windows_dll(LCMS2_DLL "liblcms2-2.dll") endif() -# We should have std::chrono everywhere, -# but boost sometimes fails to detect this (e.g. with clang 5 on Ubuntu) -# and falls back to using boost::chrono. -# This enables the use of std::chrono explicitly -# in boost.asio. -add_definitions(-DBOOST_ASIO_HAS_STD_CHRONO) - set(Protobuf_USE_STATIC_LIBS ON) find_package(Protobuf REQUIRED) @@ -638,10 +619,10 @@ target_include_directories(librwe PUBLIC "src") configure_file("src/rwe/config.h.in" "config/rwe/config.h" @ONLY) target_include_directories(librwe PUBLIC "${CMAKE_CURRENT_BINARY_DIR}/config") -target_include_directories(librwe PUBLIC ${Boost_INCLUDE_DIRS}) -target_link_libraries(librwe ${Boost_LIBRARIES}) +target_include_directories(librwe PUBLIC "libs/asio/asio/include") +add_definitions(-DASIO_STANDALONE) if(WIN32) - # winsock needed for boost asio on windows + # winsock needed for asio on windows target_link_libraries(librwe ws2_32) endif() @@ -866,7 +847,6 @@ set(TEST_FILES add_executable(rwe_test src/test.cpp ${TEST_FILES}) target_include_directories(rwe_test PUBLIC ${PROJECT_SOURCE_DIR}/libs/catch2/single_include) target_link_libraries(rwe_test rapidcheck_catch) -target_link_libraries(rwe_test rapidcheck_boost) target_link_libraries(rwe_test librwe) add_test(NAME rwe_test COMMAND rwe_test) diff --git a/devbox.json b/devbox.json index 983307b98..716747b18 100644 --- a/devbox.json +++ b/devbox.json @@ -2,7 +2,6 @@ "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.16.0/.schema/devbox.schema.json", "packages": [ "cmake", - "boost", "protobuf", "SDL2", "SDL2_image", diff --git a/devbox.lock b/devbox.lock index d9809953a..805ff46e9 100644 --- a/devbox.lock +++ b/devbox.lock @@ -43,20 +43,6 @@ } } }, - "boost": { - "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#boost", - "source": "nixpkg", - "systems": { - "x86_64-darwin": { - "outputs": [ - { - "path": "/nix/store/yvlw2q07alcpqsyqk4wxf3llchri5zbd-boost-1.87.0", - "default": true - } - ] - } - } - }, "cmake": { "resolved": "github:NixOS/nixpkgs/724cf38d99ba81fbb4a347081db93e2e3a9bc2ae?narHash=sha256-MpAKyXfJRDTgRU33Hja%2BG%2B3h9ywLAJJNRq4Pjbb4dQs%3D#cmake", "source": "nixpkg", diff --git a/libs/asio b/libs/asio new file mode 160000 index 000000000..bd500f0a0 --- /dev/null +++ b/libs/asio @@ -0,0 +1 @@ +Subproject commit bd500f0a018db9a845ebaaed5c0318343ae9f497 diff --git a/libs/spdlog b/libs/spdlog deleted file mode 160000 index 4fba14c79..000000000 --- a/libs/spdlog +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4fba14c79f356ae48d6141c561bf9fd7ba33fabd diff --git a/src/rwe/LoadingNetworkService.cpp b/src/rwe/LoadingNetworkService.cpp index aa871f4ad..34731c236 100644 --- a/src/rwe/LoadingNetworkService.cpp +++ b/src/rwe/LoadingNetworkService.cpp @@ -24,7 +24,7 @@ namespace rwe { std::scoped_lock lock(mutex); - // boost guarantees that resolve returns non-empty + // asio guarantees that resolve returns non-empty auto results = resolver.resolve(host, port); remoteEndpoints.emplace_back(playerIndex, results.begin()->endpoint(), Status::Loading); } @@ -60,7 +60,7 @@ namespace rwe try { LOG_DEBUG << "Opening listen socket on port " << port; - auto endpoint = boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v6(), std::stoi(port)); + auto endpoint = asio::ip::udp::endpoint(asio::ip::udp::v6(), std::stoi(port)); socket.open(endpoint.protocol()); socket.bind(endpoint); @@ -76,7 +76,7 @@ namespace rwe } } - void LoadingNetworkService::onReceive(const boost::system::error_code& error, std::size_t bytesTransferred) + void LoadingNetworkService::onReceive(const asio::error_code& error, std::size_t bytesTransferred) { if (error) { @@ -175,7 +175,7 @@ namespace rwe for (const auto& p : remoteEndpoints) { LOG_DEBUG << "Sending notification to " << p.endpoint.address().to_string() << " " << p.endpoint.port() << ", size " << outerMessage.ByteSizeLong(); - socket.send_to(boost::asio::buffer(sendBuffer.data(), messageSize + 4), p.endpoint); + socket.send_to(asio::buffer(sendBuffer.data(), messageSize + 4), p.endpoint); } } @@ -183,7 +183,7 @@ namespace rwe { notifyStatus(); notifyTimer.expires_after(std::chrono::milliseconds(100)); - notifyTimer.async_wait([this](const boost::system::error_code& error) { + notifyTimer.async_wait([this](const asio::error_code& error) { if (error) { LOG_ERROR << "Received error from network notify timer: " << error.message(); @@ -197,14 +197,14 @@ namespace rwe { // go back to waiting for the next message socket.async_receive_from( - boost::asio::buffer(receiveBuffer.data(), receiveBuffer.size()), + asio::buffer(receiveBuffer.data(), receiveBuffer.size()), currentRemoteEndpoint, [this](const auto& error, const auto& bytesTransferred) { onReceive(error, bytesTransferred); startListening(); }); } - boost::asio::ip::udp::endpoint LoadingNetworkService::getEndpoint(int playerIndex) + asio::ip::udp::endpoint LoadingNetworkService::getEndpoint(int playerIndex) { auto it = std::find_if(remoteEndpoints.begin(), remoteEndpoints.end(), [&](const auto& e) { return e.playerIndex == playerIndex; }); if (it == remoteEndpoints.end()) diff --git a/src/rwe/LoadingNetworkService.h b/src/rwe/LoadingNetworkService.h index c840b3fb1..4da764592 100644 --- a/src/rwe/LoadingNetworkService.h +++ b/src/rwe/LoadingNetworkService.h @@ -2,8 +2,7 @@ #include #include -#include -#include // not in asio.hpp in old boost versions +#include #include #include #include @@ -26,9 +25,9 @@ namespace rwe struct PlayerInfo { int playerIndex; - boost::asio::ip::udp::endpoint endpoint; + asio::ip::udp::endpoint endpoint; Status status; - PlayerInfo(int playerIndex, const boost::asio::ip::udp::endpoint& endpoint, Status status) : playerIndex(playerIndex), endpoint(endpoint), status(status) {} + PlayerInfo(int playerIndex, const asio::ip::udp::endpoint& endpoint, Status status) : playerIndex(playerIndex), endpoint(endpoint), status(status) {} }; private: @@ -40,13 +39,13 @@ namespace rwe std::vector remoteEndpoints; // state owned by the worker thread - boost::asio::io_context ioContext; - boost::asio::ip::udp::resolver resolver; - boost::asio::ip::udp::socket socket; + asio::io_context ioContext; + asio::ip::udp::resolver resolver; + asio::ip::udp::socket socket; std::array sendBuffer; std::array receiveBuffer; - boost::asio::ip::udp::endpoint currentRemoteEndpoint; - boost::asio::steady_timer notifyTimer; + asio::ip::udp::endpoint currentRemoteEndpoint; + asio::steady_timer notifyTimer; public: LoadingNetworkService(); @@ -55,7 +54,7 @@ namespace rwe void addEndpoint(int playerIndex, const std::string& host, const std::string& port); - boost::asio::ip::udp::endpoint getEndpoint(int playerIndex); + asio::ip::udp::endpoint getEndpoint(int playerIndex); void setDoneLoading(); @@ -68,7 +67,7 @@ namespace rwe private: void run(const std::string& port); - void onReceive(const boost::system::error_code& error, std::size_t bytesTransferred); + void onReceive(const asio::error_code& error, std::size_t bytesTransferred); void notifyStatus(); diff --git a/src/rwe/game/GameNetworkService.cpp b/src/rwe/game/GameNetworkService.cpp index 2443ff76c..301b8dc83 100644 --- a/src/rwe/game/GameNetworkService.cpp +++ b/src/rwe/game/GameNetworkService.cpp @@ -43,7 +43,7 @@ namespace rwe void GameNetworkService::submitCommands(SceneTime currentSceneTime, const GameNetworkService::CommandSet& commands) { - boost::asio::post(ioContext,[this, currentSceneTime, commands]() { + asio::post(ioContext,[this, currentSceneTime, commands]() { this->currentSceneTime = currentSceneTime; for (auto& e : endpoints) { @@ -54,7 +54,7 @@ namespace rwe void GameNetworkService::submitGameHash(GameHash hash) { - boost::asio::post(ioContext,[this, hash]() { + asio::post(ioContext,[this, hash]() { for (auto& e : endpoints) { e.hashSendBuffer.push_back(hash); @@ -65,7 +65,7 @@ namespace rwe SceneTime GameNetworkService::estimateAvergeSceneTime(SceneTime localSceneTime) { std::promise result; - boost::asio::post(ioContext,[this, localSceneTime, &result]() { + asio::post(ioContext,[this, localSceneTime, &result]() { auto time = getTimestamp(); auto otherTimes = choose(endpoints, [](const auto& e) { return e.lastKnownSceneTime; }); @@ -79,7 +79,7 @@ namespace rwe float GameNetworkService::getMaxAverageRttMillis() { std::promise result; - boost::asio::post(ioContext,[this, &result]() { + asio::post(ioContext,[this, &result]() { auto maxRtt = 0.0f; for (const auto& e : endpoints) { @@ -99,7 +99,7 @@ namespace rwe { try { - auto endpoint = boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v6(), port); + auto endpoint = asio::ip::udp::endpoint(asio::ip::udp::v6(), port); socket.open(endpoint.protocol()); socket.bind(endpoint); @@ -118,7 +118,7 @@ namespace rwe void GameNetworkService::listenForNextMessage() { socket.async_receive_from( - boost::asio::buffer(receiveBuffer.data(), receiveBuffer.size()), + asio::buffer(receiveBuffer.data(), receiveBuffer.size()), currentRemoteEndpoint, [this](const auto& error, const auto& bytesTransferred) { receive(error, bytesTransferred); @@ -172,10 +172,10 @@ namespace rwe { sendToAll(); sendTimer.expires_after(std::chrono::milliseconds(100)); - sendTimer.async_wait([this](const boost::system::error_code& error) { + sendTimer.async_wait([this](const asio::error_code& error) { if (error) { - LOG_ERROR << "Boost error while waiting on timer: " << error.message(); + LOG_ERROR << "Error while waiting on timer: " << error.message(); return; } @@ -216,7 +216,7 @@ namespace rwe // throw in a CRC to verify the message writeInt(&sendBuffer[messageSize], computeCrc(sendBuffer.data(), messageSize)); - socket.send_to(boost::asio::buffer(sendBuffer.data(), messageSize + 4), endpoint.endpoint); + socket.send_to(asio::buffer(sendBuffer.data(), messageSize + 4), endpoint.endpoint); auto nextSequenceNumber = SequenceNumber(endpoint.nextCommandToSend.value + (endpoint.sendBuffer.size())); if (endpoint.sendTimes.empty() || endpoint.sendTimes.back().first < nextSequenceNumber) @@ -225,11 +225,11 @@ namespace rwe } } - void GameNetworkService::receive(const boost::system::error_code& error, std::size_t receivedBytes) + void GameNetworkService::receive(const asio::error_code& error, std::size_t receivedBytes) { if (error) { - LOG_ERROR << "Boost error on receive: " << error.message(); + LOG_ERROR << "Error on receive: " << error.message(); return; } diff --git a/src/rwe/game/GameNetworkService.h b/src/rwe/game/GameNetworkService.h index 07a77da3b..7dbc8c542 100644 --- a/src/rwe/game/GameNetworkService.h +++ b/src/rwe/game/GameNetworkService.h @@ -1,7 +1,6 @@ #pragma once -#include -#include // not in asio.hpp in old boost versions +#include #include #include #include @@ -28,7 +27,7 @@ namespace rwe struct EndpointInfo { PlayerId playerId; - boost::asio::ip::udp::endpoint endpoint; + asio::ip::udp::endpoint endpoint; SequenceNumber nextCommandToSend{0}; SequenceNumber nextCommandToReceive{0}; @@ -67,7 +66,7 @@ namespace rwe */ float averageRoundTripTime{0}; - EndpointInfo(const PlayerId& playerId, const boost::asio::ip::udp::endpoint& endpoint) + EndpointInfo(const PlayerId& playerId, const asio::ip::udp::endpoint& endpoint) : playerId(playerId), endpoint(endpoint) { } @@ -82,16 +81,16 @@ namespace rwe std::thread networkThread; - boost::asio::io_context ioContext; - boost::asio::ip::udp::resolver resolver; - boost::asio::ip::udp::socket socket; - boost::asio::steady_timer sendTimer; + asio::io_context ioContext; + asio::ip::udp::resolver resolver; + asio::ip::udp::socket socket; + asio::steady_timer sendTimer; std::vector endpoints; std::array sendBuffer; std::array receiveBuffer; - boost::asio::ip::udp::endpoint currentRemoteEndpoint; + asio::ip::udp::endpoint currentRemoteEndpoint; PlayerCommandService* const playerCommandService; @@ -132,6 +131,6 @@ namespace rwe void send(EndpointInfo& endpoint); - void receive(const boost::system::error_code& error, std::size_t receivedBytes); + void receive(const asio::error_code& error, std::size_t receivedBytes); }; } diff --git a/src/rwe/game/GameScene_util.cpp b/src/rwe/game/GameScene_util.cpp index 18ffd5b32..f1deba89c 100644 --- a/src/rwe/game/GameScene_util.cpp +++ b/src/rwe/game/GameScene_util.cpp @@ -1,7 +1,6 @@ #include "GameScene_util.h" #include -#include #include namespace rwe From 74a8819a72e22f5447b078969a9feac3852b7f3c Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:41:10 -0700 Subject: [PATCH 17/32] Remove Boost from CI and build instructions --- .github/workflows/build.yml | 4 ---- README.md | 4 ---- 2 files changed, 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a918d2518..b0078292b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,9 +37,6 @@ jobs: gcc-12 g++-12 clang-15 - libboost-dev - libboost-filesystem-dev - libboost-program-options-dev libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev @@ -185,7 +182,6 @@ jobs: make unzip mingw-w64-x86_64-toolchain - mingw-w64-x86_64-boost mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer diff --git a/README.md b/README.md index 284973a5c..c6fe1cd62 100644 --- a/README.md +++ b/README.md @@ -95,7 +95,6 @@ Choose to run at the end of install, and in the terminal that opens install the pacman -S git make unzip mingw-w64-x86_64-cmake mingw-w64-x86_64-toolchain pacman -S \ - mingw-w64-x86_64-boost \ mingw-w64-x86_64-SDL2 \ mingw-w64-x86_64-SDL2_image \ mingw-w64-x86_64-SDL2_mixer \ @@ -178,9 +177,6 @@ Install the necessary packages: sudo apt-get install \ gcc-7 \ g++-7 \ - libboost-dev \ - libboost-filesystem-dev \ - libboost-program-options-dev \ libsdl2-dev \ libsdl2-image-dev \ libsdl2-mixer-dev \ From 279095021dbb82d4cabdb7b45aab69c30b43beab Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:55:03 -0700 Subject: [PATCH 18/32] Upgrade GitHub Actions from v3 to v4 --- .github/workflows/build.yml | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b0078292b..452735db1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive @@ -44,7 +44,7 @@ jobs: zlib1g-dev libpng-dev - name: install node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: npm @@ -63,7 +63,7 @@ jobs: run: npm run package - name: restore protobuf id: cache-protobuf-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: libs/_protobuf-install key: ${{ runner.os }}-${{ env.CC }}-protobuf @@ -72,7 +72,7 @@ jobs: run: ./build-protobuf.sh - name: save protobuf id: cache-protobuf-save - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: libs/_protobuf-install key: ${{ steps.cache-protobuf-restore.outputs.cache-primary-key }} @@ -91,7 +91,7 @@ jobs: configuration: [Debug, Release] runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive @@ -100,7 +100,7 @@ jobs: with: python-version: '3.10' - name: install node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: npm @@ -132,13 +132,13 @@ jobs: working-directory: build run: cmake --build . --target package --config ${{ matrix.configuration }} --parallel 2 - name: upload zip artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: RWE Zip ${{ runner.os }} MSVC ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.zip if-no-files-found: error - name: upload installer artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: RWE Installer ${{ runner.os }} MSVC ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe @@ -149,12 +149,12 @@ jobs: configuration: [Debug, Release] runs-on: windows-2022 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: fetch-depth: 0 submodules: recursive - name: install node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: 18 cache: npm @@ -192,7 +192,7 @@ jobs: mingw-w64-x86_64-readline - name: restore protobuf id: cache-protobuf-restore - uses: actions/cache/restore@v3 + uses: actions/cache/restore@v4 with: path: libs/_protobuf-install key: ${{ runner.os }}-mingw64-protobuf-${{ hashFiles('.git/modules/libs/protobuf/HEAD') }} @@ -202,7 +202,7 @@ jobs: shell: msys2 {0} - name: save protobuf id: cache-protobuf-save - uses: actions/cache/save@v3 + uses: actions/cache/save@v4 with: path: libs/_protobuf-install key: ${{ steps.cache-protobuf-restore.outputs.cache-primary-key }} @@ -223,13 +223,13 @@ jobs: run: make package shell: msys2 {0} - name: upload zip artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: RWE Zip ${{ runner.os }} MinGW64 ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.zip if-no-files-found: error - name: upload installer artifact - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@v4 with: name: RWE Installer ${{ runner.os }} MinGW64 ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe From df6d9f482219f2d0a0ae3799b2225794f8435373 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 17:56:44 -0700 Subject: [PATCH 19/32] Upgrade GitHub Actions to latest versions (checkout v6, setup-node v6, upload-artifact v6, cache v5, setup-python v6) --- .github/workflows/build.yml | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 452735db1..3c9fa1bfb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive @@ -44,7 +44,7 @@ jobs: zlib1g-dev libpng-dev - name: install node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 18 cache: npm @@ -63,7 +63,7 @@ jobs: run: npm run package - name: restore protobuf id: cache-protobuf-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: libs/_protobuf-install key: ${{ runner.os }}-${{ env.CC }}-protobuf @@ -72,7 +72,7 @@ jobs: run: ./build-protobuf.sh - name: save protobuf id: cache-protobuf-save - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: libs/_protobuf-install key: ${{ steps.cache-protobuf-restore.outputs.cache-primary-key }} @@ -91,16 +91,16 @@ jobs: configuration: [Debug, Release] runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - name: install python - uses: actions/setup-python@v4 + uses: actions/setup-python@v6 with: python-version: '3.10' - name: install node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 18 cache: npm @@ -132,13 +132,13 @@ jobs: working-directory: build run: cmake --build . --target package --config ${{ matrix.configuration }} --parallel 2 - name: upload zip artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: RWE Zip ${{ runner.os }} MSVC ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.zip if-no-files-found: error - name: upload installer artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MSVC ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe @@ -149,12 +149,12 @@ jobs: configuration: [Debug, Release] runs-on: windows-2022 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: fetch-depth: 0 submodules: recursive - name: install node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version: 18 cache: npm @@ -192,7 +192,7 @@ jobs: mingw-w64-x86_64-readline - name: restore protobuf id: cache-protobuf-restore - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: libs/_protobuf-install key: ${{ runner.os }}-mingw64-protobuf-${{ hashFiles('.git/modules/libs/protobuf/HEAD') }} @@ -202,7 +202,7 @@ jobs: shell: msys2 {0} - name: save protobuf id: cache-protobuf-save - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: libs/_protobuf-install key: ${{ steps.cache-protobuf-restore.outputs.cache-primary-key }} @@ -223,13 +223,13 @@ jobs: run: make package shell: msys2 {0} - name: upload zip artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: RWE Zip ${{ runner.os }} MinGW64 ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.zip if-no-files-found: error - name: upload installer artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MinGW64 ${{ matrix.configuration }} path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe From d6810bba98230b95e6f09ad9e2908aabb06b9f8a Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Mon, 23 Mar 2026 18:04:10 -0700 Subject: [PATCH 20/32] Host MSVC libs bundle on GitHub Releases, drop setup-python from CI --- .github/workflows/build.yml | 4 ---- fetch-msvc-libs.py | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3c9fa1bfb..93d53d565 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -95,10 +95,6 @@ jobs: with: fetch-depth: 0 submodules: recursive - - name: install python - uses: actions/setup-python@v6 - with: - python-version: '3.10' - name: install node.js uses: actions/setup-node@v6 with: diff --git a/fetch-msvc-libs.py b/fetch-msvc-libs.py index f6d532520..62726dc30 100644 --- a/fetch-msvc-libs.py +++ b/fetch-msvc-libs.py @@ -49,7 +49,7 @@ def log(msg): os.chdir("libs") sha256hash = binascii.unhexlify(sha256hash_hex) - bundle_url = "https://rwe.michaelheasell.com/bundles/" + bundle_name + bundle_url = "https://github.com/MHeasell/rwe/releases/download/msvc-libs-v6/" + bundle_name log("Deleting old files...") try: From 2b0d5960ef87859c567433f8d225bc4c7de9cacc Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 04:35:34 -0700 Subject: [PATCH 21/32] Fix CI: upgrade to ubuntu-24.04/gcc-14/clang-18, add missing includes, fix MinGW deps --- .github/workflows/build.yml | 25 +++++++++++++------------ src/rwe/sim/UnitState.h | 1 + 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 93d53d565..f29e3be40 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -10,18 +10,18 @@ jobs: matrix: include: - configuration: Debug - cc: gcc-12 - cxx: g++-12 + cc: gcc-14 + cxx: g++-14 - configuration: Release - cc: gcc-12 - cxx: g++-12 + cc: gcc-14 + cxx: g++-14 - configuration: Debug - cc: clang-15 - cxx: clang++-15 + cc: clang-18 + cxx: clang++-18 - configuration: Release - cc: clang-15 - cxx: clang++-15 - runs-on: ubuntu-22.04 + cc: clang-18 + cxx: clang++-18 + runs-on: ubuntu-24.04 env: CC: ${{ matrix.cc }} CXX: ${{ matrix.cxx }} @@ -34,9 +34,9 @@ jobs: run: sudo apt-get update -y - name: install apt packages run: sudo apt-get install -y - gcc-12 - g++-12 - clang-15 + gcc-14 + g++-14 + clang-18 libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev @@ -183,6 +183,7 @@ jobs: mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-glew mingw-w64-x86_64-smpeg2 + mingw-w64-x86_64-libmodplug mingw-w64-x86_64-zlib mingw-w64-x86_64-libpng mingw-w64-x86_64-readline diff --git a/src/rwe/sim/UnitState.h b/src/rwe/sim/UnitState.h index 20a25bdfc..6f0f25e9e 100644 --- a/src/rwe/sim/UnitState.h +++ b/src/rwe/sim/UnitState.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include From 4ad1201a773daca165a6310ba7a8486ea2ddf947 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 04:58:19 -0700 Subject: [PATCH 22/32] Fix SimpleLogger tests to use temp_directory_path for Windows compatibility --- src/rwe/util/SimpleLogger.test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/rwe/util/SimpleLogger.test.cpp b/src/rwe/util/SimpleLogger.test.cpp index 33f713e3a..23e9b2321 100644 --- a/src/rwe/util/SimpleLogger.test.cpp +++ b/src/rwe/util/SimpleLogger.test.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include #include @@ -39,7 +40,7 @@ namespace rwe TEST_CASE("SimpleLogger") { - const std::string path = "/tmp/rwe_test_logger.log"; + const std::string path = (std::filesystem::temp_directory_path() / "rwe_test_logger.log").string(); SECTION("writes log lines via macros") { From 0033e1bc7866638346c93115a306c22515c4072d Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 05:11:56 -0700 Subject: [PATCH 23/32] Speed up CI: shallow submodule clones, increase parallelism to 4 --- .github/workflows/build.yml | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f29e3be40..9d284cc29 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,8 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - submodules: recursive + - name: init submodules (shallow) + run: git submodule update --init --recursive --depth 1 - name: update apt package list run: sudo apt-get update -y - name: install apt packages @@ -81,7 +82,7 @@ jobs: mkdir build pushd build cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. - make -j 2 + make -j 4 popd - name: test rwe run: ./build/rwe_test @@ -94,7 +95,8 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - submodules: recursive + - name: init submodules (shallow) + run: git submodule update --init --recursive --depth 1 - name: install node.js uses: actions/setup-node@v6 with: @@ -119,14 +121,14 @@ jobs: mkdir build cd build cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. - cmake --build . --config ${{ matrix.configuration }} --parallel 2 + cmake --build . --config ${{ matrix.configuration }} --parallel 4 cd .. - name: test rwe working-directory: build run: .\${{ matrix.configuration }}\rwe_test.exe - name: package rwe working-directory: build - run: cmake --build . --target package --config ${{ matrix.configuration }} --parallel 2 + run: cmake --build . --target package --config ${{ matrix.configuration }} --parallel 4 - name: upload zip artifact uses: actions/upload-artifact@v6 with: @@ -148,7 +150,8 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - submodules: recursive + - name: init submodules (shallow) + run: git submodule update --init --recursive --depth 1 - name: install node.js uses: actions/setup-node@v6 with: @@ -208,7 +211,7 @@ jobs: mkdir build pushd build cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. - make -j 2 + make -j 4 popd shell: msys2 {0} - name: test rwe From 9b2f6d1bc45170da141736f6a4ae79850f091e95 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 05:31:05 -0700 Subject: [PATCH 24/32] Upgrade Windows CI runners from windows-2022 to windows-2025 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9d284cc29..2ced5ba04 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -90,7 +90,7 @@ jobs: strategy: matrix: configuration: [Debug, Release] - runs-on: windows-2022 + runs-on: windows-2025 steps: - uses: actions/checkout@v6 with: @@ -145,7 +145,7 @@ jobs: strategy: matrix: configuration: [Debug, Release] - runs-on: windows-2022 + runs-on: windows-2025 steps: - uses: actions/checkout@v6 with: From 8bc7bcd3e7eeaf0c3ac407b9b602abf84a18efc4 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 05:57:53 -0700 Subject: [PATCH 25/32] Switch installer from NSIS to WiX, add PCH and ccache to CI NSIS is not pre-installed on windows-2025 runners but WiX is. Precompiled headers for librwe speed up builds across all platforms. ccache added for Linux and MinGW CI builds with GitHub Actions cache. --- .github/workflows/build.yml | 22 ++++++++++++++++++---- CMakeLists.txt | 23 +++++++++++++++++++---- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ced5ba04..65c6faba3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -38,12 +38,19 @@ jobs: gcc-14 g++-14 clang-18 + ccache libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libglew-dev zlib1g-dev libpng-dev + - name: restore ccache + uses: actions/cache@v5 + with: + path: ~/.cache/ccache + key: ccache-${{ runner.os }}-${{ env.CC }}-${{ matrix.configuration }}-${{ github.sha }} + restore-keys: ccache-${{ runner.os }}-${{ env.CC }}-${{ matrix.configuration }}- - name: install node.js uses: actions/setup-node@v6 with: @@ -81,7 +88,7 @@ jobs: run: | mkdir build pushd build - cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. + cmake -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache .. make -j 4 popd - name: test rwe @@ -139,7 +146,7 @@ jobs: uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MSVC ${{ matrix.configuration }} - path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe + path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.msi if-no-files-found: error build-windows-mingw64: strategy: @@ -190,6 +197,13 @@ jobs: mingw-w64-x86_64-zlib mingw-w64-x86_64-libpng mingw-w64-x86_64-readline + mingw-w64-x86_64-ccache + - name: restore ccache + uses: actions/cache@v5 + with: + path: ~/.cache/ccache + key: ccache-${{ runner.os }}-mingw64-${{ matrix.configuration }}-${{ github.sha }} + restore-keys: ccache-${{ runner.os }}-mingw64-${{ matrix.configuration }}- - name: restore protobuf id: cache-protobuf-restore uses: actions/cache/restore@v5 @@ -210,7 +224,7 @@ jobs: run: | mkdir build pushd build - cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. + cmake -G 'Unix Makefiles' -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache .. make -j 4 popd shell: msys2 {0} @@ -232,5 +246,5 @@ jobs: uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MinGW64 ${{ matrix.configuration }} - path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe + path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.msi if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index 32ff1c815..a7577c903 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -628,6 +628,22 @@ endif() target_include_directories(librwe PUBLIC "libs/utfcpp/source") +target_precompile_headers(librwe PRIVATE + + + + + + + + + + + + + +) + target_link_libraries(librwe OpenGL::GL) target_copy_file(librwe ${GLEW_DLL}) @@ -848,6 +864,7 @@ add_executable(rwe_test src/test.cpp ${TEST_FILES}) target_include_directories(rwe_test PUBLIC ${PROJECT_SOURCE_DIR}/libs/catch2/single_include) target_link_libraries(rwe_test rapidcheck_catch) target_link_libraries(rwe_test librwe) +target_precompile_headers(rwe_test REUSE_FROM librwe) add_test(NAME rwe_test COMMAND rwe_test) install(TARGETS rwe rwe_bridge @@ -859,7 +876,7 @@ install(DIRECTORY shaders DESTINATION .) install(DIRECTORY launcher/rwe-launcher-win32-x64/ DESTINATION launcher) -set(CPACK_GENERATOR "ZIP;NSIS") +set(CPACK_GENERATOR "ZIP;WIX") set(CPACK_PACKAGE_VERSION ${RWE_GIT_DESCRIPTION}) @@ -876,8 +893,6 @@ set(CPACK_PACKAGE_VENDOR "Michael Heasell") set(CPACK_OUTPUT_FILE_PREFIX "dist") -set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") - -set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) +set(CPACK_WIX_UPGRADE_GUID "9BF7A834-221F-4790-BDB5-136D805BD39E") include(CPack) From a9b8b470016bdb84ff15c875fd007240ad56976f Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 06:03:33 -0700 Subject: [PATCH 26/32] Add sccache for MSVC CI builds --- .github/workflows/build.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 65c6faba3..20de08a94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,11 +123,19 @@ jobs: working-directory: launcher run: npm run package - run: python3 fetch-msvc-libs.py + - name: install sccache + run: choco install sccache -y + - name: restore sccache + uses: actions/cache@v5 + with: + path: ~\AppData\Local\Mozilla\sccache + key: sccache-${{ runner.os }}-msvc-${{ matrix.configuration }}-${{ github.sha }} + restore-keys: sccache-${{ runner.os }}-msvc-${{ matrix.configuration }}- - name: build rwe run: | mkdir build cd build - cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} .. + cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache .. cmake --build . --config ${{ matrix.configuration }} --parallel 4 cd .. - name: test rwe From be11f4c9739c5de41047db6dbef42fc17392589f Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 07:41:12 -0700 Subject: [PATCH 27/32] Upgrade MSVC CI to VS 2026 on windows-2025-vs2026 runner, add sccache --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20de08a94..1ba30c543 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -97,7 +97,7 @@ jobs: strategy: matrix: configuration: [Debug, Release] - runs-on: windows-2025 + runs-on: windows-2025-vs2026 steps: - uses: actions/checkout@v6 with: @@ -135,7 +135,7 @@ jobs: run: | mkdir build cd build - cmake -G "Visual Studio 17 2022" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache .. + cmake -G "Visual Studio 18 2026" -A x64 -DCMAKE_BUILD_TYPE=${{ matrix.configuration }} -DCMAKE_C_COMPILER_LAUNCHER=sccache -DCMAKE_CXX_COMPILER_LAUNCHER=sccache .. cmake --build . --config ${{ matrix.configuration }} --parallel 4 cd .. - name: test rwe From 8dd429cf4fe9ccd5af671a3750f9c08de82f0708 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 10:14:34 -0700 Subject: [PATCH 28/32] Update rapidcheck to latest upstream, rename LICENSE for WiX Switch rapidcheck submodule from MHeasell fork to upstream emil-e/rapidcheck (the fork had no custom changes). Updates from 2023 to Feb 2026, picking up std::aligned_storage removal, C++20 fixes, and cmake_minimum_required bump to 3.16. Rename LICENSE to LICENSE.txt for WiX installer compatibility. --- .gitmodules | 2 +- CMakeLists.txt | 2 +- LICENSE => LICENSE.txt | 0 libs/rapidcheck | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename LICENSE => LICENSE.txt (100%) diff --git a/.gitmodules b/.gitmodules index 7a56e0097..c0f5384d2 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "libs/rapidcheck"] path = libs/rapidcheck - url = https://github.com/MHeasell/rapidcheck.git + url = https://github.com/emil-e/rapidcheck.git [submodule "libs/protobuf"] path = libs/protobuf url = https://github.com/google/protobuf.git diff --git a/CMakeLists.txt b/CMakeLists.txt index a7577c903..40a5b2949 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -882,7 +882,7 @@ set(CPACK_PACKAGE_VERSION ${RWE_GIT_DESCRIPTION}) set(CPACK_PACKAGE_FILE_NAME "RobotWarEngine-${RWE_GIT_DESCRIPTION}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}") -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") +set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.txt") set(CPACK_RESOURCE_FILE_README "${PROJECT_SOURCE_DIR}/README.md") set(CPACK_PACKAGE_EXECUTABLES diff --git a/LICENSE b/LICENSE.txt similarity index 100% rename from LICENSE rename to LICENSE.txt diff --git a/libs/rapidcheck b/libs/rapidcheck index a5724ea5b..b96a4e626 160000 --- a/libs/rapidcheck +++ b/libs/rapidcheck @@ -1 +1 @@ -Subproject commit a5724ea5b0b00147109b0605c377f1e54c353ba2 +Subproject commit b96a4e626ef4c7348dcd16c500353c2f997a9f3f From 539585a2c5036715fdd5f1b169dbf495c34fc855 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 10:20:46 -0700 Subject: [PATCH 29/32] =?UTF-8?q?Update=20json=20(v3.9.1=20=E2=86=92=20v3.?= =?UTF-8?q?12.0)=20and=20utfcpp=20(v4.0.5=20=E2=86=92=20v4.0.9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- libs/json | 2 +- libs/utfcpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/json b/libs/json index db78ac1d7..55f93686c 160000 --- a/libs/json +++ b/libs/json @@ -1 +1 @@ -Subproject commit db78ac1d7716f56fc9f1b030b715f872f93964e4 +Subproject commit 55f93686c01528224f448c19128836e7df245f72 diff --git a/libs/utfcpp b/libs/utfcpp index 79835a5fa..6bbbaccab 160000 --- a/libs/utfcpp +++ b/libs/utfcpp @@ -1 +1 @@ -Subproject commit 79835a5fa57271f07a90ed36123e30ae9741178e +Subproject commit 6bbbaccabb1440d87acf5abfa3ed1ac604bfbe1a From 3953d8610c1f179ef4b1780a150631d6ffb73b31 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 10:38:08 -0700 Subject: [PATCH 30/32] Upgrade Catch2 from v2.13.10 to v3.13.0 Catch2 v3 is a compiled library instead of header-only, reducing test compilation overhead by ~80%. Key changes: - Link Catch2::Catch2WithMain instead of defining CATCH_CONFIG_MAIN - Replace catch2/catch.hpp with catch2/catch_test_macros.hpp - Add catch2/catch_approx.hpp where Approx is used - Qualify Approx as Catch::Approx (no longer in global namespace) - Remove MSVC _HAS_DEPRECATED_RESULT_OF workaround (fixed in rapidcheck) - Disable RC_ENABLE_CATCH to avoid target collision with Catch2 v3 --- CMakeLists.txt | 16 ++-- libs/catch2 | 2 +- src/rwe/BoxTreeSplit.test.cpp | 2 +- src/rwe/Viewport.test.cpp | 5 +- src/rwe/cob/cob_util.test.cpp | 25 ++--- src/rwe/collections/MinHeap.test.cpp | 2 +- src/rwe/collections/VectorMap.test.cpp | 2 +- src/rwe/game/dump_util.test.cpp | 2 +- src/rwe/geometry/BoundingBox3f.test.cpp | 2 +- src/rwe/geometry/Circle2f.test.cpp | 2 +- src/rwe/geometry/CollisionMesh.test.cpp | 2 +- src/rwe/geometry/Plane3f.test.cpp | 15 +-- src/rwe/geometry/Ray3f.test.cpp | 9 +- src/rwe/geometry/Rectangle2f.test.cpp | 2 +- src/rwe/geometry/Triangle3f.test.cpp | 73 +++++++-------- src/rwe/grid/DiscreteRect.test.cpp | 2 +- src/rwe/grid/EightWayDirection.test.cpp | 2 +- src/rwe/grid/Grid.test.cpp | 2 +- src/rwe/grid/Point.test.cpp | 2 +- src/rwe/io/featuretdf/io.test.cpp | 2 +- src/rwe/io/gui/gui.test.cpp | 2 +- src/rwe/io/ota/ota.test.cpp | 2 +- src/rwe/io/sidedatatdf/SideData.test.cpp | 2 +- src/rwe/io/tdf/ListTdfAdapter.test.cpp | 2 +- src/rwe/io/tdf/NetSchemaTdfAdapter.test.cpp | 2 +- src/rwe/io/tdf/SimpleTdfAdapter.test.cpp | 2 +- src/rwe/io/tdf/TdfBlock.test.cpp | 2 +- src/rwe/ip_util.test.cpp | 2 +- src/rwe/math/Matrix4f.test.cpp | 91 ++++++++++--------- src/rwe/math/Vector2f.test.cpp | 13 +-- src/rwe/math/Vector3f.test.cpp | 43 ++++----- src/rwe/math/rwe_math.test.cpp | 2 +- src/rwe/network_util.test.cpp | 2 +- .../pathfinding/pathfinding_utils.test.cpp | 2 +- src/rwe/sim/GameHash_util.test.cpp | 2 +- src/rwe/sim/SimAngle.test.cpp | 9 +- src/rwe/sim/SimVector.test.cpp | 2 +- src/rwe/sim/UnitState_util.test.cpp | 2 +- src/rwe/sim/util.test.cpp | 9 +- src/rwe/util/OpaqueArgs.test.cpp | 2 +- src/rwe/util/Result.test.cpp | 2 +- src/rwe/util/SimpleLogger.test.cpp | 2 +- src/rwe/util/SpanStream.test.cpp | 2 +- src/rwe/util/rwe_string.test.cpp | 2 +- src/test.cpp | 2 - 45 files changed, 185 insertions(+), 191 deletions(-) delete mode 100644 src/test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 40a5b2949..e87d1602f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,14 +32,12 @@ endif() project("Robot War Engine" VERSION ${CMAKE_MATCH_1}) enable_testing() -set(RC_ENABLE_CATCH ON CACHE BOOL "Enables Catch support in RapidCheck" FORCE) +add_subdirectory("libs/catch2") + +set(RC_ENABLE_CATCH OFF CACHE BOOL "Enables Catch support in RapidCheck" FORCE) set(RC_ENABLE_BOOST OFF CACHE BOOL "Enables Boost support in RapidCheck" FORCE) -# rapidcheck relies on result_of, which is deprecated in favour of invoke_result. -# MSVC hides it from their stdlib unless you supply this option. -if(MSVC) - add_definitions(-D_HAS_DEPRECATED_RESULT_OF) -endif() add_subdirectory("libs/rapidcheck") +add_subdirectory("libs/rapidcheck/extras/catch") set(JSON_BuildTests OFF CACHE INTERNAL "") set(JSON_Install OFF CACHE INTERNAL "") @@ -860,8 +858,8 @@ set(TEST_FILES src/rwe/util/rwe_string.test.cpp ) -add_executable(rwe_test src/test.cpp ${TEST_FILES}) -target_include_directories(rwe_test PUBLIC ${PROJECT_SOURCE_DIR}/libs/catch2/single_include) +add_executable(rwe_test ${TEST_FILES}) +target_link_libraries(rwe_test Catch2::Catch2WithMain) target_link_libraries(rwe_test rapidcheck_catch) target_link_libraries(rwe_test librwe) target_precompile_headers(rwe_test REUSE_FROM librwe) @@ -870,7 +868,7 @@ add_test(NAME rwe_test COMMAND rwe_test) install(TARGETS rwe rwe_bridge RUNTIME DESTINATION . ) -install(FILES LICENSE README.md DESTINATION .) +install(FILES LICENSE.txt README.md DESTINATION .) install(DIRECTORY shaders DESTINATION .) diff --git a/libs/catch2 b/libs/catch2 index 182c910b4..29c9844f6 160000 --- a/libs/catch2 +++ b/libs/catch2 @@ -1 +1 @@ -Subproject commit 182c910b4b63ff587a3440e08f84f70497e49a81 +Subproject commit 29c9844f688acb27c87338c39cd186ebfe41aa19 diff --git a/src/rwe/BoxTreeSplit.test.cpp b/src/rwe/BoxTreeSplit.test.cpp index 6e36a2d8a..032646323 100644 --- a/src/rwe/BoxTreeSplit.test.cpp +++ b/src/rwe/BoxTreeSplit.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace rwe diff --git a/src/rwe/Viewport.test.cpp b/src/rwe/Viewport.test.cpp index 5827489e9..94da4cd20 100644 --- a/src/rwe/Viewport.test.cpp +++ b/src/rwe/Viewport.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include namespace rwe @@ -12,7 +13,7 @@ namespace rwe Viewport vp(0, 0, 8, 6); auto v = vp.toClipSpace(2, 2); REQUIRE(v.x == -0.5f); - REQUIRE(v.y == Approx(0.33333333)); + REQUIRE(v.y == Catch::Approx(0.33333333)); } } diff --git a/src/rwe/cob/cob_util.test.cpp b/src/rwe/cob/cob_util.test.cpp index 75a56d893..6870e5697 100644 --- a/src/rwe/cob/cob_util.test.cpp +++ b/src/rwe/cob/cob_util.test.cpp @@ -1,21 +1,10 @@ -#include +#include +#include #include #include #include #include -namespace Catch -{ - namespace Detail - { - std::ostream& operator<<(std::ostream& os, const Approx& a) - { - os << a.toString(); - return os; - } - } -} - namespace rwe { TEST_CASE("cob_util") @@ -65,11 +54,11 @@ namespace rwe SECTION("toRadians") { - REQUIRE(toRadians(CobAngle(0)).value == Approx(0.0f)); - REQUIRE(toRadians(CobAngle(8192)).value == Approx(Pif / 4.0f)); - REQUIRE(toRadians(CobAngle(16384)).value == Approx(Pif / 2.0f)); - REQUIRE(toRadians(CobAngle(32768)).value == Approx(-Pif)); - REQUIRE(toRadians(CobAngle(49152)).value == Approx(-Pif / 2.0f)); + REQUIRE(toRadians(CobAngle(0)).value == Catch::Approx(0.0f)); + REQUIRE(toRadians(CobAngle(8192)).value == Catch::Approx(Pif / 4.0f)); + REQUIRE(toRadians(CobAngle(16384)).value == Catch::Approx(Pif / 2.0f)); + REQUIRE(toRadians(CobAngle(32768)).value == Catch::Approx(-Pif)); + REQUIRE(toRadians(CobAngle(49152)).value == Catch::Approx(-Pif / 2.0f)); } SECTION("toCobAngle") diff --git a/src/rwe/collections/MinHeap.test.cpp b/src/rwe/collections/MinHeap.test.cpp index 485281a3a..8bd2bdd8a 100644 --- a/src/rwe/collections/MinHeap.test.cpp +++ b/src/rwe/collections/MinHeap.test.cpp @@ -10,7 +10,7 @@ namespace std } } -#include +#include #include #include #include diff --git a/src/rwe/collections/VectorMap.test.cpp b/src/rwe/collections/VectorMap.test.cpp index 6ba047988..6dcc8a03e 100644 --- a/src/rwe/collections/VectorMap.test.cpp +++ b/src/rwe/collections/VectorMap.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/game/dump_util.test.cpp b/src/rwe/game/dump_util.test.cpp index 80421e871..a613aebb3 100644 --- a/src/rwe/game/dump_util.test.cpp +++ b/src/rwe/game/dump_util.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace rwe diff --git a/src/rwe/geometry/BoundingBox3f.test.cpp b/src/rwe/geometry/BoundingBox3f.test.cpp index a0fb1d5c3..4e8e403bd 100644 --- a/src/rwe/geometry/BoundingBox3f.test.cpp +++ b/src/rwe/geometry/BoundingBox3f.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/geometry/Circle2f.test.cpp b/src/rwe/geometry/Circle2f.test.cpp index 5088fc86d..90fa63040 100644 --- a/src/rwe/geometry/Circle2f.test.cpp +++ b/src/rwe/geometry/Circle2f.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/geometry/CollisionMesh.test.cpp b/src/rwe/geometry/CollisionMesh.test.cpp index bb3c9bdfc..0980073c2 100644 --- a/src/rwe/geometry/CollisionMesh.test.cpp +++ b/src/rwe/geometry/CollisionMesh.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/geometry/Plane3f.test.cpp b/src/rwe/geometry/Plane3f.test.cpp index b373e5df5..f2fcfec12 100644 --- a/src/rwe/geometry/Plane3f.test.cpp +++ b/src/rwe/geometry/Plane3f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -86,13 +87,13 @@ namespace rwe // not strictly required by implementation // (could use b or c as the point) // but no better way to verify at the moment - REQUIRE(p.point.x == Approx(0.0f)); - REQUIRE(p.point.y == Approx(0.0f)); - REQUIRE(p.point.z == Approx(0.0f)); + REQUIRE(p.point.x == Catch::Approx(0.0f)); + REQUIRE(p.point.y == Catch::Approx(0.0f)); + REQUIRE(p.point.z == Catch::Approx(0.0f)); - REQUIRE(p.normal.x == Approx(0.0f)); - REQUIRE(p.normal.y == Approx(0.0f)); - REQUIRE(p.normal.z == Approx(1.0f)); + REQUIRE(p.normal.x == Catch::Approx(0.0f)); + REQUIRE(p.normal.y == Catch::Approx(0.0f)); + REQUIRE(p.normal.z == Catch::Approx(1.0f)); } } } diff --git a/src/rwe/geometry/Ray3f.test.cpp b/src/rwe/geometry/Ray3f.test.cpp index 75f9aecd5..c9764fd2f 100644 --- a/src/rwe/geometry/Ray3f.test.cpp +++ b/src/rwe/geometry/Ray3f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include @@ -10,9 +11,9 @@ namespace rwe { Ray3f r(Vector3f(3.0f, 10.0f, 4.0f), Vector3f(0.0f, -2.0f, 0.0f)); auto i = r.pointAt(3.0f); - REQUIRE(i.x == Approx(3.0f)); - REQUIRE(i.y == Approx(4.0f)); - REQUIRE(i.z == Approx(4.0f)); + REQUIRE(i.x == Catch::Approx(3.0f)); + REQUIRE(i.y == Catch::Approx(4.0f)); + REQUIRE(i.z == Catch::Approx(4.0f)); } } diff --git a/src/rwe/geometry/Rectangle2f.test.cpp b/src/rwe/geometry/Rectangle2f.test.cpp index a8509baee..ce84dcadd 100644 --- a/src/rwe/geometry/Rectangle2f.test.cpp +++ b/src/rwe/geometry/Rectangle2f.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/geometry/Triangle3f.test.cpp b/src/rwe/geometry/Triangle3f.test.cpp index 9aea37d6b..44576a8fb 100644 --- a/src/rwe/geometry/Triangle3f.test.cpp +++ b/src/rwe/geometry/Triangle3f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -18,9 +19,9 @@ namespace rwe Vector3f(0.0f, 2.0f, 0.0f)); Vector3f bary = tri.toBarycentric(p); - REQUIRE(bary.x == Approx(0.5f)); - REQUIRE(bary.y == Approx(0.5f)); - REQUIRE(bary.z == Approx(0.0f)); + REQUIRE(bary.x == Catch::Approx(0.5f)); + REQUIRE(bary.y == Catch::Approx(0.5f)); + REQUIRE(bary.z == Catch::Approx(0.0f)); } SECTION("barycentric conversion test 2") @@ -32,9 +33,9 @@ namespace rwe Vector3f(0.0f, 2.0f, 0.0f)); Vector3f bary = tri.toBarycentric(p); - REQUIRE(bary.x == Approx(0.5f)); - REQUIRE(bary.y == Approx(0.25f)); - REQUIRE(bary.z == Approx(0.25f)); + REQUIRE(bary.x == Catch::Approx(0.5f)); + REQUIRE(bary.y == Catch::Approx(0.25f)); + REQUIRE(bary.z == Catch::Approx(0.25f)); } SECTION("barycentric conversion test 3") @@ -46,9 +47,9 @@ namespace rwe Vector3f(0.5f, 0.5f, 0.5f)); Vector3f bary = tri.toBarycentric(p); - REQUIRE(bary.x == Approx(5.5f)); - REQUIRE(bary.y == Approx(-10.0f)); - REQUIRE(bary.z == Approx(5.5f)); + REQUIRE(bary.x == Catch::Approx(5.5f)); + REQUIRE(bary.y == Catch::Approx(-10.0f)); + REQUIRE(bary.z == Catch::Approx(5.5f)); } } @@ -63,9 +64,9 @@ namespace rwe Vector3f(0.0f, 2.0f, 0.0f)); Vector3f cart = tri.toCartesian(p); - REQUIRE(cart.x == Approx(1.0f)); - REQUIRE(cart.y == Approx(0.0f)); - REQUIRE(cart.z == Approx(0.0f)); + REQUIRE(cart.x == Catch::Approx(1.0f)); + REQUIRE(cart.y == Catch::Approx(0.0f)); + REQUIRE(cart.z == Catch::Approx(0.0f)); } SECTION("test 2") @@ -77,9 +78,9 @@ namespace rwe Vector3f(0.0f, 2.0f, 0.0f)); Vector3f cart = tri.toCartesian(p); - REQUIRE(cart.x == Approx(0.5f)); - REQUIRE(cart.y == Approx(0.5f)); - REQUIRE(cart.z == Approx(0.0f)); + REQUIRE(cart.x == Catch::Approx(0.5f)); + REQUIRE(cart.y == Catch::Approx(0.5f)); + REQUIRE(cart.z == Catch::Approx(0.0f)); } SECTION("test 3") @@ -91,9 +92,9 @@ namespace rwe Vector3f(0.5f, 0.5f, 0.5f)); Vector3f cart = tri.toCartesian(p); - REQUIRE(cart.x == Approx(-5.0f)); - REQUIRE(cart.y == Approx(0.5f)); - REQUIRE(cart.z == Approx(5.0f)); + REQUIRE(cart.x == Catch::Approx(-5.0f)); + REQUIRE(cart.y == Catch::Approx(0.5f)); + REQUIRE(cart.z == Catch::Approx(5.0f)); } } @@ -110,7 +111,7 @@ namespace rwe Vector3f(0.0f, 0.0f, -1.0f)); auto intersect = tri.intersect(r); REQUIRE(intersect); - REQUIRE(*intersect == Approx(10.0f)); + REQUIRE(*intersect == Catch::Approx(10.0f)); } SECTION("hits at the corner of the triangle") @@ -124,7 +125,7 @@ namespace rwe Vector3f(0.0f, 0.0f, -1.0f)); auto intersect = tri.intersect(r); REQUIRE(intersect); - REQUIRE(*intersect == Approx(10.0f)); + REQUIRE(*intersect == Catch::Approx(10.0f)); } SECTION("misses just below the corner of the triangle") @@ -202,9 +203,9 @@ namespace rwe Vector3f(0.0f, 1.0f, 0.0f)); auto intersect = tri.intersectLine(Vector3f(0.0f, 0.0f, 10.0f), Vector3f(0.0f, 0.0f, -10.0f)); REQUIRE(intersect); - REQUIRE(intersect->x == Approx(0.0f)); - REQUIRE(intersect->y == Approx(0.0f)); - REQUIRE(intersect->z == Approx(0.0f)); + REQUIRE(intersect->x == Catch::Approx(0.0f)); + REQUIRE(intersect->y == Catch::Approx(0.0f)); + REQUIRE(intersect->z == Catch::Approx(0.0f)); } SECTION("works for a line in the other direction") @@ -215,9 +216,9 @@ namespace rwe Vector3f(0.0f, 1.0f, 0.0f)); auto intersect = tri.intersectLine(Vector3f(0.0f, 0.0f, -10.0f), Vector3f(0.0f, 0.0f, 10.0f)); REQUIRE(intersect); - REQUIRE(intersect->x == Approx(0.0f)); - REQUIRE(intersect->y == Approx(0.0f)); - REQUIRE(intersect->z == Approx(0.0f)); + REQUIRE(intersect->x == Catch::Approx(0.0f)); + REQUIRE(intersect->y == Catch::Approx(0.0f)); + REQUIRE(intersect->z == Catch::Approx(0.0f)); } SECTION("hits at the corner of the triangle") @@ -228,9 +229,9 @@ namespace rwe Vector3f(0.0f, 1.0f, 0.0f)); auto intersect = tri.intersectLine(Vector3f(-1.0f, -1.0f, 10.0f), Vector3f(-1.0f, -1.0f, -10.0f)); REQUIRE(intersect); - REQUIRE(intersect->x == Approx(-1.0f)); - REQUIRE(intersect->y == Approx(-1.0f)); - REQUIRE(intersect->z == Approx(0.0f)); + REQUIRE(intersect->x == Catch::Approx(-1.0f)); + REQUIRE(intersect->y == Catch::Approx(-1.0f)); + REQUIRE(intersect->z == Catch::Approx(0.0f)); } SECTION("misses just below the corner of the triangle") @@ -266,13 +267,13 @@ namespace rwe Vector3f(0, 1, 0)); Plane3f p = tri.toPlane(); - REQUIRE(p.point.x == Approx(-1.0f)); - REQUIRE(p.point.y == Approx(-1.0f)); - REQUIRE(p.point.z == Approx(0.0f)); + REQUIRE(p.point.x == Catch::Approx(-1.0f)); + REQUIRE(p.point.y == Catch::Approx(-1.0f)); + REQUIRE(p.point.z == Catch::Approx(0.0f)); - REQUIRE(p.normal.x == Approx(0.0f)); - REQUIRE(p.normal.y == Approx(0.0f)); - REQUIRE(p.normal.z == Approx(4.0f)); + REQUIRE(p.normal.x == Catch::Approx(0.0f)); + REQUIRE(p.normal.y == Catch::Approx(0.0f)); + REQUIRE(p.normal.z == Catch::Approx(4.0f)); } } } diff --git a/src/rwe/grid/DiscreteRect.test.cpp b/src/rwe/grid/DiscreteRect.test.cpp index 6532531c7..218a35914 100644 --- a/src/rwe/grid/DiscreteRect.test.cpp +++ b/src/rwe/grid/DiscreteRect.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/grid/EightWayDirection.test.cpp b/src/rwe/grid/EightWayDirection.test.cpp index 1021cb4a5..a0142e7e6 100644 --- a/src/rwe/grid/EightWayDirection.test.cpp +++ b/src/rwe/grid/EightWayDirection.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace rwe diff --git a/src/rwe/grid/Grid.test.cpp b/src/rwe/grid/Grid.test.cpp index 0e6e5d5bc..434110629 100644 --- a/src/rwe/grid/Grid.test.cpp +++ b/src/rwe/grid/Grid.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/grid/Point.test.cpp b/src/rwe/grid/Point.test.cpp index eaee1dd15..9b559315a 100644 --- a/src/rwe/grid/Point.test.cpp +++ b/src/rwe/grid/Point.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/io/featuretdf/io.test.cpp b/src/rwe/io/featuretdf/io.test.cpp index 350aaec35..8062c5a39 100644 --- a/src/rwe/io/featuretdf/io.test.cpp +++ b/src/rwe/io/featuretdf/io.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/io/gui/gui.test.cpp b/src/rwe/io/gui/gui.test.cpp index a868eb19f..ec646f5c8 100644 --- a/src/rwe/io/gui/gui.test.cpp +++ b/src/rwe/io/gui/gui.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/ota/ota.test.cpp b/src/rwe/io/ota/ota.test.cpp index 9c6be8dc7..1e4767339 100644 --- a/src/rwe/io/ota/ota.test.cpp +++ b/src/rwe/io/ota/ota.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/sidedatatdf/SideData.test.cpp b/src/rwe/io/sidedatatdf/SideData.test.cpp index 4bcefc7d7..0f03820b7 100644 --- a/src/rwe/io/sidedatatdf/SideData.test.cpp +++ b/src/rwe/io/sidedatatdf/SideData.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/tdf/ListTdfAdapter.test.cpp b/src/rwe/io/tdf/ListTdfAdapter.test.cpp index c7dc32b58..9a712c1e2 100644 --- a/src/rwe/io/tdf/ListTdfAdapter.test.cpp +++ b/src/rwe/io/tdf/ListTdfAdapter.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/tdf/NetSchemaTdfAdapter.test.cpp b/src/rwe/io/tdf/NetSchemaTdfAdapter.test.cpp index 64ced68fd..3dca08baf 100644 --- a/src/rwe/io/tdf/NetSchemaTdfAdapter.test.cpp +++ b/src/rwe/io/tdf/NetSchemaTdfAdapter.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/tdf/SimpleTdfAdapter.test.cpp b/src/rwe/io/tdf/SimpleTdfAdapter.test.cpp index da09fb390..d86fb913c 100644 --- a/src/rwe/io/tdf/SimpleTdfAdapter.test.cpp +++ b/src/rwe/io/tdf/SimpleTdfAdapter.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/io/tdf/TdfBlock.test.cpp b/src/rwe/io/tdf/TdfBlock.test.cpp index b7dd76920..398b6e2af 100644 --- a/src/rwe/io/tdf/TdfBlock.test.cpp +++ b/src/rwe/io/tdf/TdfBlock.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/ip_util.test.cpp b/src/rwe/ip_util.test.cpp index e85abb47e..a88433753 100644 --- a/src/rwe/ip_util.test.cpp +++ b/src/rwe/ip_util.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include using namespace std::string_literals; diff --git a/src/rwe/math/Matrix4f.test.cpp b/src/rwe/math/Matrix4f.test.cpp index 41879b3f7..c3fe5ce15 100644 --- a/src/rwe/math/Matrix4f.test.cpp +++ b/src/rwe/math/Matrix4f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -66,25 +67,25 @@ namespace rwe Matrix4f b = Matrix4f::inverseOrthographicProjection(-40.0f, 60.0f, -30.0f, 10.0f, 2.0f, 200.0f); Matrix4f c = b * a; - REQUIRE(c.data[0] == Approx(1.0f)); - REQUIRE(c.data[1] == Approx(0.0f)); - REQUIRE(c.data[2] == Approx(0.0f)); - REQUIRE(c.data[3] == Approx(0.0f)); - - REQUIRE(c.data[4] == Approx(0.0f)); - REQUIRE(c.data[5] == Approx(1.0f)); - REQUIRE(c.data[6] == Approx(0.0f)); - REQUIRE(c.data[7] == Approx(0.0f)); - - REQUIRE(c.data[8] == Approx(0.0f)); - REQUIRE(c.data[9] == Approx(0.0f)); - REQUIRE(c.data[10] == Approx(1.0f)); - REQUIRE(c.data[11] == Approx(0.0f)); - - REQUIRE(c.data[12] == Approx(0.0f)); - REQUIRE(c.data[13] == Approx(0.0f)); - REQUIRE(c.data[14] == Approx(0.0f)); - REQUIRE(c.data[15] == Approx(1.0f)); + REQUIRE(c.data[0] == Catch::Approx(1.0f)); + REQUIRE(c.data[1] == Catch::Approx(0.0f)); + REQUIRE(c.data[2] == Catch::Approx(0.0f)); + REQUIRE(c.data[3] == Catch::Approx(0.0f)); + + REQUIRE(c.data[4] == Catch::Approx(0.0f)); + REQUIRE(c.data[5] == Catch::Approx(1.0f)); + REQUIRE(c.data[6] == Catch::Approx(0.0f)); + REQUIRE(c.data[7] == Catch::Approx(0.0f)); + + REQUIRE(c.data[8] == Catch::Approx(0.0f)); + REQUIRE(c.data[9] == Catch::Approx(0.0f)); + REQUIRE(c.data[10] == Catch::Approx(1.0f)); + REQUIRE(c.data[11] == Catch::Approx(0.0f)); + + REQUIRE(c.data[12] == Catch::Approx(0.0f)); + REQUIRE(c.data[13] == Catch::Approx(0.0f)); + REQUIRE(c.data[14] == Catch::Approx(0.0f)); + REQUIRE(c.data[15] == Catch::Approx(1.0f)); } } @@ -96,25 +97,25 @@ namespace rwe Matrix4f b = Matrix4f::cabinetProjection(0.0f, -0.5f); Matrix4f c = b * a; - REQUIRE(c.data[0] == Approx(1.0f)); - REQUIRE(c.data[1] == Approx(0.0f)); - REQUIRE(c.data[2] == Approx(0.0f)); - REQUIRE(c.data[3] == Approx(0.0f)); - - REQUIRE(c.data[4] == Approx(0.0f)); - REQUIRE(c.data[5] == Approx(1.0f)); - REQUIRE(c.data[6] == Approx(0.0f)); - REQUIRE(c.data[7] == Approx(0.0f)); - - REQUIRE(c.data[8] == Approx(0.0f)); - REQUIRE(c.data[9] == Approx(0.0f)); - REQUIRE(c.data[10] == Approx(1.0f)); - REQUIRE(c.data[11] == Approx(0.0f)); - - REQUIRE(c.data[12] == Approx(0.0f)); - REQUIRE(c.data[13] == Approx(0.0f)); - REQUIRE(c.data[14] == Approx(0.0f)); - REQUIRE(c.data[15] == Approx(1.0f)); + REQUIRE(c.data[0] == Catch::Approx(1.0f)); + REQUIRE(c.data[1] == Catch::Approx(0.0f)); + REQUIRE(c.data[2] == Catch::Approx(0.0f)); + REQUIRE(c.data[3] == Catch::Approx(0.0f)); + + REQUIRE(c.data[4] == Catch::Approx(0.0f)); + REQUIRE(c.data[5] == Catch::Approx(1.0f)); + REQUIRE(c.data[6] == Catch::Approx(0.0f)); + REQUIRE(c.data[7] == Catch::Approx(0.0f)); + + REQUIRE(c.data[8] == Catch::Approx(0.0f)); + REQUIRE(c.data[9] == Catch::Approx(0.0f)); + REQUIRE(c.data[10] == Catch::Approx(1.0f)); + REQUIRE(c.data[11] == Catch::Approx(0.0f)); + + REQUIRE(c.data[12] == Catch::Approx(0.0f)); + REQUIRE(c.data[13] == Catch::Approx(0.0f)); + REQUIRE(c.data[14] == Catch::Approx(0.0f)); + REQUIRE(c.data[15] == Catch::Approx(1.0f)); } } @@ -125,9 +126,9 @@ namespace rwe Matrix4f m = Matrix4f::translation(Vector3f(3.0f, 4.0f, 5.0f)); Vector3f v(11.0f, 15.0f, 19.0f); auto u = m * v; - REQUIRE(u.x == Approx(14.0f)); - REQUIRE(u.y == Approx(19.0f)); - REQUIRE(u.z == Approx(24.0f)); + REQUIRE(u.x == Catch::Approx(14.0f)); + REQUIRE(u.y == Catch::Approx(19.0f)); + REQUIRE(u.z == Catch::Approx(24.0f)); } } @@ -188,9 +189,9 @@ namespace rwe auto m = Matrix4f::rotationAxisAngle(Vector3f(0.0f, 1.0f, 0.0f), Pif / 2.0f); auto v = Vector3f(5.0f, 0.0f, 3.0f); auto v2 = m * v; - REQUIRE(v2.x == Approx(3.0f)); - REQUIRE(v2.y == Approx(0.0f)); - REQUIRE(v2.z == Approx(-5.0f)); + REQUIRE(v2.x == Catch::Approx(3.0f)); + REQUIRE(v2.y == Catch::Approx(0.0f)); + REQUIRE(v2.z == Catch::Approx(-5.0f)); } } } diff --git a/src/rwe/math/Vector2f.test.cpp b/src/rwe/math/Vector2f.test.cpp index da7a6455b..4d6b5025a 100644 --- a/src/rwe/math/Vector2f.test.cpp +++ b/src/rwe/math/Vector2f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include @@ -12,31 +13,31 @@ namespace rwe { Vector2f a(1.0f, 0.0f); Vector2f b(1.0f, 0.0f); - REQUIRE(a.angleTo(b) == Approx(0.0f)); + REQUIRE(a.angleTo(b) == Catch::Approx(0.0f)); } SECTION("works for perpendicular vectors") { Vector2f a(1.0f, 0.0f); Vector2f b(0.0f, 1.0f); - REQUIRE(a.angleTo(b) == Approx(Pif / 2.0f)); + REQUIRE(a.angleTo(b) == Catch::Approx(Pif / 2.0f)); } SECTION("works for perpendicular vectors with negative angle") { Vector2f a(0.0f, 1.0f); Vector2f b(1.0f, 0.0f); - REQUIRE(a.angleTo(b) == Approx(-Pif / 2.0f)); + REQUIRE(a.angleTo(b) == Catch::Approx(-Pif / 2.0f)); } SECTION("works for opposite vectors") { Vector2f a(0.0f, 1.0f); Vector2f b(0.0f, -1.0f); - REQUIRE(a.angleTo(b) == Approx(-Pif)); + REQUIRE(a.angleTo(b) == Catch::Approx(-Pif)); } SECTION("works for vectors not of the same length") { Vector2f a(0.0f, 1.0f); Vector2f b(-1.0f, 1.0f); - REQUIRE(a.angleTo(b) == Approx(Pif / 4.0f)); + REQUIRE(a.angleTo(b) == Catch::Approx(Pif / 4.0f)); } } } diff --git a/src/rwe/math/Vector3f.test.cpp b/src/rwe/math/Vector3f.test.cpp index 0c0273c66..2b40b4c96 100644 --- a/src/rwe/math/Vector3f.test.cpp +++ b/src/rwe/math/Vector3f.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -53,9 +54,9 @@ namespace rwe { Vector3f v(3.0, 4.0, 0.0); Vector3f n = v.normalized(); - REQUIRE(n.x == Approx(0.6f)); - REQUIRE(n.y == Approx(0.8f)); - REQUIRE(n.z == Approx(0.0f)); + REQUIRE(n.x == Catch::Approx(0.6f)); + REQUIRE(n.y == Catch::Approx(0.8f)); + REQUIRE(n.z == Catch::Approx(0.0f)); } } @@ -65,9 +66,9 @@ namespace rwe { Vector3f v(3.0, 4.0, 0.0); Vector3f n = v.normalizedOr(Vector3f(1.0f, 0.0f, 0.0f)); - REQUIRE(n.x == Approx(0.6f)); - REQUIRE(n.y == Approx(0.8f)); - REQUIRE(n.z == Approx(0.0f)); + REQUIRE(n.x == Catch::Approx(0.6f)); + REQUIRE(n.y == Catch::Approx(0.8f)); + REQUIRE(n.z == Catch::Approx(0.0f)); } SECTION("when length is zero, returns the default value") { @@ -153,21 +154,21 @@ namespace rwe Vector3f a(1.0f, 0.0f, 0.0f); Vector3f b(1.0f, 0.0f, 0.0f); Vector3f n(0.0f, 0.0f, 1.0f); - REQUIRE(angleTo(a, b, n) == Approx(0.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(0.0f)); } SECTION("Y") { Vector3f a(0.0f, 1.0f, 0.0f); Vector3f b(0.0f, 1.0f, 0.0f); Vector3f n(1.0f, 0.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(0.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(0.0f)); } SECTION("Z") { Vector3f a(0.0f, 0.0f, 1.0f); Vector3f b(0.0f, 0.0f, 1.0f); Vector3f n(0.0f, 1.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(0.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(0.0f)); } } SECTION("works for perpendicular vectors") @@ -177,21 +178,21 @@ namespace rwe Vector3f a(1.0f, 0.0f, 0.0f); Vector3f b(0.0f, 1.0f, 0.0f); Vector3f n(0.0f, 0.0f, 1.0f); - REQUIRE(angleTo(a, b, n) == Approx(Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(Pif / 2.0f)); } SECTION("Z -> X") { Vector3f a(0.0f, 0.0f, 1.0f); Vector3f b(1.0f, 0.0f, 0.0f); Vector3f n(0.0f, 1.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(Pif / 2.0f)); } SECTION("Y -> Z") { Vector3f a(0.0f, 1.0f, 0.0f); Vector3f b(0.0f, 0.0f, 1.0f); Vector3f n(1.0f, 0.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(Pif / 2.0f)); } } SECTION("works for perpendicular vectors with negative angle") @@ -201,21 +202,21 @@ namespace rwe Vector3f a(0.0f, 1.0f, 0.0f); Vector3f b(1.0f, 0.0f, 0.0f); Vector3f n(0.0f, 0.0f, 1.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif / 2.0f)); } SECTION("X -> Z") { Vector3f a(1.0f, 0.0f, 0.0f); Vector3f b(0.0f, 0.0f, 1.0f); Vector3f n(0.0f, 1.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif / 2.0f)); } SECTION("Z -> Y") { Vector3f a(0.0f, 0.0f, 1.0f); Vector3f b(0.0f, 1.0f, 0.0f); Vector3f n(1.0f, 0.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif / 2.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif / 2.0f)); } } SECTION("works for opposite vectors") @@ -225,7 +226,7 @@ namespace rwe Vector3f a(1.0f, 0.0f, 0.0f); Vector3f b(-1.0f, 0.0f, 0.0f); Vector3f n(0.0f, 0.0f, 1.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif)); } SECTION("Y") @@ -233,7 +234,7 @@ namespace rwe Vector3f a(0.0f, 1.0f, 0.0f); Vector3f b(0.0f, -1.0f, 0.0f); Vector3f n(1.0f, 0.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif)); } SECTION("Z") @@ -241,7 +242,7 @@ namespace rwe Vector3f a(0.0f, 0.0f, 1.0f); Vector3f b(0.0f, 0.0f, -1.0f); Vector3f n(0.0f, 1.0f, 0.0f); - REQUIRE(angleTo(a, b, n) == Approx(-Pif)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(-Pif)); } } SECTION("works for vectors not of the same length") @@ -249,7 +250,7 @@ namespace rwe Vector3f a(0.0f, 1.0f, 0.0f); Vector3f b(-1.0f, 1.0f, 0.0f); Vector3f n(-1.0f, 1.0f, 1.0f); - REQUIRE(angleTo(a, b, n) == Approx(Pif / 4.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(Pif / 4.0f)); } SECTION("it doesnt emit nan") { @@ -259,7 +260,7 @@ namespace rwe Vector3f a(171.99025, -0.00849914551, -38.2367249); Vector3f b(171.99025, 0, -38.2367249); Vector3f n(38.2367249, -0, 171.99025); - REQUIRE(angleTo(a, b, n) == Approx(0.0f)); + REQUIRE(angleTo(a, b, n) == Catch::Approx(0.0f)); } } diff --git a/src/rwe/math/rwe_math.test.cpp b/src/rwe/math/rwe_math.test.cpp index d52992025..2384c69b8 100644 --- a/src/rwe/math/rwe_math.test.cpp +++ b/src/rwe/math/rwe_math.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/network_util.test.cpp b/src/rwe/network_util.test.cpp index 24962fcad..f0a316436 100644 --- a/src/rwe/network_util.test.cpp +++ b/src/rwe/network_util.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/pathfinding/pathfinding_utils.test.cpp b/src/rwe/pathfinding/pathfinding_utils.test.cpp index 426f67967..b0f494387 100644 --- a/src/rwe/pathfinding/pathfinding_utils.test.cpp +++ b/src/rwe/pathfinding/pathfinding_utils.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/sim/GameHash_util.test.cpp b/src/rwe/sim/GameHash_util.test.cpp index cf614ffc0..1aeef2e8e 100644 --- a/src/rwe/sim/GameHash_util.test.cpp +++ b/src/rwe/sim/GameHash_util.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/sim/SimAngle.test.cpp b/src/rwe/sim/SimAngle.test.cpp index 698673fb1..b85a5bb03 100644 --- a/src/rwe/sim/SimAngle.test.cpp +++ b/src/rwe/sim/SimAngle.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include #include #include @@ -40,9 +41,9 @@ namespace rwe SECTION("converts SimAngle to radians") { REQUIRE(toRadians(SimAngle(0)) == RadiansAngle(0.0f)); - REQUIRE(toRadians(SimAngle(16384)).value == Approx(Pif / 2.0f)); - REQUIRE(toRadians(SimAngle(32768)).value == Approx(-Pif)); - REQUIRE(toRadians(SimAngle(49152)).value == Approx(-Pif / 2.0f)); + REQUIRE(toRadians(SimAngle(16384)).value == Catch::Approx(Pif / 2.0f)); + REQUIRE(toRadians(SimAngle(32768)).value == Catch::Approx(-Pif)); + REQUIRE(toRadians(SimAngle(49152)).value == Catch::Approx(-Pif / 2.0f)); } rc::prop("fromRadians inverts toRadians", [](SimAngle a) { diff --git a/src/rwe/sim/SimVector.test.cpp b/src/rwe/sim/SimVector.test.cpp index 106b54577..1180ef272 100644 --- a/src/rwe/sim/SimVector.test.cpp +++ b/src/rwe/sim/SimVector.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace rwe diff --git a/src/rwe/sim/UnitState_util.test.cpp b/src/rwe/sim/UnitState_util.test.cpp index 6672fd77b..45a87e8f9 100644 --- a/src/rwe/sim/UnitState_util.test.cpp +++ b/src/rwe/sim/UnitState_util.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include namespace rwe diff --git a/src/rwe/sim/util.test.cpp b/src/rwe/sim/util.test.cpp index d04f2b840..7d29b5f45 100644 --- a/src/rwe/sim/util.test.cpp +++ b/src/rwe/sim/util.test.cpp @@ -1,4 +1,5 @@ -#include +#include +#include #include namespace rwe @@ -73,9 +74,9 @@ namespace rwe // still introduces a bit of fp error due to conversion to radians auto barExpectedPosition = SimVector(31_ss, 22_ss, -7_ss); auto barActualPosition = getPieceTransform("bar", modelDef, pieces) * SimVector(0_ss, 0_ss, 0_ss); - REQUIRE(barActualPosition.x.value == Approx(barExpectedPosition.x.value)); - REQUIRE(barActualPosition.y.value == Approx(barExpectedPosition.y.value)); - REQUIRE(barActualPosition.z.value == Approx(barExpectedPosition.z.value)); + REQUIRE(barActualPosition.x.value == Catch::Approx(barExpectedPosition.x.value)); + REQUIRE(barActualPosition.y.value == Catch::Approx(barExpectedPosition.y.value)); + REQUIRE(barActualPosition.z.value == Catch::Approx(barExpectedPosition.z.value)); } } } diff --git a/src/rwe/util/OpaqueArgs.test.cpp b/src/rwe/util/OpaqueArgs.test.cpp index a10e01d43..2c22d05af 100644 --- a/src/rwe/util/OpaqueArgs.test.cpp +++ b/src/rwe/util/OpaqueArgs.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/util/Result.test.cpp b/src/rwe/util/Result.test.cpp index 1e228fa6c..02d8333dc 100644 --- a/src/rwe/util/Result.test.cpp +++ b/src/rwe/util/Result.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/util/SimpleLogger.test.cpp b/src/rwe/util/SimpleLogger.test.cpp index 23e9b2321..952299dd7 100644 --- a/src/rwe/util/SimpleLogger.test.cpp +++ b/src/rwe/util/SimpleLogger.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include #include diff --git a/src/rwe/util/SpanStream.test.cpp b/src/rwe/util/SpanStream.test.cpp index efd27664f..7f4cf23f9 100644 --- a/src/rwe/util/SpanStream.test.cpp +++ b/src/rwe/util/SpanStream.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include #include diff --git a/src/rwe/util/rwe_string.test.cpp b/src/rwe/util/rwe_string.test.cpp index f92f61b33..635dcd3c3 100644 --- a/src/rwe/util/rwe_string.test.cpp +++ b/src/rwe/util/rwe_string.test.cpp @@ -1,4 +1,4 @@ -#include +#include #include diff --git a/src/test.cpp b/src/test.cpp deleted file mode 100644 index 4ed06df1f..000000000 --- a/src/test.cpp +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include From 25d27b19b3a24dd973f4422e2bb1a8c98e788374 Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 10:51:39 -0700 Subject: [PATCH 31/32] Bump cmake_minimum_required to 3.16, add policy compat for CMake 4.x --- CMakeLists.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index e87d1602f..bea34cb0f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,9 @@ -cmake_minimum_required(VERSION 3.11) +cmake_minimum_required(VERSION 3.16) + +# Allow submodules with older cmake_minimum_required to configure under CMake 4.x +if(CMAKE_VERSION VERSION_GREATER_EQUAL "4.0") + set(CMAKE_POLICY_VERSION_MINIMUM 3.5) +endif() # Generate compile_commands.json for clangd-based editor integration # (VS Code, Zed, etc.). The .clangd file in the repo root points From 0679463f593717aeb92f3e5e61e1c7aa156ddc9d Mon Sep 17 00:00:00 2001 From: Kevin Hake Date: Tue, 24 Mar 2026 11:12:33 -0700 Subject: [PATCH 32/32] Revert WiX, install NSIS on Windows CI runners --- .github/workflows/build.yml | 10 ++++++---- CMakeLists.txt | 10 ++++++---- LICENSE.txt => LICENSE | 0 3 files changed, 12 insertions(+), 8 deletions(-) rename LICENSE.txt => LICENSE (100%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1ba30c543..e18eb2846 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -123,8 +123,8 @@ jobs: working-directory: launcher run: npm run package - run: python3 fetch-msvc-libs.py - - name: install sccache - run: choco install sccache -y + - name: install tools + run: choco install sccache nsis -y - name: restore sccache uses: actions/cache@v5 with: @@ -154,7 +154,7 @@ jobs: uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MSVC ${{ matrix.configuration }} - path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.msi + path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe if-no-files-found: error build-windows-mingw64: strategy: @@ -185,6 +185,8 @@ jobs: - name: package launcher working-directory: launcher run: npm run package + - name: install nsis + run: choco install nsis -y - uses: msys2/setup-msys2@v2 with: msystem: MINGW64 @@ -254,5 +256,5 @@ jobs: uses: actions/upload-artifact@v6 with: name: RWE Installer ${{ runner.os }} MinGW64 ${{ matrix.configuration }} - path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.msi + path: build/dist/RobotWarEngine-*-Windows-${{ matrix.configuration }}.exe if-no-files-found: error diff --git a/CMakeLists.txt b/CMakeLists.txt index bea34cb0f..8e3732229 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -873,19 +873,19 @@ add_test(NAME rwe_test COMMAND rwe_test) install(TARGETS rwe rwe_bridge RUNTIME DESTINATION . ) -install(FILES LICENSE.txt README.md DESTINATION .) +install(FILES LICENSE README.md DESTINATION .) install(DIRECTORY shaders DESTINATION .) install(DIRECTORY launcher/rwe-launcher-win32-x64/ DESTINATION launcher) -set(CPACK_GENERATOR "ZIP;WIX") +set(CPACK_GENERATOR "ZIP;NSIS") set(CPACK_PACKAGE_VERSION ${RWE_GIT_DESCRIPTION}) set(CPACK_PACKAGE_FILE_NAME "RobotWarEngine-${RWE_GIT_DESCRIPTION}-${CMAKE_SYSTEM_NAME}-${CMAKE_BUILD_TYPE}") -set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE.txt") +set(CPACK_RESOURCE_FILE_LICENSE "${PROJECT_SOURCE_DIR}/LICENSE") set(CPACK_RESOURCE_FILE_README "${PROJECT_SOURCE_DIR}/README.md") set(CPACK_PACKAGE_EXECUTABLES @@ -896,6 +896,8 @@ set(CPACK_PACKAGE_VENDOR "Michael Heasell") set(CPACK_OUTPUT_FILE_PREFIX "dist") -set(CPACK_WIX_UPGRADE_GUID "9BF7A834-221F-4790-BDB5-136D805BD39E") +set(CPACK_NSIS_EXECUTABLES_DIRECTORY ".") + +set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL ON) include(CPack) diff --git a/LICENSE.txt b/LICENSE similarity index 100% rename from LICENSE.txt rename to LICENSE