diff --git a/AGENTS.md b/AGENTS.md index 0f87ef9915c..4d346e24500 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,18 +8,49 @@ RecoilEngine is an open-source real-time strategy (RTS) game engine written in C ## Build Commands +### Submodules + +The repo uses git submodules for vendored libraries (`rts/lib/*`, `tools/pr-downloader`, AI skirmish bots, etc.). If you cloned without `--recurse-submodules`, initialize them before building: +```bash +git submodule update --init --recursive +``` + ### Building the Engine **Using Docker (Recommended):** ```bash -# Build for Linux +# Full build (default: RELWITHDEBINFO, -O3 -g -DNDEBUG, Ninja) +# Output lands in build--/ (e.g. build-amd64-linux/) and the +# ready-to-use install in build-amd64-linux/install/ docker-build-v2/build.sh linux -# Build for Windows +# Parallelism +docker-build-v2/build.sh -j 8 linux + +# Windows cross-build docker-build-v2/build.sh windows -# Build with custom CMake options +# Change optimization level — trailing -D… is forwarded to configure.sh and +# overrides the baked-in RELWITHDEBINFO default. docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=DEBUG +docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=RELEASE +docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=PROFILE + +# Combine cmake options (configure phase) +docker-build-v2/build.sh linux -DBUILD_spring-headless=OFF -DTRACY_ENABLE=ON + +# List all available cmake options and their current values +docker-build-v2/build.sh --configure linux -LH + +# Build a specific target — use --compile so args flow to `cmake --build`, +# not to configure. Without --compile, `-t …` would be rejected by configure. +docker-build-v2/build.sh --compile linux -t engine-headless +docker-build-v2/build.sh --compile linux -t engine-legacy +docker-build-v2/build.sh --compile linux -t tests --verbose + +# Split the phases +docker-build-v2/build.sh --configure linux # configure only +docker-build-v2/build.sh --compile linux # compile only (reuses existing config) ``` **Without Docker:** @@ -27,16 +58,44 @@ docker-build-v2/build.sh linux -DCMAKE_BUILD_TYPE=DEBUG # Create build directory mkdir -p build && cd build -# Configure +# Configure — project requires C++23 (clang ≥ 17 or gcc ≥ 13 on PATH). +# CMAKE_BUILD_TYPE defaults to RELWITHDEBINFO when omitted. cmake .. -# Build specific target -cmake --build . --target engine-headless -j$(nproc) - -# Build all -cmake --build . -j$(nproc) +# Optional: Default generator is Unix Makefiles; add `-G Ninja` for faster builds if ninja is installed. +cmake -G Ninja .. + +# Optional: pin to gcc-13 + gold linker via the in-repo toolchain file +# (tracked under docker-build-v2/; same compiler the docker build uses). +cmake \ + -DCMAKE_TOOLCHAIN_FILE=../docker-build-v2/images/all-linux/toolchain.cmake .. + +# Optional: speed up incremental builds with ccache +cmake \ + -DCMAKE_C_COMPILER_LAUNCHER=ccache \ + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache .. + +# Change optimization level by re-running cmake (no wipe required): +cmake -DCMAKE_BUILD_TYPE=DEBUG .. # no optimization, full symbols +cmake -DCMAKE_BUILD_TYPE=RELEASE .. # optimized, no debug info +cmake -DCMAKE_BUILD_TYPE=RELWITHDEBINFO .. # optimized + debug info (default) +cmake -DCMAKE_BUILD_TYPE=PROFILE .. # optimized + profiling hooks + +# Build (generator-agnostic — works under Ninja or Make) +cmake --build . + +# Build a specific target +cmake --build . --target engine-headless +cmake --build . --target engine-legacy +cmake --build . --target engine-dedicated +cmake --build . --target tests ``` +> The docker flow writes to **`build--/`** (e.g. `build-amd64-linux/` +> or `build-amd64-windows/`), which is a different directory than the `build/` +> used by this flow. When running tests, point commands at whichever build +> directory you populated. + ### Build Types - `DEBUG` - Debug build with full symbols and no optimization - `RELEASE` - Optimized release build @@ -44,55 +103,64 @@ cmake --build . -j$(nproc) - `PROFILE` - Profiling build ### Build Targets -- `engine-legacy` - Main engine build -- `engine-headless` - Headless server build -- `engine-dedicated` - Dedicated server build -- `tests` - Build all test executables -- `check` - Build and run all tests -- `spring-content` - Build game content packages +- `engine-legacy` — main interactive engine build +- `engine-headless` — headless engine (no graphics) +- `engine-dedicated` — dedicated server +- `unitsync` — unitsync shared library +- `pr-downloader` — content downloader tool +- `tests` — phony; builds every `test_*` executable under `build/test/` +- `check` — phony; depends on `engine-headless` + all `test_*` executables, then runs ctest with `--output-on-failure -V` +- `install` — install into `CMAKE_INSTALL_PREFIX` ## Testing -### Test Framework -The project uses **Catch2** for unit testing. Test files are located in the `test/` directory. +### Writing Tests +See `test/AGENTS.md` for details on writing tests, available compile flags, patterns, and test helpers. ### Running Tests **Build and run all tests:** ```bash -# From build directory -make tests # Build all test executables -make check # Build and run all tests via CTest -make test # Alternative: run via CTest +# From build/ — ctest / check recipes below assume a non-docker build. +# For a docker build, run `docker-build-v2/build.sh --compile linux -t check` +# (runs ctest inside the container) or invoke the binaries in +# build-amd64-linux/test/ directly. +cmake --build . --target tests # build all test executables (no run) +ctest # run all tests (does not rebuild) +# OR +cmake --build . --target check # rebuild engine-headless + all tests, then run ctest -V ``` -**Run a single test:** +`check` is the safe default when iterating; bare `ctest` is faster when nothing relevant has changed since the last build. + +**Run a single test (from repo root):** ```bash -# Tests are built as executable binaries in the build directory -# Pattern: test_ +# Tests are built as executable binaries under /test/ +# Pattern: /test/test_ +# where depends on if you built in docker or not (see above). -# Run specific test executable -./test_Float3 -./test_Matrix44f -./test_SyncedPrimitive -./test_UDPListener +./build/test/test_Float3 +./build/test/test_Matrix44f +./build/test/test_SyncedPrimitive +./build/test/test_UDPListener -# Run with verbose output -./test_Float3 -s +# Catch2: show passing assertions too +./build/test/test_Float3 -s -# Run specific test case -./test_Float3 "TestSection" +# Run a specific test case by name (positional arg matches TEST_CASE name, supports wildcards) +./build/test/test_Float3 "Float3" +./build/test/test_Float3 "Float34_*" ``` -**Run via CTest:** +**Run via CTest (from inside build/):** ```bash -# Run specific test by name -ctest -R Float3 -V +# Filter by regex, show output only on failure +ctest -R Float3 --output-on-failure -# Run with regex pattern -ctest -R Matrix -V +# Same, but verbose (full stdout regardless of result) +ctest -R Float3 -V -# List all available tests +# List all registered tests without running ctest -N ``` @@ -325,20 +393,6 @@ Use preprocessor directives for platform-specific code: - Use tabs for indentation in CMake files - Keep lines reasonably short -### Adding Tests -In `test/CMakeLists.txt`: -```cmake -set(test_name TestName) -set(test_src - "${CMAKE_CURRENT_SOURCE_DIR}/path/to/TestFile.cpp" - ${test_Common_sources} -) -set(test_libs - library_name -) -add_spring_test(${test_name} "${test_src}" "${test_libs}" "${test_flags}") -``` - ## Project Structure - `rts/` - Main engine source code @@ -388,6 +442,10 @@ The engine uses custom thread pools. See `THREADPOOL` define and related code. 4. Follow the workflow in `contributing.md` 5. Disclose any AI assistance used +### Additional docs +Please see @coding-agents/ for additional documentation: +- coding-agents/ENGINE_PERFORMANCE.md — notes on scale targets and engine performance internals. Useful for performance related changes. +- coding-agents/BACKWARDS_COMPATIBILITY.md - notes on when we should strive to be backwards compatible. Reference it for any major reworks or api changes. ## Additional Resources - Official website: https://recoilengine.org diff --git a/AI/Wrappers/CUtils/Util.c b/AI/Wrappers/CUtils/Util.c index c33d2e418e8..4764a619900 100644 --- a/AI/Wrappers/CUtils/Util.c +++ b/AI/Wrappers/CUtils/Util.c @@ -487,11 +487,7 @@ static void util_initFileSelector(const char* suffix) { fileSelectorSuffix = suffix; } -#if defined(__APPLE__) -static int util_fileSelector(struct dirent* fileDesc) { -#else static int util_fileSelector(const struct dirent* fileDesc) { -#endif return util_endsWith(fileDesc->d_name, fileSelectorSuffix); } diff --git a/coding-agents/BACKWARDS_COMPATIBILITY.md b/coding-agents/BACKWARDS_COMPATIBILITY.md new file mode 100644 index 00000000000..d36c3775a78 --- /dev/null +++ b/coding-agents/BACKWARDS_COMPATIBILITY.md @@ -0,0 +1,25 @@ +# Backwards compatibility + +Recoil isn't 100% beholden to backwards compatibility, but breaking changes are weighed carefully against their benefit. Backwards compatiblity is a constraint, not a veto. + +## Why the bar is high + +Recoil games aren't short-cycle Unreal/Unity titles — they're lifetime hobby projects. There is no steady stream of new games picking up the latest engine; the games we have are the games we have. They fall into two camps, and neither absorbs churn well: + +- **Mature games** need stability above all else. +- **Games still in active development** have the flexibility, but rarely the volunteer bandwidth to chase significant engine breakage. + +## How to weigh a change + +- Quantify the benefit (perf, correctness, maintainability) concretely, not in the abstract. +- Identify which games or content would break, and how mechanical the fix is on their side. "Rename a call site" is very different from "rearchitect your gadget." +- Prefer changes whose blast radius is contained or whose adaptation is mechanical. Avoid changes that force games to rethink core logic with no real mitigation path. + +## Precedents + +- **Multi-threaded unit movement & collision** — landed with a large perf win and effectively no game-side impact (ignoring incidentally-fixed bugs). This is the shape of change to look for. +- **Multi-threading `Unit::Update`, `Unit::SlowUpdate`, or projectiles** — don't. The impact on games would be huge and there isn't much that can be done to mitigate it. Not a path worth proposing. + +## The upshot + +Backwards compatiblity constraints don't close the door on performance work — they just point it at the areas where the blast radius is small. Plenty of wins are still on the table; pick the ones games don't have to pay for. diff --git a/coding-agents/ENGINE_PERFORMANCE.md b/coding-agents/ENGINE_PERFORMANCE.md new file mode 100644 index 00000000000..f3bb67f5d77 --- /dev/null +++ b/coding-agents/ENGINE_PERFORMANCE.md @@ -0,0 +1,55 @@ +# Engine performance + +Recoil is an RTS engine built for large-scale games — designed to handle thousands of units at once. + +## Scale target + +- **Target:** ~10k concurrent units, *including buildings*. +- Mobile units tend to be ~40% of that late-game total. +- Largest seen in a real game: ~17.7k units. That's a data point, not a design target. + +## Sim, draw, and update frames + +The main loop is `Update → Draw`, repeating — see the diagram and table below for the per-phase breakdown. Each iteration first drains any queued sim-frame packets (0..N per iteration), then renders one draw frame. `CGame::Update` dispatches `SimFrame()` calls as `NETMSG_NEWFRAME` packets arrive; `CGame::Draw` runs the unsynced update phase and then renders. The sim burst is capped at ~500 ms (`minDrawFPS`) so draw always gets to run, and it's all one thread — sim and rendering are **not concurrent**; parallelism only happens *inside* a phase. + +Conversely, if no sim frames are in the queue the main loop runs `Draw`/`UpdateUnsynced` as fast as possible — many draw iterations can pass between successive sim frames, with visuals interpolating smoothly in between via `globalRendering->timeOffset`. + +``` +main-loop iteration (repeats as fast as possible) +├── CGame::Update (mostly synced) +│ └── SimFrame × 0..N ← processes queued sim frames capped at +| ~500ms per iteration +└── CGame::Draw (unsynced) + ├── UpdateUnsynced ← unsynced update phase + └── render world + screen ← Draw::World + Draw::Screen +``` + +| Phase | Rate | Synced? | Responsibility | +|---|---|---|---| +| **Sim frame** — `CGame::SimFrame` | fixed 30 Hz (`GAME_SPEED`) | mostly yes | advance deterministic state: units, pathing, projectiles, line-of-sight, scripts, Lua `GameFrame` | +| **Draw frame** — `CGame::Draw` | variable | no | update phase (see below) + render world/screen | +| **Update phase** — `CGame::UpdateUnsynced` *(inside draw frame)* | per draw frame | no | timings, interpolation, camera, GUI, sound, world-drawer prep | + +### Profiler buckets + +The engine `CTimeProfiler` (and the `benchmark` tool) report three peer buckets: `Sim` (the whole synced step), `Update` (`CGame::UpdateUnsynced`), and `Draw` (rendering only, *excluding* the Update that runs first). + +`Sim` is **"mostly" synced**: it also bills unsynced work that runs inline during `SimFrame`. +- **Explicit Lua callins** — `GameFrame`/`GameFramePost` run near the start of each sim frame. +- **Event-driven Lua callins** — unsynced widgets can subscribe to synced game events, so their handlers run inline as those events fire during the frame. +- **C++-only unsynced sections** — e.g. the MT projectile visual pass (`Sim::Projectiles::UpdateUnsyncedMT`) and ghosted-building updates (`CUnitDrawer::UpdateGhostedBuildings`). + +### Scheduling and CPU budget + +- Sim has a target rate set by the server; draw is as fast as the hardware allows. The sim target is `30 Hz × speedFactor`; at a speed factor of 1x, in-game time tracks real-world time 1:1, and at 2x speed the server fires twice as many sim frames per real-world second so the world evolves twice as fast. +- **Zero, one, or many** sim frames per draw frame — if the client falls behind, pending sim frames burst in the next iteration to catch up. +- Visuals interpolate between sim frames, so draw rate can exceed sim rate without stutter. +- Sim time is carefully budgeted and scheduled against draw frames (because they run serially) so there's always a minimum fps for the player + +## Multi-threading + +The engine runs one **main thread** plus a pool of **worker threads**, all pinned to distinct cores. We typically aim for 6-8 worker threads. The main thread drives the sim/draw loop; workers pick up parallel work dispatched from the main thread (via `for_mt` and friends in `rts/System/Threading/ThreadPool.h`). The main thread also participates in draining the task queue while it waits. + +Most parallel work in the engine is **homogeneous** — the same operation applied over many items (unit updates, projectile steps, etc.) via `for_mt`. Keeping parallel work homogeneous is a deliberate discipline: it makes determinism easier to reason about and keeps sim output independent of how work happens to land across threads. + +**QTPFS is the one heterogeneous exception.** The quad-tree pathfinder maintains its own per-worker search state (`SearchThreadData`, `SparseData`) independent of engine sim state, which lets it safely run path searches on the worker pool *in the background* via `for_mt_background`. Background tasks yield to higher-priority work by rescheduling themselves when other jobs arrive, so QTPFS soaks up idle worker capacity without preempting foreground parallelism. diff --git a/doc/site/content/_index.md b/doc/site/content/_index.md index fcbbe83428f..0d0a8d3b14b 100644 --- a/doc/site/content/_index.md +++ b/doc/site/content/_index.md @@ -19,7 +19,7 @@ draft = false {{< /cards >}} {{< cards >}} {{< card title="Tech Annihilation" image="/showcase/ta.jpeg" link="https://github.com/techannihilation/TA" >}} -{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="splinterfaction.info" >}} +{{< card title="SplinterFaction" image="showcase/splinter_faction.jpg" link="https://splinterfaction.info" >}} {{< card title="Mechcommander: Legacy" image="/showcase/mcl.jpg" link="https://github.com/SpringMCLegacy/SpringMCLegacy/wiki" >}} {{< /cards >}} diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 55096582c3a..0e073538cb7 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -74,7 +74,7 @@ This is the bleeding-edge changelog since version 2025.06, for **pre-release 202 - add `Spring.GetClosestEnemyUnit(x, y, z, range = inf, allyTeamID, bool useLoS = true, bool spherical = false, bool requireEnemyToSeePos = false) → unitID?` to LuaRules. - large QTPFS perf improvements. - always output logs to stdout. -- add `Engine.isHeadless`, available in unsynced only. +- add boolean `Platform.isHeadless`. - archive cache version 20 → 21. ## Fixes diff --git a/docker-build-v2/build.sh b/docker-build-v2/build.sh index 765ba4a1495..6bded8b8b57 100755 --- a/docker-build-v2/build.sh +++ b/docker-build-v2/build.sh @@ -179,7 +179,16 @@ if [[ "$GIT_DIR" != "$GIT_COMMON_DIR" ]]; then WORKTREE_MOUNTS="-v $GIT_COMMON_DIR:$GIT_COMMON_DIR:ro" fi -$RUNTIME run --platform=linux/$ARCH -it --rm \ +# Docker's -t requires stdin AND stdout to be TTYs; in CI, pipes, or agent +# contexts one or both are missing and docker errors out with "the input +# device is not a TTY". Only add -t when it's safe; -i is harmless either +# way (non-interactive stdin just sees EOF). +TTY_FLAG= +if [[ -t 0 && -t 1 ]]; then + TTY_FLAG=-t +fi + +$RUNTIME run --platform=linux/$ARCH -i $TTY_FLAG --rm \ -v "$CWD${P}":/build/src:z,ro \ -v "$CWD${P}.cache${P}ccache-$PLATFORM":/build/cache:z,rw \ -v "$CWD${P}build-$PLATFORM":/build/out:z,rw \ diff --git a/rts/Lua/LuaConstEngine.cpp b/rts/Lua/LuaConstEngine.cpp index 068a26d0ecf..0d38c1ffbf0 100644 --- a/rts/Lua/LuaConstEngine.cpp +++ b/rts/Lua/LuaConstEngine.cpp @@ -59,9 +59,6 @@ bool LuaConstEngine::PushEntries(lua_State* L) LuaPushNamedString(L, "buildFlags" , SpringVersion::GetAdditional()); LuaPushNamedNumber(L, "wordSize", (!CLuaHandle::GetHandleSynced(L))? Platform::NativeWordSize() * 8: 0); - if (!CLuaHandle::GetHandleSynced(L)) - LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless()); - LuaPushNamedNumber(L, "gameSpeed", GAME_SPEED); LuaPushNamedNumber(L, "maxCustomPaletteID", MAX_CUSTOM_COLORS - 1); diff --git a/rts/Lua/LuaConstPlatform.cpp b/rts/Lua/LuaConstPlatform.cpp index 0a5f667ed80..7262bd7d5a5 100644 --- a/rts/Lua/LuaConstPlatform.cpp +++ b/rts/Lua/LuaConstPlatform.cpp @@ -2,6 +2,7 @@ #include "LuaConstPlatform.h" #include "LuaUtils.h" +#include "Game/GameVersion.h" #include "System/Platform/Hardware.h" #include "System/Platform/Misc.h" #include "Rendering/GlobalRendering.h" @@ -147,5 +148,8 @@ bool LuaConstPlatform::PushEntries(lua_State* L) /*** @field Platform.macAddrHash string */ LuaPushNamedString(L, "macAddrHash", Platform::GetMacAddrHash()); + /*** @field Platform.isHeadless boolean Is this a headless build which only simulates and doesnt offer interactive IO? */ + LuaPushNamedBool(L, "isHeadless", SpringVersion::IsHeadless()); + return true; } diff --git a/rts/Lua/LuaSyncedRead.cpp b/rts/Lua/LuaSyncedRead.cpp index 40b25741b52..3983a71f3ff 100644 --- a/rts/Lua/LuaSyncedRead.cpp +++ b/rts/Lua/LuaSyncedRead.cpp @@ -399,6 +399,8 @@ bool LuaSyncedRead::PushEntries(lua_State* L) REGISTER_LUA_CFUNC(TraceRayGroundInDirection); REGISTER_LUA_CFUNC(TraceRayGroundBetweenPositions); + REGISTER_LUA_CFUNC(TraceRayInDirection); + REGISTER_LUA_CFUNC(TraceRayBetweenPositions); REGISTER_LUA_CFUNC(GetRadarErrorParams); @@ -9011,6 +9013,142 @@ int LuaSyncedRead::GetUnitScriptNames(lua_State* L) return 1; } + +static int TraceRayImpl(lua_State *const L, const float3 &pos, const float3 &dir, const float maxLen, std::string_view type) +{ + if (type != "unit" && type != "feature" && type != "both") + return luaL_error(L, "invalid type '%s', expected 'unit', 'feature', or 'both'", type.data()); + + const bool testUnits = (type == "unit" || type == "both"); + const bool testFeatures = (type == "feature" || type == "both"); + + QuadFieldQuery qfQuery; + quadField.GetQuadsOnRay(qfQuery, pos, dir, maxLen); + + spring::unordered_set testedUnitIDs; + spring::unordered_set testedFeatureIDs; + std::vector > hits; + + for (const int quadIdx : *qfQuery.quads) { + const CQuadField::Quad& quad = quadField.GetQuad(quadIdx); + + if (testUnits) { + for (const auto *unit : quad.units) { + if (!unit->HasCollidableStateBit(CSolidObject::CSTATE_BIT_QUADMAPRAYS)) + continue; + + if (!testedUnitIDs.insert(unit->id).second) + continue; + + if (!LuaUtils::IsUnitInLos(L, unit)) + continue; + + CollisionQuery cq; + if (CCollisionHandler::DetectHit(unit, unit->GetTransformMatrix(true), pos, pos + dir * maxLen, &cq, true)) { + const float len = cq.GetHitPosDist(pos, dir); + if (len > maxLen) // possibly a bug in CCollisionHandler::DetectHit? + continue; + hits.emplace_back(len, unit->id, "unit"); + } + } + } + + if (testFeatures) { + for (const auto *feature : quad.features) { + if (!feature->HasCollidableStateBit(CSolidObject::CSTATE_BIT_QUADMAPRAYS)) + continue; + + if (!testedFeatureIDs.insert(feature->id).second) + continue; + + if (!LuaUtils::IsFeatureVisible(L, feature)) + continue; + + CollisionQuery cq; + if (CCollisionHandler::DetectHit(feature, feature->GetTransformMatrix(true), pos, pos + dir * maxLen, &cq, true)) { + const float len = cq.GetHitPosDist(pos, dir); + if (len > maxLen) + continue; + hits.emplace_back(len, feature->id, "feature"); + } + } + } + } + + std::stable_sort(hits.begin(), hits.end(), [] (const auto& a, const auto& b) { + return std::get<0>(a) < std::get<0>(b); + }); + + lua_createtable(L, hits.size(), 0); + + int num = 0; + for (const auto& [hitLength, objectID, objectType] : hits) { + lua_createtable(L, 3, 0); + + lua_pushnumber(L, hitLength); + lua_rawseti(L, -2, 1); + lua_pushnumber(L, objectID); + lua_rawseti(L, -2, 2); + lua_pushstring(L, objectType); + lua_rawseti(L, -2, 3); + + lua_rawseti(L, -2, ++num); + } + + return 1; +} + +/*** Traces a ray from a position in a direction + * + * @function Spring.TraceRayInDirection + * + * Returns all unit and/or feature hits along a ray, sorted by distance + * from the start position. + * + * @param posX number + * @param posY number + * @param posZ number + * @param dirX number + * @param dirY number + * @param dirZ number + * @param maxLength number + * @param type string Object type to test: `"unit"`, `"feature"`, or `"both"` + * @return table[] hits Array of `{hitLength, objectID, objectType}` entries + */ +int LuaSyncedRead::TraceRayInDirection(lua_State* L) +{ + float3 pos(luaL_checkfloat(L, 1), luaL_checkfloat(L, 2), luaL_checkfloat(L, 3)); + float3 dir(luaL_checkfloat(L, 4), luaL_checkfloat(L, 5), luaL_checkfloat(L, 6)); + const float maxLen = luaL_optfloat(L, 7, 999999.f); + const char* type = luaL_checkstring(L, 8); + return TraceRayImpl(L, pos, dir, maxLen, type); +} + +/*** Traces a ray between two positions + * + * @function Spring.TraceRayBetweenPositions + * + * Checks for unit and/or feature collisions between two positions + * and returns all hits sorted by distance from the start position. + * + * @param startX number + * @param startY number + * @param startZ number + * @param endX number + * @param endY number + * @param endZ number + * @param type string Object type to test: `"unit"`, `"feature"`, or `"both"` + * @return table[] hits Array of `{hitLength, objectID, objectType}` entries + */ +int LuaSyncedRead::TraceRayBetweenPositions(lua_State* L) +{ + float3 start(luaL_checkfloat(L, 1), luaL_checkfloat(L, 2), luaL_checkfloat(L, 3)); + float3 end(luaL_checkfloat(L, 4), luaL_checkfloat(L, 5), luaL_checkfloat(L, 6)); + const char* type = luaL_checkstring(L, 7); + const auto [dir, length] = (end - start).GetNormalized(); + return TraceRayImpl(L, start, dir, length, type); +} + static int TraceRayGroundImpl(lua_State *const L, const float3 &pos, const float3 &dir, const float maxLen, const bool testWater) { const float rayLength = CGround::LineGroundWaterCol(pos, dir, maxLen, testWater, CLuaHandle::GetHandleSynced(L)); diff --git a/rts/Lua/LuaSyncedRead.h b/rts/Lua/LuaSyncedRead.h index 98c632ad9b0..dbec13c0ab7 100644 --- a/rts/Lua/LuaSyncedRead.h +++ b/rts/Lua/LuaSyncedRead.h @@ -302,9 +302,8 @@ class LuaSyncedRead { static int GetRadarErrorParams(lua_State* L); - static int TraceRay(lua_State* L); //TODO: not implemented - static int TraceRayUnits(lua_State* L); //TODO: not implemented - static int TraceRayFeatures(lua_State* L); //TODO: not implemented + static int TraceRayInDirection(lua_State* L); + static int TraceRayBetweenPositions(lua_State* L); static int TraceRayGroundBetweenPositions(lua_State* L); static int TraceRayGroundInDirection(lua_State* L); }; diff --git a/rts/Lua/LuaTextures.cpp b/rts/Lua/LuaTextures.cpp index 8d3a083dbef..0ae691d63c0 100644 --- a/rts/Lua/LuaTextures.cpp +++ b/rts/Lua/LuaTextures.cpp @@ -90,7 +90,9 @@ std::string LuaTextures::Create(const Texture& tex) } break; } - if (glGetError() != GL_NO_ERROR) { + if (const GLenum texErr = glGetError(); texErr != GL_NO_ERROR) { + LOG_L(L_ERROR, "[LuaTextures::%s] glTexImage failed: target=0x%x size=%dx%d fmt=0x%x dataFmt=0x%x dataType=0x%x border=%d glError=0x%x", + __func__, tex.target, tex.xsize, tex.ysize, tex.format, dataFormat, dataType, tex.border, texErr); glDeleteTextures(1, &texID); glBindTexture(tex.target, currentBinding); return ""; diff --git a/rts/Rendering/IconHandler.h b/rts/Rendering/IconHandler.h index 6c67749d739..ec0e5bce6e3 100644 --- a/rts/Rendering/IconHandler.h +++ b/rts/Rendering/IconHandler.h @@ -13,7 +13,7 @@ #include "Rendering/GL/RenderBuffersFwd.h" #include "Rendering/Textures/TextureAtlas.h" -class UnitDef; +struct UnitDef; class CTextureRenderAtlas; namespace icon { diff --git a/rts/Rml/SolLua/bind/Context.cpp b/rts/Rml/SolLua/bind/Context.cpp index db0dcb8960b..bd40fd4e535 100644 --- a/rts/Rml/SolLua/bind/Context.cpp +++ b/rts/Rml/SolLua/bind/Context.cpp @@ -131,7 +131,7 @@ struct lua_iterator_state sol::state_view l{s}; int index = 0; int count = keytable.size(); - while (keytable.get(++index).get_type() != sol::type::nil && index <= count) { + while (keytable.get(++index).get_type() != sol::type::lua_nil && index <= count) { this->keys.emplace_back(sol::object(l, sol::in_place, index)); } } else { @@ -199,7 +199,7 @@ createNewIndexFunction(std::shared_ptr data, const } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign prop.as().raw_set(solkey, value_raw.as().call(value)); } else { @@ -290,7 +290,7 @@ sol::table openDataModel(Rml::Context& self, const Rml::String& name, sol::objec } if (value.is()) { auto value_raw = value.as().raw_get("__raw"); - if (value_raw != sol::nil && value_raw.is()) { + if (value_raw != sol::lua_nil && value_raw.is()) { // new value is a datamodel proxy, so get the underlying table to assign data->Table.raw_set(key, value_raw.as().call(value)); } else { diff --git a/rts/Rml/SolLua/bind/Element.cpp b/rts/Rml/SolLua/bind/Element.cpp index e2edf19159a..3bf3ecc7fea 100644 --- a/rts/Rml/SolLua/bind/Element.cpp +++ b/rts/Rml/SolLua/bind/Element.cpp @@ -158,7 +158,7 @@ namespace Rml::SolLua void Set(const sol::this_state L, const std::string& name, const sol::object& value) { - if (value.get_type() == sol::type::nil) { + if (value.get_type() == sol::type::lua_nil) { m_element->RemoveProperty(name); return; } diff --git a/rts/Rml/SolLua/bind/bind.cpp b/rts/Rml/SolLua/bind/bind.cpp index 21eabe9d46c..98ce9fec37c 100644 --- a/rts/Rml/SolLua/bind/bind.cpp +++ b/rts/Rml/SolLua/bind/bind.cpp @@ -39,7 +39,7 @@ namespace Rml::SolLua sol::object makeObjectFromVariant(const Rml::Variant* variant, sol::state_view s) { - if (!variant) return sol::make_object(s, sol::nil); + if (!variant) return sol::make_object(s, sol::lua_nil); switch (variant->GetType()) { @@ -69,10 +69,10 @@ namespace Rml::SolLua case Rml::Variant::VOIDPTR: return sol::make_object(s, variant->Get()); default: - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } - return sol::make_object(s, sol::nil); + return sol::make_object(s, sol::lua_nil); } } // end namespace Rml::SolLua diff --git a/rts/Sim/Misc/SmoothHeightMesh.h b/rts/Sim/Misc/SmoothHeightMesh.h index 888686af138..91866ddfb6c 100644 --- a/rts/Sim/Misc/SmoothHeightMesh.h +++ b/rts/Sim/Misc/SmoothHeightMesh.h @@ -22,7 +22,7 @@ namespace SmoothHeightMeshNamespace { */ class SmoothHeightMesh { - friend class SmoothHeightMeshDrawer; + friend struct SmoothHeightMeshDrawer; public: diff --git a/rts/Sim/MoveTypes/Components/MoveTypesComponents.h b/rts/Sim/MoveTypes/Components/MoveTypesComponents.h index b0d1d8b4265..fa7380707bb 100644 --- a/rts/Sim/MoveTypes/Components/MoveTypesComponents.h +++ b/rts/Sim/MoveTypes/Components/MoveTypesComponents.h @@ -7,8 +7,8 @@ #include "System/Ecs/Components/BaseComponents.h" #include -struct CUnit; -struct CFeature; +class CUnit; +class CFeature; namespace MoveTypes { diff --git a/rts/Sim/MoveTypes/MoveDefHandler.h b/rts/Sim/MoveTypes/MoveDefHandler.h index fbec124d13c..9c8d1db5601 100644 --- a/rts/Sim/MoveTypes/MoveDefHandler.h +++ b/rts/Sim/MoveTypes/MoveDefHandler.h @@ -18,7 +18,7 @@ class CUnit; class LuaTable; namespace MoveTypes { - class CheckCollisionQuery; + struct CheckCollisionQuery; } namespace MoveDefs { diff --git a/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h b/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h index 7db24ab3835..da7354b01fb 100644 --- a/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h +++ b/rts/Sim/MoveTypes/Utils/UnitTrapCheckUtils.h @@ -3,8 +3,8 @@ #ifndef UNIT_TRAP_CHECK_UTILS_H__ #define UNIT_TRAP_CHECK_UTILS_H__ -struct CFeature; -struct CUnit; +class CFeature; +class CUnit; namespace MoveTypes { void RegisterFeatureForUnitTrapCheck(CFeature* object); diff --git a/rts/Sim/Path/HAPFS/PathSearch.h b/rts/Sim/Path/HAPFS/PathSearch.h index 1fae37cc5dd..fdb3bfc7d85 100644 --- a/rts/Sim/Path/HAPFS/PathSearch.h +++ b/rts/Sim/Path/HAPFS/PathSearch.h @@ -6,7 +6,7 @@ #include "System/float3.h" class CSolidObject; -class MoveDef; +struct MoveDef; namespace HAPFS { struct PathSearch { diff --git a/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h b/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h index 19f7c9285cc..4bde1a878b5 100644 --- a/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h +++ b/rts/Sim/Path/QTPFS/Components/PathSpeedModInfo.h @@ -12,7 +12,7 @@ namespace QTPFS { -class INode; +struct INode; struct NodeLayerSpeedInfoSweep { static constexpr std::size_t page_size = MoveDefHandler::MAX_MOVE_DEFS; diff --git a/rts/Sim/Units/CommandAI/BuilderCAI.cpp b/rts/Sim/Units/CommandAI/BuilderCAI.cpp index b747e342615..e1faddb7dd6 100644 --- a/rts/Sim/Units/CommandAI/BuilderCAI.cpp +++ b/rts/Sim/Units/CommandAI/BuilderCAI.cpp @@ -891,13 +891,13 @@ void CBuilderCAI::ExecuteGuard(Command& c) StopSlowGuard(); } return; - } else if (b->curReclaim && owner->unitDef->canReclaim) { + } else if (b->curReclaim && !b->curReclaim->detached && owner->unitDef->canReclaim) { StopSlowGuard(); if (!ReclaimObject(b->curReclaim)) { StopMove(); } return; - } else if (b->curResurrect && owner->unitDef->canResurrect) { + } else if (b->curResurrect && !b->curResurrect->detached && owner->unitDef->canResurrect) { StopSlowGuard(); if (!ResurrectObject(b->curResurrect)) { StopMove(); diff --git a/rts/Sim/Units/UnitTypes/Builder.cpp b/rts/Sim/Units/UnitTypes/Builder.cpp index 4cf3240b1cd..deb8c846b73 100644 --- a/rts/Sim/Units/UnitTypes/Builder.cpp +++ b/rts/Sim/Units/UnitTypes/Builder.cpp @@ -605,6 +605,15 @@ void CBuilder::SetRepairTarget(CUnit* target) void CBuilder::SetReclaimTarget(CSolidObject* target) { RECOIL_DETAILED_TRACY_ZONE; + // A target already being destroyed (detached) cannot have a death dependence + // registered (AddDeathDependence no-ops on detached objects), which would leave + // curReclaim dangling. This happens when ~CObject's DependentDied cascade re-enters + // reclaim logic mid-deletion (e.g. CBuilderCAI::ExecuteGuard reading a guardee's + // stale curReclaim) on the very feature being freed. Refuse the dead target. + assert(target != nullptr); + if (target->detached) + return; + if (dynamic_cast(target) != nullptr && !static_cast(target)->def->reclaimable) return; @@ -630,6 +639,11 @@ void CBuilder::SetReclaimTarget(CSolidObject* target) void CBuilder::SetResurrectTarget(CFeature* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (curResurrect == target || target->udef == nullptr) return; @@ -646,6 +660,11 @@ void CBuilder::SetResurrectTarget(CFeature* target) void CBuilder::SetCaptureTarget(CUnit* target) { RECOIL_DETAILED_TRACY_ZONE; + // see SetReclaimTarget: never depend on an object that is already being destroyed + assert(target != nullptr); + if (target->detached) + return; + if (target == curCapture) return; diff --git a/rts/System/FileSystem/Archives/DirArchive.cpp b/rts/System/FileSystem/Archives/DirArchive.cpp index 829a56c4251..55242d39db0 100644 --- a/rts/System/FileSystem/Archives/DirArchive.cpp +++ b/rts/System/FileSystem/Archives/DirArchive.cpp @@ -42,7 +42,7 @@ CDirArchive::CDirArchive(const std::string& archiveName) // all variables here will use forward slashes, no need for conversion std::string rawFileName = dataDirsAccess.LocateFile(dirName + origName); - files.emplace_back(origName, std::move(rawFileName), -1, 0); + files.emplace_back(Files{origName, std::move(rawFileName), -1, 0}); // convert to lowercase and store lcNameIndex[StringToLower(std::move(origName))] = static_cast(files.size() - 1); diff --git a/rts/System/FileSystem/Archives/SevenZipArchive.cpp b/rts/System/FileSystem/Archives/SevenZipArchive.cpp index e40ce9e798d..d5994770067 100644 --- a/rts/System/FileSystem/Archives/SevenZipArchive.cpp +++ b/rts/System/FileSystem/Archives/SevenZipArchive.cpp @@ -147,12 +147,12 @@ CSevenZipArchive::CSevenZipArchive(const std::string& name) continue; } - const auto& fd = fileEntries.emplace_back( - i, //fp - SzArEx_GetFileSize(&db, i), // size + const auto& fd = fileEntries.emplace_back(FileEntry{ + static_cast(i), //fp + static_cast(SzArEx_GetFileSize(&db, i)), // size db.MTime.Vals ? static_cast(CTimeUtil::NTFSTimeToTime64(db.MTime.Vals[i].Low, db.MTime.Vals[i].High)) : 0, // modtime std::move(fileName.value()) // origName - ); + }); lcNameIndex.emplace(StringToLower(fd.origName), fileEntries.size() - 1); } diff --git a/rts/System/FileSystem/Archives/ZipArchive.cpp b/rts/System/FileSystem/Archives/ZipArchive.cpp index 37914269c49..e7b13e3e790 100644 --- a/rts/System/FileSystem/Archives/ZipArchive.cpp +++ b/rts/System/FileSystem/Archives/ZipArchive.cpp @@ -58,13 +58,13 @@ CZipArchive::CZipArchive(const std::string& archiveName) unz_file_pos fp{}; unzGetFilePos(zip, &fp); - const auto& fd = fileEntries.emplace_back( + const auto& fd = fileEntries.emplace_back(FileEntry{ std::move(fp), //fp - info.uncompressed_size, //size + static_cast(info.uncompressed_size), //size fName, //origName - info.crc, //crc + static_cast(info.crc), //crc static_cast(CTimeUtil::DosTimeToTime64(info.dosDate)) //modTime - ); + }); lcNameIndex.emplace(StringToLower(fd.origName), fileEntries.size() - 1); } diff --git a/rts/System/FileSystem/FileSystem.cpp b/rts/System/FileSystem/FileSystem.cpp index d1471810f48..f319d2199ca 100644 --- a/rts/System/FileSystem/FileSystem.cpp +++ b/rts/System/FileSystem/FileSystem.cpp @@ -61,7 +61,7 @@ namespace Impl { return std::string(reinterpret_cast(utf8.c_str())); } RECOIL_FORCE_INLINE std::string StoreUTF8AsString(const std::u8string_view& utf8) { - return std::string(reinterpret_cast(utf8.data())); + return std::string(reinterpret_cast(utf8.data()), utf8.size()); } RECOIL_FORCE_INLINE std::string StorePathAsString(const fs::path& path) { return StoreUTF8AsString(path.u8string()); diff --git a/rts/System/Log/DefaultFilter.cpp b/rts/System/Log/DefaultFilter.cpp index cf0c3939d29..3ec33410cf1 100644 --- a/rts/System/Log/DefaultFilter.cpp +++ b/rts/System/Log/DefaultFilter.cpp @@ -157,13 +157,15 @@ void log_filter_section_setMinLevel(int level, const char* section) // (same string but will not become garbage) section = *registeredSection; - if (level == log_filter_section_getDefaultMinLevel(section)) { - using P = decltype(log_filter::sectionMinLevels)::value_type; - - const auto sectionComparer = [](const P& a, const P& b) { return (log_filter_section_compare()(a.first, b.first)); }; - const auto sectionMinLevel = std::lower_bound(secLvls.begin(), secLvls.begin() + log_filter::numLevels, P{section, 0}, sectionComparer); + // locate any existing override for this section (the array is kept sorted) + using P = decltype(log_filter::sectionMinLevels)::value_type; + const auto sectionComparer = [](const P& a, const P& b) { return (log_filter_section_compare()(a.first, b.first)); }; + const auto sectionMinLevel = std::lower_bound(secLvls.begin(), secLvls.begin() + log_filter::numLevels, P{section, 0}, sectionComparer); + const bool exists = (sectionMinLevel != (secLvls.begin() + log_filter::numLevels) && strcmp(sectionMinLevel->first, section) == 0); - if (sectionMinLevel == (secLvls.begin() + log_filter::numLevels) || strcmp(sectionMinLevel->first, section) != 0) + if (level == log_filter_section_getDefaultMinLevel(section)) { + // back to default: drop any existing override (nothing to do otherwise) + if (!exists) return; // erase @@ -175,6 +177,13 @@ void log_filter_section_setMinLevel(int level, const char* section) return; } + // non-default: update any existing override in-place + if (exists) { + sectionMinLevel->second = level; + return; + } + + // add a net new override secLvls[log_filter::numLevels++] = {section, level}; // swap into position diff --git a/rts/System/Matrix44f.h b/rts/System/Matrix44f.h index 3dd6767dd33..bd5e6d1fc5b 100644 --- a/rts/System/Matrix44f.h +++ b/rts/System/Matrix44f.h @@ -2,6 +2,7 @@ #pragma once +#include #include #include #include diff --git a/rts/System/Platform/Mac/SDLMain.h b/rts/System/Platform/Mac/SDLMain.h deleted file mode 100644 index 995d036c3f0..00000000000 --- a/rts/System/Platform/Mac/SDLMain.h +++ /dev/null @@ -1,18 +0,0 @@ -/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ - -/* - * SDLMain.m - main entry point for our Cocoa-ized SDL app - * Initial Version: Darrell Walisser - * Non-NIB-Code & other changes: Max Horn - * Feel free to customize this file to suit your needs. - */ - -#ifdef __APPLE__ - -#import - -@interface SDLMain : NSObject -@end - -#endif - diff --git a/rts/System/Platform/Mac/SDLMain.m b/rts/System/Platform/Mac/SDLMain.m deleted file mode 100644 index 2fea69074ad..00000000000 --- a/rts/System/Platform/Mac/SDLMain.m +++ /dev/null @@ -1,299 +0,0 @@ -/* SDLMain.m - main entry point for our Cocoa-ized SDL app - Initial Version: Darrell Walisser - Non-NIB-Code & other changes: Max Horn - - Feel free to customize this file to suit your needs -*/ - -#import "SDL.h" -#import "SDLMain.h" -#import /* for MAXPATHLEN */ -#import -#import - -/* Use this flag to determine whether we use SDLMain.nib or not */ -#define SDL_USE_NIB_FILE 0 - - -static int gArgc; -static char **gArgv; -static BOOL gFinderLaunch; - -//extern NSAutoreleasePool *pool; -//void PreInitMac(); - -void MacMessageBox(const char *msg, const char *caption, unsigned int flags){ - NSAlert *alert = [[[NSAlert alloc] init] autorelease]; - [alert addButtonWithTitle:@"OK"]; - [alert setMessageText:[NSString stringWithCString:caption]]; - [alert setInformativeText:[NSString stringWithCString:msg]]; - [alert setAlertStyle:NSWarningAlertStyle]; - [alert runModal]; -} - -#if SDL_USE_NIB_FILE -/* A helper category for NSString */ -@interface NSString (ReplaceSubString) -- (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString; -@end -#else -/* An internal Apple class used to setup Apple menus */ -@interface NSAppleMenuController:NSObject {} -- (void)controlMenu:(NSMenu *)aMenu; -@end -#endif - -@interface SDLApplication : NSApplication -@end - -@implementation SDLApplication -/* Invoked from the Quit menu item */ -- (void)terminate:(id)sender -{ - /* Post a SDL_QUIT event */ - SDL_Event event; - event.type = SDL_QUIT; - SDL_PushEvent(&event); -} -@end - - -/* The main class of the application, the application's delegate */ -@implementation SDLMain - -/* Set the working directory to the .app's parent directory */ -- (void) setupWorkingDirectory:(BOOL)shouldChdir -{ - char parentdir[MAXPATHLEN]; - char *c; - - strncpy ( parentdir, gArgv[0], sizeof(parentdir) ); - c = (char*) parentdir; - - while (*c != '\0') /* go to end */ - c++; - - while (*c != '/') /* back up to parent */ - c--; - - *c++ = '\0'; /* cut off last part (binary name) */ - - if (shouldChdir) - { - assert ( chdir (parentdir) == 0 ); /* chdir to the binary app's parent */ - assert ( chdir ("../../../") == 0 ); /* chdir to the .app's parent */ - } -} - -#if SDL_USE_NIB_FILE - -/* Fix menu to contain the real app name instead of "SDL App" */ -- (void)fixMenu:(NSMenu *)aMenu withAppName:(NSString *)appName -{ - NSRange aRange; - NSEnumerator *enumerator; - NSMenuItem *menuItem; - - aRange = [[aMenu title] rangeOfString:@"SDL App"]; - if (aRange.length != 0) - [aMenu setTitle: [[aMenu title] stringByReplacingRange:aRange with:appName]]; - - enumerator = [[aMenu itemArray] objectEnumerator]; - while ((menuItem = [enumerator nextObject])) - { - aRange = [[menuItem title] rangeOfString:@"SDL App"]; - if (aRange.length != 0) - [menuItem setTitle: [[menuItem title] stringByReplacingRange:aRange with:appName]]; - if ([menuItem hasSubmenu]) - [self fixMenu:[menuItem submenu] withAppName:appName]; - } - [ aMenu sizeToFit ]; -} - -#else - -void setupAppleMenu(void) -{ - /* warning: this code is very odd */ - NSAppleMenuController *appleMenuController; - NSMenu *appleMenu; - NSMenuItem *appleMenuItem; - - appleMenuController = [[NSAppleMenuController alloc] init]; - appleMenu = [[NSMenu alloc] initWithTitle:@""]; - appleMenuItem = [[NSMenuItem alloc] initWithTitle:@"" action:nil keyEquivalent:@""]; - - [appleMenuItem setSubmenu:appleMenu]; - - /* yes, we do need to add it and then remove it -- - if you don't add it, it doesn't get displayed - if you don't remove it, you have an extra, titleless item in the menubar - when you remove it, it appears to stick around - very, very odd */ - [[NSApp mainMenu] addItem:appleMenuItem]; - [appleMenuController controlMenu:appleMenu]; - [[NSApp mainMenu] removeItem:appleMenuItem]; - [appleMenu release]; - [appleMenuItem release]; -} - -/* Create a window menu */ -void setupWindowMenu(void) -{ - NSMenu *windowMenu; - NSMenuItem *windowMenuItem; - NSMenuItem *menuItem; - - - windowMenu = [[NSMenu alloc] initWithTitle:@"Window"]; - - /* "Minimize" item */ - menuItem = [[NSMenuItem alloc] initWithTitle:@"Minimize" action:@selector(performMiniaturize:) keyEquivalent:@"m"]; - [windowMenu addItem:menuItem]; - [menuItem release]; - - /* Put menu into the menubar */ - windowMenuItem = [[NSMenuItem alloc] initWithTitle:@"Window" action:nil keyEquivalent:@""]; - [windowMenuItem setSubmenu:windowMenu]; - [[NSApp mainMenu] addItem:windowMenuItem]; - - /* Tell the application object that this is now the window menu */ - [NSApp setWindowsMenu:windowMenu]; - - /* Finally give up our references to the objects */ - [windowMenu release]; - [windowMenuItem release]; -} - -/* Replacement for NSApplicationMain */ -void CustomApplicationMain () -{ - NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; - SDLMain *sdlMain; - //PreInitMac(); - - /* Ensure the application object is initialised */ - [SDLApplication sharedApplication]; - - /* Set up the menubar */ - [NSApp setMainMenu:[[NSMenu alloc] init]]; - setupAppleMenu(); - setupWindowMenu(); - - /* Create SDLMain and make it the app delegate */ - sdlMain = [[SDLMain alloc] init]; - [NSApp setDelegate:sdlMain]; - - /* Bring the app to foreground */ - ProcessSerialNumber psn; - GetCurrentProcess(&psn); - TransformProcessType(&psn,kProcessTransformToForegroundApplication); - - /* Start the main event loop */ - [NSApp run]; - - [sdlMain release]; - [pool release]; -} - -#endif - -/* Called when the internal event loop has just started running */ -- (void) applicationDidFinishLaunching: (NSNotification *) note -{ - int status; - - /* Set the working directory to the .app's parent directory */ - [self setupWorkingDirectory:gFinderLaunch]; - -#if SDL_USE_NIB_FILE - /* Set the main menu to contain the real app name instead of "SDL App" */ - [self fixMenu:[NSApp mainMenu] withAppName:[[NSProcessInfo processInfo] processName]]; -#endif - - /* Hand off to main application code */ - status = SDL_main (gArgc, gArgv); - - /* We're done, thank you for playing */ - exit(status); -} -@end - - -@implementation NSString (ReplaceSubString) - -- (NSString *)stringByReplacingRange:(NSRange)aRange with:(NSString *)aString -{ - unsigned int bufferSize; - unsigned int selfLen = [self length]; - unsigned int aStringLen = [aString length]; - unichar *buffer; - NSRange localRange; - NSString *result; - - bufferSize = selfLen + aStringLen - aRange.length; - buffer = NSAllocateMemoryPages(bufferSize*sizeof(unichar)); - - /* Get first part into buffer */ - localRange.location = 0; - localRange.length = aRange.location; - [self getCharacters:buffer range:localRange]; - - /* Get middle part into buffer */ - localRange.location = 0; - localRange.length = aStringLen; - [aString getCharacters:(buffer+aRange.location) range:localRange]; - - /* Get last part into buffer */ - localRange.location = aRange.location + aRange.length; - localRange.length = selfLen - localRange.location; - [self getCharacters:(buffer+aRange.location+aStringLen) range:localRange]; - - /* Build output string */ - result = [NSString stringWithCharacters:buffer length:bufferSize]; - - NSDeallocateMemoryPages(buffer, bufferSize); - - return result; -} - -@end - - - -#ifdef main -# undef main -#endif - - -/* Main entry point to executable - should *not* be SDL_main! */ -int main (int argc, char **argv) -{ - - /* Copy the arguments into a global variable */ - int i; - - /* This is passed if we are launched by double-clicking */ - if ( argc >= 2 && strncmp (argv[1], "-psn", 4) == 0 ) { - gArgc = 1; - gFinderLaunch = YES; - } else { - gArgc = argc; - gFinderLaunch = NO; - } - gArgv = (char**) malloc (sizeof(*gArgv) * (gArgc+1)); - assert (gArgv != NULL); - for (i = 0; i < gArgc; i++) - gArgv[i] = argv[i]; - gArgv[i] = NULL; - -#if SDL_USE_NIB_FILE - [SDLApplication poseAsClass:[NSApplication class]]; - NSApplicationMain (); -#else - CustomApplicationMain (); -#endif - return 0; -} - - diff --git a/rts/System/SafeUtil.h b/rts/System/SafeUtil.h index ce19b3f9738..9fbf946cc82 100644 --- a/rts/System/SafeUtil.h +++ b/rts/System/SafeUtil.h @@ -5,6 +5,8 @@ #include #include +#include +#include namespace spring { template inline void SafeDestruct(T*& p) @@ -112,8 +114,8 @@ namespace spring { static_assert(sizeof(TIn) == sizeof(TOut), "Types must match sizes"); static_assert(std::is_trivially_copyable::value , "Requires TriviallyCopyable input"); static_assert(std::is_trivially_copyable::value, "Requires TriviallyCopyable output"); - static_assert(std::is_trivially_constructible_v, - "This implementation additionally requires destination type to be trivially constructible"); + static_assert(std::is_trivially_default_constructible::value, + "This implementation additionally requires destination type to be trivially default-constructible"); TOut t2; std::memcpy(std::addressof(t2), std::addressof(t1), sizeof(TIn)); diff --git a/rts/System/SpringApp.cpp b/rts/System/SpringApp.cpp index e5a011b19c3..a2274b6f9e5 100644 --- a/rts/System/SpringApp.cpp +++ b/rts/System/SpringApp.cpp @@ -13,7 +13,9 @@ #undef KeyRelease #else #include // isatty +#ifndef __APPLE__ #include // XInitThreads +#endif #undef KeyPress #undef KeyRelease diff --git a/rts/System/SpringHashMap.hpp b/rts/System/SpringHashMap.hpp index 1bf487a6111..f6a359cb14d 100644 --- a/rts/System/SpringHashMap.hpp +++ b/rts/System/SpringHashMap.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include #include diff --git a/rts/System/SpringHashSet.hpp b/rts/System/SpringHashSet.hpp index 3375e12520c..366a5d35211 100644 --- a/rts/System/SpringHashSet.hpp +++ b/rts/System/SpringHashSet.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include // malloc #include diff --git a/rts/System/float3.h b/rts/System/float3.h index f096c112c0d..bf47dd90c2a 100644 --- a/rts/System/float3.h +++ b/rts/System/float3.h @@ -9,7 +9,6 @@ #include #include "System/BranchPrediction.h" -#include "lib/streflop/streflop_cond.h" #include "System/creg/creg_cond.h" #include "System/FastMath.h" #include "System/type2.h" diff --git a/rts/System/simd_compat.h b/rts/System/simd_compat.h index a405e02f587..c68b59e3484 100644 --- a/rts/System/simd_compat.h +++ b/rts/System/simd_compat.h @@ -3,6 +3,19 @@ #ifdef SSE2NEON #include "lib/sse2neon/sse2neon.h" + // sse2neon leaks 's FE_XXX macros, which collide with the ones streflop + // redefines and trigger a #warning. Undef them here so streflop gets a clean slate. + #undef FE_INVALID + #undef FE_DENORMAL + #undef FE_DIVBYZERO + #undef FE_OVERFLOW + #undef FE_UNDERFLOW + #undef FE_INEXACT + #undef FE_ALL_EXCEPT + #undef FE_TONEAREST + #undef FE_DOWNWARD + #undef FE_UPWARD + #undef FE_TOWARDZERO #else #ifdef _MSC_VER #include // MSVC umbrella diff --git a/rts/build/cmake/FindSevenZip.cmake b/rts/build/cmake/FindSevenZip.cmake index 1077ec7b17e..0e3e4002a67 100644 --- a/rts/build/cmake/FindSevenZip.cmake +++ b/rts/build/cmake/FindSevenZip.cmake @@ -23,7 +23,7 @@ ENDIF (SEVENZIP_BIN) set(progfilesx86 "ProgramFiles(x86)") find_program(SEVENZIP_BIN - NAMES 7z 7za + NAMES 7z 7za 7zz HINTS "${MINGWDIR}" "${MINGWLIBS}/bin" "$ENV{${progfilesx86}}/7-zip" "$ENV{ProgramFiles}/7-zip" "$ENV{ProgramW6432}/7-zip" PATH_SUFFIXES bin DOC "7zip executable" diff --git a/rts/builds/legacy/CMakeLists.txt b/rts/builds/legacy/CMakeLists.txt index cbcc906f83d..f1d19ede7e3 100644 --- a/rts/builds/legacy/CMakeLists.txt +++ b/rts/builds/legacy/CMakeLists.txt @@ -49,7 +49,7 @@ find_freetype_hack() # hack to find different named freetype.dll find_package_static(Freetype 2.8.1 REQUIRED) list(APPEND engineLibraries Freetype::Freetype) -if (UNIX) +if (UNIX AND NOT APPLE) find_package(X11 REQUIRED) target_link_libraries(Game PRIVATE X11::Xcursor) list(APPEND engineLibraries ${X11_Xcursor_LIB} ${X11_X11_LIB}) diff --git a/rts/lib/glad/CMakeLists.txt b/rts/lib/glad/CMakeLists.txt index a67e710dd36..7f7391dd7d3 100644 --- a/rts/lib/glad/CMakeLists.txt +++ b/rts/lib/glad/CMakeLists.txt @@ -1,10 +1,10 @@ cmake_minimum_required(VERSION 3.5) project(Glad) -if (UNIX AND NOT MINGW) +if (UNIX AND NOT MINGW AND NOT APPLE) add_library(glad glad.c glad_glx.c) -else (UNIX AND NOT MINGW) +else (UNIX AND NOT MINGW AND NOT APPLE) add_library(glad glad.c) -endif (UNIX AND NOT MINGW) +endif (UNIX AND NOT MINGW AND NOT APPLE) target_include_directories(glad PUBLIC /) \ No newline at end of file diff --git a/test/AGENTS.md b/test/AGENTS.md new file mode 100644 index 00000000000..17fe17159f0 --- /dev/null +++ b/test/AGENTS.md @@ -0,0 +1,128 @@ +# AGENTS.md + +This file provides guidance to coding agents when working with code in this repository's test folder. + +## Build & Run Tests + +```bash +# From build/: build all test executables +cmake --build . --target tests + +# From build/: run tests. ctest/check recipes here assume a non-docker +# build. For a docker build, run `docker-build-v2/build.sh --compile linux -t check` +# (runs ctest inside the container) or invoke binaries in +# build-amd64-linux/test/ directly (see below). +ctest # run already-built tests; does not rebuild +cmake --build . --target check # rebuild engine-headless + all tests first, then ctest -V + +# From build/: run ctest with filters +ctest --output-on-failure # concise; only shows failing output +ctest -R Float3 --output-on-failure # filter by name regex +ctest -R Float3 -V # same, verbose +ctest -N # list all registered tests without running + +# From repo root: run a single test binary directly (fastest iteration). +# Use build-amd64-linux/ instead of build/ if you built via docker. +./build/test/test_Float3 +./build/test/test_Float3 -s # Catch2: show passing assertions too +./build/test/test_Float3 "Float3" # filter by TEST_CASE name (supports wildcards) +``` + +`cmake --build . --target check` is the full-fat target: it depends on `engine-headless` and +every `test_*` executable, so it relinks anything stale before running ctest with +`--output-on-failure -V`. Use bare `ctest` when you want to skip the rebuild. + +## Framework + +**Catch2** (amalgamated single-header version) in `lib/catch2/`. Custom main in `lib/catch2/catch_main.cpp` with leak detection enabled via `CATCH_AMALGAMATED_CUSTOM_MAIN`. + +## Test Organization + +``` +engine/System/ # Core system tests (math, threading, I/O, serialization) +engine/Sim/Misc/ # Simulation tests (QuadField, Ellipsoid) +lib/luasocket/ # Lua socket restriction tests +other/ # Mutex benchmarks, memory pool tests +unitsync/ # UnitSync API tests +validation/ # Integration tests (shell scripts that run full game simulation) +tools/CompileFailTest/ # Negative test framework (tests that must NOT compile) +headercheck/ # Header isolation tests (cmake -DHEADERCHECK=ON) +``` + +## Adding a New Test + +1. Create test source in the appropriate subdirectory under `engine/`, `other/`, etc. +2. In `test/CMakeLists.txt`, add a block using the `add_spring_test` macro: +```cmake +set(test_name MyTest) +set(test_src + "${CMAKE_CURRENT_SOURCE_DIR}/engine/System/testMyTest.cpp" + ${test_Common_sources} +) +set(test_libs "") +set(test_flags "-DNOT_USING_CREG -DNOT_USING_STREFLOP -DBUILDING_AI") +add_spring_test(${test_name} "${test_src}" "${test_libs}" "${test_flags}") +``` +3. The macro creates executable `test_` and registers it with ctest as `test`. + +## Common Compile Flags + +| Flag | Purpose | +|------|---------| +| `-DUNIT_TEST` | Always set for all tests (global) | +| `-DSYNCCHECK` | Always set for all tests (global) | +| `-DNOT_USING_CREG` | Stubs out `CR_*` macros. Default unless the test exercises save/load serialization. | +| `-DNOT_USING_STREFLOP` | Falls back to ``. Default unless the test verifies synced floating-point determinism. | +| `-DBUILDING_AI` | Makes engine headers skip engine-only paths. Pair with `NOT_USING_CREG` and `NOT_USING_STREFLOP`. | +| `-DTHREADPOOL` | Selects the real thread pool over the stub. Set only when the test needs real parallelism. | +| `-DUNITSYNC` | Marks the file as part of unitsync. Only needed for tests that link the unitsync library. | + +## Patterns + +### Basic test file +```cpp +#include +#include "System/Log/ILog.h" + +TEST_CASE("MyFeature") { + CHECK(1 + 1 == 2); + SECTION("sub-case") { + CHECK(true); + } +} +``` + +### Tests that need timing +```cpp +#include "System/Misc/SpringTime.h" +TEST_CASE("TimingTest") { + InitSpringTime ist; // RAII - must be instantiated before using spring_time + // ... +} +``` + +### Thread-safe assertions +Catch2 is NOT thread-safe. Multi-threaded tests must guard assertions: +```cpp +static spring::mutex m; +#define SAFE_CHECK(expr) { std::lock_guard lk(m); CHECK(expr); } +``` + +### Compile-fail tests +Tests that verify code correctly fails to compile. Source uses `#ifdef FAIL` guards: +```cpp +#ifdef FAIL +#ifdef TEST1 + int x = someStronglyTypedEnum; // must not compile +#endif +#endif +``` +Registered in CMakeLists.txt via: +```cmake +spring_test_compile_fail(testName_fail1 ${test_src} "-DTEST1") +``` + +## Test Helpers (mock/stub files) + +- `engine/System/NullGlobalConfig.cpp` — provides default `globalConfig` without full engine init +- `engine/System/Nullerrorhandler.cpp` — stubs `ErrorMessageBox()` to prevent GUI popups diff --git a/test/engine/System/Log/TestILog.cpp b/test/engine/System/Log/TestILog.cpp index cb8d4b3e4a1..7b7d1345f0c 100644 --- a/test/engine/System/Log/TestILog.cpp +++ b/test/engine/System/Log/TestILog.cpp @@ -3,6 +3,7 @@ #include "System/Log/FileSink.h" #include "System/Log/StreamSink.h" #include "System/Log/LogUtil.h" +#include "System/Log/DefaultFilter.h" #include @@ -231,3 +232,33 @@ TEST_CASE("IsEnabled") TLOG_SL( "other-one-time-section", L_DEBUG, "Testing LOG_IS_ENABLED_S"); } + +// Regression for the duplicate-entry leak in log_filter_section_setMinLevel. +// Setting a section to a non-default level used to *append* a new row every call +// instead of updating the existing one, so repeated changes to one section filled +// the fixed-size sectionMinLevels table and then made *all* section-level changes +// silently fail ("too many section-levels"). +TEST_CASE("SectionMinLevelNoDuplicateLeak") +{ + // non-default levels for these (non-default) sections; restored at the end + const int savedDefined = log_filter_section_getMinLevel(LOG_SECTION_DEFINED); + const int savedOneTime = log_filter_section_getMinLevel(LOG_SECTION_ONE_TIME_0); + + // hammer one section far more than the table could ever hold + for (int i = 0; i < 300; ++i) + log_filter_section_setMinLevel((i & 1) ? LOG_LEVEL_WARNING : LOG_LEVEL_ERROR, LOG_SECTION_DEFINED); + + // the most recent value wins (a single, updated-in-place entry) + log_filter_section_setMinLevel(LOG_LEVEL_ERROR, LOG_SECTION_DEFINED); + CHECK(log_filter_section_getMinLevel(LOG_SECTION_DEFINED) == LOG_LEVEL_ERROR); + + // and a *different* section must still be settable: with the old append bug + // the table is saturated by now and this set would be dropped + log_filter_section_setMinLevel(LOG_LEVEL_WARNING, LOG_SECTION_ONE_TIME_0); + CHECK(log_filter_section_getMinLevel(LOG_SECTION_ONE_TIME_0) == LOG_LEVEL_WARNING); + + // restore original levels (setting back to default takes the erase path) + log_filter_section_setMinLevel(savedDefined, LOG_SECTION_DEFINED); + log_filter_section_setMinLevel(savedOneTime, LOG_SECTION_ONE_TIME_0); +} + diff --git a/tools/DemoTool/CMakeLists.txt b/tools/DemoTool/CMakeLists.txt index dc9cbf66ecf..fdc47e0461d 100644 --- a/tools/DemoTool/CMakeLists.txt +++ b/tools/DemoTool/CMakeLists.txt @@ -70,6 +70,8 @@ target_link_libraries(demotool gflags_nothreads_static 7zip ${ZLIB_LIBRARY} + nowide::nowide + fmt::fmt ${PLATFORM_LIBS} Tracy::TracyClient ) diff --git a/tools/DemoTool/DemoTool.cpp b/tools/DemoTool/DemoTool.cpp index 96bb8930344..d26b0c9b955 100644 --- a/tools/DemoTool/DemoTool.cpp +++ b/tools/DemoTool/DemoTool.cpp @@ -1,5 +1,6 @@ /* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ +#include #include #include #include @@ -417,7 +418,7 @@ void TrafficDump(CDemoReader& reader, bool trafficStats) //uchar myPlayerNum; int frameNum; uint checksum; std::cout << "NETMSG_SYNCRESPONSE: Playernum: "<< (unsigned)buffer[1]; std::cout << " Framenum: " << *(int*)(buffer+2); - std::cout << " Checksum: " << (unsigned)buffer[6]; + std::cout << " Checksum: " << *(uint32_t*)(buffer+6); std::cout << std::endl; break; case NETMSG_DIRECT_CONTROL: