Compare commits
57 Commits
2518f4f14a
...
refactorin
| Author | SHA1 | Date | |
|---|---|---|---|
| 590bca458c | |||
| 59067a9c49 | |||
| 72d85d681c | |||
| a86ba3428a | |||
| 56b7248ac7 | |||
| 009f8c6d14 | |||
| a90218f5c0 | |||
| 7ce0751c60 | |||
| b3d6264ed3 | |||
| ade716edf2 | |||
| 3272431353 | |||
| d1b688f45e | |||
| df7f60c898 | |||
| 58e173ad5b | |||
| bb50f527d6 | |||
| 7540c21d5c | |||
| 2522a8c974 | |||
| e39b81eb22 | |||
| 71d0dad3f2 | |||
| fda88fe75c | |||
| d713257fb5 | |||
| 0029236135 | |||
| 622447c45b | |||
| a34d66f548 | |||
| cb5572ffdd | |||
| 10ba226af7 | |||
| 0a3288d1d1 | |||
| 2c9433cea6 | |||
| 21eb6ad096 | |||
| bf579bb76e | |||
| 6bde69dc33 | |||
| bf99cd0694 | |||
| 6f107e3479 | |||
| 8f42519911 | |||
| bb7b90f9ea | |||
| e702c01005 | |||
| b1720dc2b3 | |||
| 099f0b55fc | |||
| b133a21914 | |||
| 69848eb9f8 | |||
| 993325d97c | |||
| 75f306f650 | |||
| 0eb9c97e5d | |||
| 28d0416458 | |||
| 59fde8dbbc | |||
| 5355f9f77d | |||
| f678dab387 | |||
| ebee62166d | |||
| 3c549a160c | |||
| dc58f6ea32 | |||
| 5bd804601c | |||
| 92a4f02cef | |||
| 84d32b6c16 | |||
| d31ff68ab7 | |||
| b722955f7e | |||
| 370a3036c1 | |||
| f60f111ccc |
155
.claude/CLAUDE.md
Normal file
155
.claude/CLAUDE.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Interaction
|
||||||
|
|
||||||
|
* ONLY modify code or other files if explicitly asked to do so
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
Dota Factory is a single-player game that blends a Factorio-style factory builder with
|
||||||
|
DOTA-style wave defence. The player builds a factory on an asteroid — mining ores,
|
||||||
|
transporting materials over belts and splitters, and crafting through a config-defined
|
||||||
|
production tree — to supply shipyards that produce autonomous combat ships. Those ships
|
||||||
|
fight off endless enemy waves advancing from the right. See `docs/concept.md` for the full design.
|
||||||
|
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
* the project root and the git repository root are the same directory
|
||||||
|
* project requirements can be found at `docs/requirements.md`
|
||||||
|
* architecture decisions can be found at `docs/architecture.md`
|
||||||
|
* game content design (ship/module roster, layout grids, footprint gating) can be found at `docs/content_design.md`
|
||||||
|
* replay/determinism design can be found at `docs/replay_design.md`
|
||||||
|
* balancing rules, targets, tuned numbers, process, and history live under `docs/balancing/`
|
||||||
|
|
||||||
|
Requirements carry stable `REQ-<AREA>-<NAME>` ids. They are cited throughout the code in
|
||||||
|
comments — when changing behavior, find the governing REQ id first and
|
||||||
|
keep the citation accurate.
|
||||||
|
|
||||||
|
## Coding Guidelines
|
||||||
|
|
||||||
|
* avoid duplicate code
|
||||||
|
* do not use the "auto" keyword, with two exceptions:
|
||||||
|
* **named local lambdas** — a lambda's type is unnameable, and `std::function`
|
||||||
|
is not an acceptable substitute in per-tick code because it adds a heap
|
||||||
|
allocation and an indirect call
|
||||||
|
* **iterator types** — `auto it = m_buildings.find(id)` is allowed where
|
||||||
|
spelling the iterator out adds length without adding information
|
||||||
|
* everywhere else the type is written out; in particular `auto` is not used
|
||||||
|
for plain values, return values, or range-for element types
|
||||||
|
* use Qt utility data types (like QPoint, QVector3D, QString, etc.)
|
||||||
|
* wrap strings that appear in the UI with Qt's "tr()"
|
||||||
|
* use the EventManager/EventHandler instead of defining own signals and slots
|
||||||
|
* use std::optional if a variable can be "not set"
|
||||||
|
* start the name of a getter method with "get"
|
||||||
|
* don't use abbreviations, except very common ones ("s" for seconds, "min", "max", etc.)
|
||||||
|
* if a variable holds a value that has a unit or if a function returns a value that has a unit, append that unit to the name (e.g. "m_shipVelocity_mps", "getAcceleration_mpss()")
|
||||||
|
* always enclose scopes in braces
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
Requires CMake 3.14.4+, a C++17 compiler, and Qt 5 (developed against Qt 5.12.3,
|
||||||
|
MSVC 2017 x64; `Qt5_DIR` is cached in `build/CMakeCache.txt`). Needs Qt components
|
||||||
|
Widgets, Network, Multimedia, Charts, Svg, plus OpenGL.
|
||||||
|
|
||||||
|
External dependencies vendored under `src/external/`:
|
||||||
|
* **toml++** — reading TOML config files
|
||||||
|
* **tinyexpr** — evaluating formula strings from config files
|
||||||
|
* **EnTT** — entity registry backing the ship/station/debris simulation
|
||||||
|
* **Catch2** — test framework
|
||||||
|
|
||||||
|
Configure and build (a configured `build/` tree already exists):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -S . -B build # configure (multi-config VS generator)
|
||||||
|
cmake --build build --config Debug # all targets
|
||||||
|
cmake --build build --config Debug --target DotaFactory_test
|
||||||
|
```
|
||||||
|
|
||||||
|
Targets: `DotaFactory` (app), `DotaFactory_lib`, `DotaFactory_ui`, `DotaFactory_test`,
|
||||||
|
`DotaFactory_balancing`. Executables land in `build/DotaFactory/<Config>/{app,balancing}/`.
|
||||||
|
|
||||||
|
**Adding a source file requires editing CMake.** Every directory under `src/` has its own
|
||||||
|
`CMakeLists.txt` listing files explicitly in `HDRS`/`SRCS` (or `TEST_FILES` for tests) —
|
||||||
|
there is no globbing. A new file that is not registered simply will not compile.
|
||||||
|
|
||||||
|
Config data is not copied: `CONFIG_DIR` is a compile definition pointing at
|
||||||
|
`bin/app/data/config` for the app and balancing tool, and `bin/test/data/config` for
|
||||||
|
tests (a separate fixture set). On Windows the build also junctions `bin/*/data` into the
|
||||||
|
output directories and copies the Qt DLLs.
|
||||||
|
|
||||||
|
Run the app: `build/DotaFactory/Debug/app/DotaFactory.exe`, optionally
|
||||||
|
`--replay <file>` for view-only playback of a recorded run.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
Catch2, single executable, links `lib` only — no QApplication, no display.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
build/DotaFactory/Debug/app/DotaFactory_test.exe # all
|
||||||
|
build/DotaFactory/Debug/app/DotaFactory_test.exe "[belt],[building]" # by tag
|
||||||
|
build/DotaFactory/Debug/app/DotaFactory_test.exe "BeltSystem: *" # by name pattern
|
||||||
|
build/DotaFactory/Debug/app/DotaFactory_test.exe --reporter compact
|
||||||
|
```
|
||||||
|
|
||||||
|
Common tags: `[building] [belt] [behavior] [blueprint] [modules] [config] [wave] [combat]
|
||||||
|
[replay] [determinism] [ship] [debris] [threat] [unlock]`.
|
||||||
|
|
||||||
|
`src/test/SimulationTestAccess.h` is a friend-struct backdoor to `Simulation`'s private
|
||||||
|
mutators; tests use it instead of duplicating the command path. It lives under `src/test`
|
||||||
|
and is deliberately off the lib/ui/app include path.
|
||||||
|
|
||||||
|
## Verification Tools
|
||||||
|
|
||||||
|
Python scripts in `tools/` read the real configs and are the first check
|
||||||
|
after config edits (see `docs/balancing/process.md`):
|
||||||
|
|
||||||
|
* `verify_recipes.py` — recipe-tree closure, visuals coverage, orphan items
|
||||||
|
* `verify_layouts.py` — module footprint gating per hull layout
|
||||||
|
* `threat_report.py` — item/module/ship threat values, ratios, belt feasibility
|
||||||
|
|
||||||
|
The `DotaFactory_balancing` target runs parallel arena simulations from
|
||||||
|
`bin/balancing/data/balancing.toml` for combat-stat tuning.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
See `docs/architecture.md` for the full write-up. Highlights and the
|
||||||
|
invariants that are easy to break:
|
||||||
|
|
||||||
|
* Strict simulation/presentation split, enforced at the CMake target level: `lib`
|
||||||
|
(sim + config, Qt Core/Gui only — no QtWidgets), `ui` (QtWidgets + QOpenGLWidget),
|
||||||
|
`app` (thin main), `test` (Catch2 against `lib`).
|
||||||
|
* Fixed 30 Hz tick simulation, 60 FPS render, accumulator-driven; game speed is a
|
||||||
|
tick-rate multiplier. All sim quantities are in ticks, never wall-clock seconds.
|
||||||
|
* The tick order in `Simulation::tick()` is load-bearing for determinism — see the
|
||||||
|
Tick Order section of `architecture.md` before reordering systems.
|
||||||
|
* **Command chokepoint:** every sim mutation during play flows through
|
||||||
|
`Simulation::apply(const Command&)` (see `sim/Command.h`, `CommandManager`), so runs can
|
||||||
|
be recorded and replayed. Commands reference stable ids (`BuildingId`, tile coords,
|
||||||
|
choice indices) — never raw `entt::entity` handles. UI code must not call sim mutators
|
||||||
|
directly. Determinism is checksummed (`StateChecksum`) and covered by
|
||||||
|
`DeterminismTest` / `ReplayPlaybackTest`.
|
||||||
|
* Config is loaded once at startup, formulas compiled once via tinyexpr, immutable
|
||||||
|
afterwards; malformed config aborts startup rather than failing mid-game. Restart
|
||||||
|
reloads config from disk (REQ-CFG-RELOAD).
|
||||||
|
* **The sim uses EnTT for ships, stations, debris, and module child entities**, wrapped by
|
||||||
|
`core/EntityAdmin` (registry, factory methods, `forEach<Ts...>` views). Components live
|
||||||
|
in `lib/ecs/component/`, systems in `lib/ecs/system/`. Note: `architecture.md`'s
|
||||||
|
"Ships" and "Why Not ECS" sections still describe the earlier
|
||||||
|
`std::optional<Component>` design and are stale on this point; the code is authoritative.
|
||||||
|
Buildings and the belt subsystem stay outside the entity model.
|
||||||
|
* Ship AI is score-based, not fixed-priority: `AiSystem` runs evaluate → select → execute
|
||||||
|
phases over per-behavior evaluator/executor pairs in `lib/ecs/system/ai/`. Evaluators and
|
||||||
|
executors never mutate the world; world mutation lives in `CombatSystem`,
|
||||||
|
`SalvagerSystem`, `RepairSystem`, `MovementIntentSystem`.
|
||||||
|
* Belt subsystem is behind a narrow port-level interface (`tryPutItem` / `tryTakeItem` /
|
||||||
|
`clearTiles` / `tick` / `forEachVisualItem`); per-tile implementation now, swappable
|
||||||
|
later. No other system asks "what is on tile X".
|
||||||
|
* All inter-widget and sim→UI communication goes through the `EventManager`/`EventHandler`
|
||||||
|
singleton in `lib/eventsystem/` (events in `lib/eventsystem/event/`). The sim itself
|
||||||
|
stays free of EventManager for determinism — it buffers `BeamFiredEvent`s in a vector
|
||||||
|
that the UI drains each frame and re-emits.
|
||||||
|
* State-change events are *refresh signals*, not carriers of truth: a widget re-reads the
|
||||||
|
value from `Simulation` rather than caching the event payload.
|
||||||
28
.claude/skills/bug/SKILL.md
Normal file
28
.claude/skills/bug/SKILL.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: bug
|
||||||
|
description: Investigate a reported bug, find and explain its root cause, and propose a fix — without implementing anything
|
||||||
|
argument-hint: <description of the buggy behavior>
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
A bug has been reported:
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
Investigate it and propose a solution. **Do not implement anything** — no edits, no new files, no fixes applied. The goal of this pass is understanding and a proposal the user can approve first.
|
||||||
|
|
||||||
|
Work through it like this:
|
||||||
|
|
||||||
|
1. **Pin down expected vs. actual.** Restate what the behavior should be and what it actually is. If the report is ambiguous about the conditions that trigger it, note your assumptions explicitly.
|
||||||
|
|
||||||
|
2. **Find the relevant code.** Search for the subsystem(s) involved (Grep/Glob, then Read the actual files). Don't reason from memory or from names alone — read the implementation that runs in this case.
|
||||||
|
|
||||||
|
3. **Trace the real execution path.** Follow the data/control flow step by step for the specific failing scenario. For the tick-based simulation, that means tracing the relevant systems in tick order, including the per-tick progress/cap arithmetic where it matters. Use the project's actual constants (tick rate, belt speed, etc.) rather than hand-waving.
|
||||||
|
|
||||||
|
4. **State the root cause precisely.** Name the exact mechanism, citing `file:line`. Explain *why* it produces the observed symptom — connect the cause to the visible effect concretely (e.g. "single-slot output serializes to one item per full-tile traversal, so items land ~1 tile apart"). Confirm it explains the specific trigger conditions in the report.
|
||||||
|
|
||||||
|
5. **Propose a solution.** Describe the change and where it would go (`file:line`), reusing existing patterns in the codebase. If the symptom has more than one contributing path, say so. If the fix involves a design or balance trade-off (correctness vs. throughput, lossless vs. capped, a visual side effect, etc.), surface it as a decision for the user — give a recommendation, but ask before assuming which behavior they want.
|
||||||
|
|
||||||
|
6. **Stop and hand back.** End with the proposal and any open questions. Offer to implement (and to add tests) only once the user has chosen a direction.
|
||||||
|
|
||||||
|
Keep the write-up grounded in what the code actually does — quote the lines that matter. Adhere to the repository's coding guidelines and architecture notes (see `.claude/CLAUDE.md`) when describing any proposed change.
|
||||||
91
.claude/skills/cpp-pro/SKILL.md
Normal file
91
.claude/skills/cpp-pro/SKILL.md
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
---
|
||||||
|
name: C++ Pro
|
||||||
|
description: Expert C++ developer specializing in modern C++20/23, systems programming, and high-performance computing. Masters template metaprogramming, zero-overhead abstractions, and low-level optimization with emphasis on safety and efficiency.
|
||||||
|
triggers:
|
||||||
|
- C++
|
||||||
|
- C++17
|
||||||
|
- C++20
|
||||||
|
- C++23
|
||||||
|
- modern C++
|
||||||
|
- template metaprogramming
|
||||||
|
- systems programming
|
||||||
|
- performance optimization
|
||||||
|
- SIMD
|
||||||
|
- memory management
|
||||||
|
- CMake
|
||||||
|
role: specialist
|
||||||
|
scope: implementation
|
||||||
|
output-format: code
|
||||||
|
---
|
||||||
|
|
||||||
|
# C++ Pro
|
||||||
|
|
||||||
|
Senior C++ developer with deep expertise in modern C++20/23, systems programming, high-performance computing, and zero-overhead abstractions.
|
||||||
|
|
||||||
|
## Role Definition
|
||||||
|
|
||||||
|
You are a senior C++ engineer with 15+ years of systems programming experience. You specialize in modern C++20/23, template metaprogramming, performance optimization, and building production-grade systems with emphasis on safety, efficiency, and maintainability. You follow C++ Core Guidelines and leverage cutting-edge language features.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
- Building high-performance C++ applications
|
||||||
|
- Implementing template metaprogramming solutions
|
||||||
|
- Optimizing memory-critical systems
|
||||||
|
- Developing concurrent and parallel algorithms
|
||||||
|
- Creating custom allocators and memory pools
|
||||||
|
- Systems programming and embedded development
|
||||||
|
|
||||||
|
## Core Workflow
|
||||||
|
|
||||||
|
1. **Analyze architecture** - Review build system, compiler flags, performance requirements
|
||||||
|
2. **Design with concepts** - Create type-safe interfaces using C++20 concepts
|
||||||
|
3. **Implement zero-cost** - Apply RAII, constexpr, and zero-overhead abstractions
|
||||||
|
4. **Verify quality** - Run sanitizers, static analysis, and performance benchmarks
|
||||||
|
5. **Optimize** - Profile, measure, and apply targeted optimizations
|
||||||
|
|
||||||
|
## Reference Guide
|
||||||
|
|
||||||
|
Load detailed guidance based on context:
|
||||||
|
|
||||||
|
| Topic | Reference | Load When |
|
||||||
|
|-------|-----------|-----------|
|
||||||
|
| Modern C++ Features | `references/modern-cpp.md` | C++20/23 features, concepts, ranges, coroutines |
|
||||||
|
| Template Metaprogramming | `references/templates.md` | Variadic templates, SFINAE, type traits, CRTP |
|
||||||
|
| Memory & Performance | `references/memory-performance.md` | Allocators, SIMD, cache optimization, move semantics |
|
||||||
|
| Concurrency | `references/concurrency.md` | Atomics, lock-free structures, thread pools, coroutines |
|
||||||
|
| Build & Tooling | `references/build-tooling.md` | CMake, sanitizers, static analysis, testing |
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
|
||||||
|
### MUST DO
|
||||||
|
- Follow C++ Core Guidelines
|
||||||
|
- Use concepts for template constraints
|
||||||
|
- Apply RAII universally
|
||||||
|
- Do not use `auto`
|
||||||
|
- Prefer `std::unique_ptr` and `std::shared_ptr`
|
||||||
|
- Write const-correct code
|
||||||
|
- Use forward declarations in header files if possible
|
||||||
|
- Use descriptive functions names and variable names instead of writing comments
|
||||||
|
|
||||||
|
### MUST NOT DO
|
||||||
|
- Use raw `new`/`delete` (prefer smart pointers)
|
||||||
|
- Ignore compiler warnings
|
||||||
|
- Use C-style casts (use static_cast, etc.)
|
||||||
|
- Mix exception and error code patterns inconsistently
|
||||||
|
- Write non-const-correct code
|
||||||
|
- Use `using namespace std` in headers
|
||||||
|
- Ignore undefined behavior
|
||||||
|
- Skip move semantics for expensive types
|
||||||
|
- Write lots of comments
|
||||||
|
|
||||||
|
## Output Templates
|
||||||
|
|
||||||
|
When implementing C++ features, provide:
|
||||||
|
1. Header file with interfaces and templates
|
||||||
|
2. Implementation file (when needed)
|
||||||
|
3. CMakeLists.txt updates (if applicable)
|
||||||
|
4. Test file demonstrating usage
|
||||||
|
|
||||||
|
## Knowledge Reference
|
||||||
|
|
||||||
|
C++20/23, concepts, ranges, coroutines, modules, template metaprogramming, SFINAE, type traits, CRTP, smart pointers, custom allocators, move semantics, RAII, SIMD, atomics, lock-free programming, CMake, Conan, sanitizers, clang-tidy, cppcheck, Catch2, GoogleTest
|
||||||
443
.claude/skills/cpp-pro/references/build-tooling.md
Normal file
443
.claude/skills/cpp-pro/references/build-tooling.md
Normal file
@@ -0,0 +1,443 @@
|
|||||||
|
# Build Systems and Tooling
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: CMake, sanitizers, static analysis, testing frameworks, CI/CD
|
||||||
|
|
||||||
|
## Modern CMake
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Set C++ standard
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
# Export compile commands for tools
|
||||||
|
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
# Compiler warnings
|
||||||
|
if(MSVC)
|
||||||
|
add_compile_options(/W4 /WX)
|
||||||
|
else()
|
||||||
|
add_compile_options(-Wall -Wextra -Wpedantic -Werror)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# Create library target
|
||||||
|
add_library(mylib
|
||||||
|
src/mylib.cpp
|
||||||
|
include/mylib.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(mylib
|
||||||
|
PUBLIC
|
||||||
|
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||||
|
$<INSTALL_INTERFACE:include>
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(mylib PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
# Create executable
|
||||||
|
add_executable(myapp src/main.cpp)
|
||||||
|
target_link_libraries(myapp PRIVATE mylib)
|
||||||
|
|
||||||
|
# Dependencies with FetchContent
|
||||||
|
include(FetchContent)
|
||||||
|
|
||||||
|
FetchContent_Declare(
|
||||||
|
fmt
|
||||||
|
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
|
||||||
|
GIT_TAG 10.1.1
|
||||||
|
)
|
||||||
|
FetchContent_MakeAvailable(fmt)
|
||||||
|
|
||||||
|
target_link_libraries(mylib PUBLIC fmt::fmt)
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
enable_testing()
|
||||||
|
add_subdirectory(tests)
|
||||||
|
|
||||||
|
# Install rules
|
||||||
|
include(GNUInstallDirs)
|
||||||
|
install(TARGETS mylib myapp
|
||||||
|
EXPORT MyProjectTargets
|
||||||
|
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||||
|
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
)
|
||||||
|
|
||||||
|
install(DIRECTORY include/
|
||||||
|
DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Sanitizers
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# AddressSanitizer (ASan) - memory errors
|
||||||
|
set(CMAKE_CXX_FLAGS_ASAN
|
||||||
|
"-g -O1 -fsanitize=address -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for ASan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# UndefinedBehaviorSanitizer (UBSan)
|
||||||
|
set(CMAKE_CXX_FLAGS_UBSAN
|
||||||
|
"-g -O1 -fsanitize=undefined -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for UBSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ThreadSanitizer (TSan) - data races
|
||||||
|
set(CMAKE_CXX_FLAGS_TSAN
|
||||||
|
"-g -O1 -fsanitize=thread -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for TSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# MemorySanitizer (MSan) - uninitialized reads
|
||||||
|
set(CMAKE_CXX_FLAGS_MSAN
|
||||||
|
"-g -O1 -fsanitize=memory -fno-omit-frame-pointer"
|
||||||
|
CACHE STRING "Flags for MSan build"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Usage: cmake -DCMAKE_BUILD_TYPE=ASAN ..
|
||||||
|
```
|
||||||
|
|
||||||
|
## Static Analysis
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .clang-tidy configuration
|
||||||
|
---
|
||||||
|
Checks: >
|
||||||
|
*,
|
||||||
|
-fuchsia-*,
|
||||||
|
-google-*,
|
||||||
|
-llvm-*,
|
||||||
|
-modernize-use-trailing-return-type,
|
||||||
|
-readability-identifier-length
|
||||||
|
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
|
||||||
|
CheckOptions:
|
||||||
|
- key: readability-identifier-naming.ClassCase
|
||||||
|
value: CamelCase
|
||||||
|
- key: readability-identifier-naming.FunctionCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.VariableCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.ConstantCase
|
||||||
|
value: UPPER_CASE
|
||||||
|
- key: readability-identifier-naming.MemberCase
|
||||||
|
value: lower_case
|
||||||
|
- key: readability-identifier-naming.MemberSuffix
|
||||||
|
value: '_'
|
||||||
|
- key: modernize-use-nullptr.NullMacros
|
||||||
|
value: 'NULL'
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run clang-tidy
|
||||||
|
clang-tidy src/*.cpp -p build/
|
||||||
|
|
||||||
|
# Run cppcheck
|
||||||
|
cppcheck --enable=all --std=c++20 --suppress=missingInclude src/
|
||||||
|
|
||||||
|
# Run include-what-you-use
|
||||||
|
include-what-you-use -std=c++20 src/main.cpp
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with Catch2
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <catch2/catch_test_macros.hpp>
|
||||||
|
#include <catch2/benchmark/catch_benchmark.hpp>
|
||||||
|
#include "mylib.h"
|
||||||
|
|
||||||
|
TEST_CASE("Vector operations", "[vector]") {
|
||||||
|
std::vector<int> vec{1, 2, 3};
|
||||||
|
|
||||||
|
SECTION("push_back") {
|
||||||
|
vec.push_back(4);
|
||||||
|
REQUIRE(vec.size() == 4);
|
||||||
|
REQUIRE(vec.back() == 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("pop_back") {
|
||||||
|
vec.pop_back();
|
||||||
|
REQUIRE(vec.size() == 2);
|
||||||
|
REQUIRE(vec.back() == 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Exception handling", "[exceptions]") {
|
||||||
|
REQUIRE_THROWS_AS(risky_function(), std::runtime_error);
|
||||||
|
REQUIRE_THROWS_WITH(risky_function(), "error message");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Floating point", "[math]") {
|
||||||
|
REQUIRE_THAT(compute_value(),
|
||||||
|
Catch::Matchers::WithinAbs(3.14, 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
BENCHMARK("Vector creation") {
|
||||||
|
return std::vector<int>(1000);
|
||||||
|
};
|
||||||
|
|
||||||
|
BENCHMARK("Vector fill") {
|
||||||
|
std::vector<int> vec(1000);
|
||||||
|
for (int i = 0; i < 1000; ++i) {
|
||||||
|
vec[i] = i;
|
||||||
|
}
|
||||||
|
return vec;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing with GoogleTest
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
#include <gmock/gmock.h>
|
||||||
|
#include "calculator.h"
|
||||||
|
|
||||||
|
class CalculatorTest : public ::testing::Test {
|
||||||
|
protected:
|
||||||
|
void SetUp() override {
|
||||||
|
calc = std::make_unique<Calculator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override {
|
||||||
|
calc.reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::unique_ptr<Calculator> calc;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(CalculatorTest, Addition) {
|
||||||
|
EXPECT_EQ(calc->add(2, 3), 5);
|
||||||
|
EXPECT_EQ(calc->add(-1, 1), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(CalculatorTest, Division) {
|
||||||
|
EXPECT_DOUBLE_EQ(calc->divide(10, 2), 5.0);
|
||||||
|
EXPECT_THROW(calc->divide(10, 0), std::invalid_argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameterized tests
|
||||||
|
class AdditionTest : public ::testing::TestWithParam<std::tuple<int, int, int>> {};
|
||||||
|
|
||||||
|
TEST_P(AdditionTest, ValidAddition) {
|
||||||
|
auto [a, b, expected] = GetParam();
|
||||||
|
Calculator calc;
|
||||||
|
EXPECT_EQ(calc.add(a, b), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(
|
||||||
|
AdditionSuite,
|
||||||
|
AdditionTest,
|
||||||
|
::testing::Values(
|
||||||
|
std::make_tuple(1, 2, 3),
|
||||||
|
std::make_tuple(-1, -2, -3),
|
||||||
|
std::make_tuple(0, 0, 0)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mock objects
|
||||||
|
class MockDatabase : public Database {
|
||||||
|
public:
|
||||||
|
MOCK_METHOD(void, connect, (const std::string&), (override));
|
||||||
|
MOCK_METHOD(std::string, query, (const std::string&), (override));
|
||||||
|
MOCK_METHOD(void, disconnect, (), (override));
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST(ServiceTest, UsesDatabase) {
|
||||||
|
MockDatabase mock_db;
|
||||||
|
EXPECT_CALL(mock_db, connect("localhost"))
|
||||||
|
.Times(1);
|
||||||
|
EXPECT_CALL(mock_db, query("SELECT *"))
|
||||||
|
.WillOnce(::testing::Return("result"));
|
||||||
|
|
||||||
|
Service service(mock_db);
|
||||||
|
service.process();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Performance Profiling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Benchmark with Google Benchmark
|
||||||
|
#include <benchmark/benchmark.h>
|
||||||
|
|
||||||
|
static void BM_VectorPush(benchmark::State& state) {
|
||||||
|
for (auto _ : state) {
|
||||||
|
std::vector<int> vec;
|
||||||
|
for (int i = 0; i < state.range(0); ++i) {
|
||||||
|
vec.push_back(i);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_VectorPush)->Range(8, 8<<10);
|
||||||
|
|
||||||
|
static void BM_VectorReserve(benchmark::State& state) {
|
||||||
|
for (auto _ : state) {
|
||||||
|
std::vector<int> vec;
|
||||||
|
vec.reserve(state.range(0));
|
||||||
|
for (int i = 0; i < state.range(0); ++i) {
|
||||||
|
vec.push_back(i);
|
||||||
|
}
|
||||||
|
benchmark::DoNotOptimize(vec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BENCHMARK(BM_VectorReserve)->Range(8, 8<<10);
|
||||||
|
|
||||||
|
BENCHMARK_MAIN();
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Profiling with perf (Linux)
|
||||||
|
perf record -g ./myapp
|
||||||
|
perf report
|
||||||
|
|
||||||
|
# Profiling with Instruments (macOS)
|
||||||
|
instruments -t "Time Profiler" ./myapp
|
||||||
|
|
||||||
|
# Valgrind callgrind
|
||||||
|
valgrind --tool=callgrind ./myapp
|
||||||
|
kcachegrind callgrind.out.*
|
||||||
|
|
||||||
|
# Memory profiling
|
||||||
|
valgrind --tool=massif ./myapp
|
||||||
|
ms_print massif.out.*
|
||||||
|
```
|
||||||
|
|
||||||
|
## Conan Package Manager
|
||||||
|
|
||||||
|
```python
|
||||||
|
# conanfile.txt
|
||||||
|
[requires]
|
||||||
|
fmt/10.1.1
|
||||||
|
spdlog/1.12.0
|
||||||
|
catch2/3.4.0
|
||||||
|
|
||||||
|
[generators]
|
||||||
|
CMakeDeps
|
||||||
|
CMakeToolchain
|
||||||
|
|
||||||
|
[options]
|
||||||
|
fmt:header_only=True
|
||||||
|
```
|
||||||
|
|
||||||
|
```cmake
|
||||||
|
# CMakeLists.txt with Conan
|
||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
project(MyProject)
|
||||||
|
|
||||||
|
find_package(fmt REQUIRED)
|
||||||
|
find_package(spdlog REQUIRED)
|
||||||
|
find_package(Catch2 REQUIRED)
|
||||||
|
|
||||||
|
add_executable(myapp src/main.cpp)
|
||||||
|
target_link_libraries(myapp
|
||||||
|
PRIVATE
|
||||||
|
fmt::fmt
|
||||||
|
spdlog::spdlog
|
||||||
|
)
|
||||||
|
|
||||||
|
add_executable(tests test/main.cpp)
|
||||||
|
target_link_libraries(tests
|
||||||
|
PRIVATE
|
||||||
|
Catch2::Catch2WithMain
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dependencies
|
||||||
|
conan install . --output-folder=build --build=missing
|
||||||
|
cd build
|
||||||
|
cmake .. -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
|
||||||
|
cmake --build .
|
||||||
|
```
|
||||||
|
|
||||||
|
## CI/CD with GitHub Actions
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# .github/workflows/ci.yml
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
compiler: [gcc, clang, msvc]
|
||||||
|
build_type: [Debug, Release]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
pip install conan
|
||||||
|
conan install . --output-folder=build --build=missing
|
||||||
|
|
||||||
|
- name: Configure
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cmake --build build --config ${{ matrix.build_type }}
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: ctest --test-dir build -C ${{ matrix.build_type }}
|
||||||
|
|
||||||
|
sanitizers:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
sanitizer: [asan, ubsan, tsan]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build with sanitizer
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=${{ matrix.sanitizer }}
|
||||||
|
cmake --build build
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: ctest --test-dir build
|
||||||
|
|
||||||
|
static-analysis:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run clang-tidy
|
||||||
|
run: |
|
||||||
|
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
|
||||||
|
clang-tidy src/*.cpp -p build/
|
||||||
|
|
||||||
|
- name: Run cppcheck
|
||||||
|
run: cppcheck --enable=all --error-exitcode=1 src/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Tool | Purpose | Command |
|
||||||
|
|------|---------|---------|
|
||||||
|
| CMake | Build system | `cmake -B build && cmake --build build` |
|
||||||
|
| Conan | Package manager | `conan install . --build=missing` |
|
||||||
|
| ASan | Memory errors | `-fsanitize=address` |
|
||||||
|
| UBSan | Undefined behavior | `-fsanitize=undefined` |
|
||||||
|
| TSan | Data races | `-fsanitize=thread` |
|
||||||
|
| clang-tidy | Static analysis | `clang-tidy src/*.cpp` |
|
||||||
|
| cppcheck | Static analysis | `cppcheck --enable=all src/` |
|
||||||
|
| Catch2 | Unit testing | `TEST_CASE("name") { REQUIRE(...); }` |
|
||||||
|
| GoogleTest | Unit testing | `TEST(Suite, Name) { EXPECT_EQ(...); }` |
|
||||||
|
| Google Benchmark | Performance | `BENCHMARK(func)->Range(...)` |
|
||||||
|
| Valgrind | Memory profiler | `valgrind --tool=memcheck ./app` |
|
||||||
440
.claude/skills/cpp-pro/references/concurrency.md
Normal file
440
.claude/skills/cpp-pro/references/concurrency.md
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
# Concurrency and Parallel Programming
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Atomics, lock-free structures, thread pools, parallel algorithms, coroutines
|
||||||
|
|
||||||
|
## Atomics and Memory Ordering
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <atomic>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// Basic atomics
|
||||||
|
std::atomic<int> counter{0};
|
||||||
|
std::atomic<bool> flag{false};
|
||||||
|
|
||||||
|
// Memory ordering
|
||||||
|
void producer(std::atomic<int>& data, std::atomic<bool>& ready) {
|
||||||
|
data.store(42, std::memory_order_relaxed);
|
||||||
|
ready.store(true, std::memory_order_release); // Release barrier
|
||||||
|
}
|
||||||
|
|
||||||
|
void consumer(std::atomic<int>& data, std::atomic<bool>& ready) {
|
||||||
|
while (!ready.load(std::memory_order_acquire)) { // Acquire barrier
|
||||||
|
std::this_thread::yield();
|
||||||
|
}
|
||||||
|
int value = data.load(std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare-and-swap
|
||||||
|
bool try_acquire_lock(std::atomic<bool>& lock) {
|
||||||
|
bool expected = false;
|
||||||
|
return lock.compare_exchange_strong(expected, true,
|
||||||
|
std::memory_order_acquire,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch-and-add
|
||||||
|
int increment_counter(std::atomic<int>& counter) {
|
||||||
|
return counter.fetch_add(1, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lock-Free Data Structures
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <atomic>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Lock-free stack
|
||||||
|
template<typename T>
|
||||||
|
class LockFreeStack {
|
||||||
|
struct Node {
|
||||||
|
T data;
|
||||||
|
Node* next;
|
||||||
|
Node(const T& value) : data(value), next(nullptr) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::atomic<Node*> head_{nullptr};
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(const T& value) {
|
||||||
|
Node* new_node = new Node(value);
|
||||||
|
new_node->next = head_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
while (!head_.compare_exchange_weak(new_node->next, new_node,
|
||||||
|
std::memory_order_release,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
// Retry with updated head
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(T& result) {
|
||||||
|
Node* old_head = head_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
while (old_head &&
|
||||||
|
!head_.compare_exchange_weak(old_head, old_head->next,
|
||||||
|
std::memory_order_acquire,
|
||||||
|
std::memory_order_relaxed)) {
|
||||||
|
// Retry
|
||||||
|
}
|
||||||
|
|
||||||
|
if (old_head) {
|
||||||
|
result = old_head->data;
|
||||||
|
delete old_head; // Note: ABA problem exists
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lock-free queue (single producer, single consumer)
|
||||||
|
template<typename T, size_t Size>
|
||||||
|
class SPSCQueue {
|
||||||
|
std::array<T, Size> buffer_;
|
||||||
|
alignas(64) std::atomic<size_t> head_{0};
|
||||||
|
alignas(64) std::atomic<size_t> tail_{0};
|
||||||
|
|
||||||
|
public:
|
||||||
|
bool push(const T& item) {
|
||||||
|
size_t head = head_.load(std::memory_order_relaxed);
|
||||||
|
size_t next_head = (head + 1) % Size;
|
||||||
|
|
||||||
|
if (next_head == tail_.load(std::memory_order_acquire)) {
|
||||||
|
return false; // Queue full
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer_[head] = item;
|
||||||
|
head_.store(next_head, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool pop(T& item) {
|
||||||
|
size_t tail = tail_.load(std::memory_order_relaxed);
|
||||||
|
|
||||||
|
if (tail == head_.load(std::memory_order_acquire)) {
|
||||||
|
return false; // Queue empty
|
||||||
|
}
|
||||||
|
|
||||||
|
item = buffer_[tail];
|
||||||
|
tail_.store((tail + 1) % Size, std::memory_order_release);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Thread Pool
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <thread>
|
||||||
|
#include <queue>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <functional>
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
class ThreadPool {
|
||||||
|
std::vector<std::thread> workers_;
|
||||||
|
std::queue<std::function<void()>> tasks_;
|
||||||
|
std::mutex queue_mutex_;
|
||||||
|
std::condition_variable condition_;
|
||||||
|
bool stop_ = false;
|
||||||
|
|
||||||
|
public:
|
||||||
|
ThreadPool(size_t num_threads) {
|
||||||
|
for (size_t i = 0; i < num_threads; ++i) {
|
||||||
|
workers_.emplace_back([this] {
|
||||||
|
while (true) {
|
||||||
|
std::function<void()> task;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
condition_.wait(lock, [this] {
|
||||||
|
return stop_ || !tasks_.empty();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stop_ && tasks_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
task = std::move(tasks_.front());
|
||||||
|
tasks_.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
task();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~ThreadPool() {
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
stop_ = true;
|
||||||
|
}
|
||||||
|
condition_.notify_all();
|
||||||
|
for (auto& worker : workers_) {
|
||||||
|
worker.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename F, typename... Args>
|
||||||
|
auto enqueue(F&& f, Args&&... args)
|
||||||
|
-> std::future<typename std::invoke_result_t<F, Args...>> {
|
||||||
|
|
||||||
|
using return_type = typename std::invoke_result_t<F, Args...>;
|
||||||
|
|
||||||
|
auto task = std::make_shared<std::packaged_task<return_type()>>(
|
||||||
|
std::bind(std::forward<F>(f), std::forward<Args>(args)...)
|
||||||
|
);
|
||||||
|
|
||||||
|
std::future<return_type> result = task->get_future();
|
||||||
|
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lock(queue_mutex_);
|
||||||
|
if (stop_) {
|
||||||
|
throw std::runtime_error("enqueue on stopped ThreadPool");
|
||||||
|
}
|
||||||
|
tasks_.emplace([task]() { (*task)(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
condition_.notify_one();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parallel STL Algorithms
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <algorithm>
|
||||||
|
#include <execution>
|
||||||
|
#include <vector>
|
||||||
|
#include <numeric>
|
||||||
|
|
||||||
|
void parallel_algorithms_demo() {
|
||||||
|
std::vector<int> vec(1'000'000);
|
||||||
|
std::iota(vec.begin(), vec.end(), 0);
|
||||||
|
|
||||||
|
// Parallel sort
|
||||||
|
std::sort(std::execution::par, vec.begin(), vec.end());
|
||||||
|
|
||||||
|
// Parallel for_each
|
||||||
|
std::for_each(std::execution::par_unseq, vec.begin(), vec.end(),
|
||||||
|
[](int& x) { x *= 2; });
|
||||||
|
|
||||||
|
// Parallel transform
|
||||||
|
std::vector<int> result(vec.size());
|
||||||
|
std::transform(std::execution::par, vec.begin(), vec.end(),
|
||||||
|
result.begin(), [](int x) { return x * x; });
|
||||||
|
|
||||||
|
// Parallel reduce
|
||||||
|
int sum = std::reduce(std::execution::par, vec.begin(), vec.end());
|
||||||
|
|
||||||
|
// Parallel transform_reduce (map-reduce)
|
||||||
|
int sum_of_squares = std::transform_reduce(
|
||||||
|
std::execution::par,
|
||||||
|
vec.begin(), vec.end(),
|
||||||
|
0,
|
||||||
|
std::plus<>(),
|
||||||
|
[](int x) { return x * x; }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Synchronization Primitives
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <mutex>
|
||||||
|
#include <shared_mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
|
||||||
|
// Mutex types
|
||||||
|
std::mutex mtx;
|
||||||
|
std::recursive_mutex rec_mtx;
|
||||||
|
std::timed_mutex timed_mtx;
|
||||||
|
std::shared_mutex shared_mtx;
|
||||||
|
|
||||||
|
// RAII locks
|
||||||
|
void exclusive_access() {
|
||||||
|
std::lock_guard<std::mutex> lock(mtx);
|
||||||
|
// Critical section
|
||||||
|
}
|
||||||
|
|
||||||
|
void unique_lock_example() {
|
||||||
|
std::unique_lock<std::mutex> lock(mtx);
|
||||||
|
// Can unlock and relock
|
||||||
|
lock.unlock();
|
||||||
|
// Do some work
|
||||||
|
lock.lock();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reader-writer lock
|
||||||
|
class SharedData {
|
||||||
|
mutable std::shared_mutex mutex_;
|
||||||
|
std::string data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
std::string read() const {
|
||||||
|
std::shared_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
return data_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void write(std::string new_data) {
|
||||||
|
std::unique_lock<std::shared_mutex> lock(mutex_);
|
||||||
|
data_ = std::move(new_data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Condition variable
|
||||||
|
class Queue {
|
||||||
|
std::queue<int> queue_;
|
||||||
|
std::mutex mutex_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(int value) {
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mutex_);
|
||||||
|
queue_.push(value);
|
||||||
|
}
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
int pop() {
|
||||||
|
std::unique_lock<std::mutex> lock(mutex_);
|
||||||
|
cv_.wait(lock, [this] { return !queue_.empty(); });
|
||||||
|
int value = queue_.front();
|
||||||
|
queue_.pop();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// std::scoped_lock - multiple mutexes
|
||||||
|
std::mutex mtx1, mtx2;
|
||||||
|
|
||||||
|
void transfer(Account& from, Account& to, int amount) {
|
||||||
|
std::scoped_lock lock(from.mutex, to.mutex); // Deadlock-free
|
||||||
|
from.balance -= amount;
|
||||||
|
to.balance += amount;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Async and Futures
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
// std::async
|
||||||
|
auto future = std::async(std::launch::async, []() {
|
||||||
|
return expensive_computation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get result (blocks until ready)
|
||||||
|
auto result = future.get();
|
||||||
|
|
||||||
|
// Promise and future
|
||||||
|
void producer(std::promise<int> promise) {
|
||||||
|
int value = compute_value();
|
||||||
|
promise.set_value(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void consumer(std::future<int> future) {
|
||||||
|
int value = future.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::promise<int> promise;
|
||||||
|
std::future<int> future = promise.get_future();
|
||||||
|
|
||||||
|
std::thread producer_thread(producer, std::move(promise));
|
||||||
|
std::thread consumer_thread(consumer, std::move(future));
|
||||||
|
|
||||||
|
// Packaged task
|
||||||
|
std::packaged_task<int(int, int)> task([](int a, int b) {
|
||||||
|
return a + b;
|
||||||
|
});
|
||||||
|
|
||||||
|
std::future<int> task_future = task.get_future();
|
||||||
|
std::thread task_thread(std::move(task), 5, 3);
|
||||||
|
|
||||||
|
int sum = task_future.get(); // 8
|
||||||
|
task_thread.join();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coroutine-Based Concurrency
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <coroutine>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
// Async task coroutine
|
||||||
|
template<typename T>
|
||||||
|
struct AsyncTask {
|
||||||
|
struct promise_type {
|
||||||
|
std::optional<T> value;
|
||||||
|
std::exception_ptr exception;
|
||||||
|
|
||||||
|
AsyncTask get_return_object() {
|
||||||
|
return AsyncTask{
|
||||||
|
std::coroutine_handle<promise_type>::from_promise(*this)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::suspend_never initial_suspend() { return {}; }
|
||||||
|
std::suspend_always final_suspend() noexcept { return {}; }
|
||||||
|
|
||||||
|
void return_value(T v) {
|
||||||
|
value = std::move(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
void unhandled_exception() {
|
||||||
|
exception = std::current_exception();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
|
||||||
|
AsyncTask(std::coroutine_handle<promise_type> h) : handle(h) {}
|
||||||
|
~AsyncTask() { if (handle) handle.destroy(); }
|
||||||
|
|
||||||
|
T get() {
|
||||||
|
if (!handle.done()) {
|
||||||
|
handle.resume();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.promise().exception) {
|
||||||
|
std::rethrow_exception(handle.promise().exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
return *handle.promise().value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
AsyncTask<int> async_compute() {
|
||||||
|
co_return 42;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Primitive | Use Case | Performance |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| std::atomic | Simple shared state | Lock-free |
|
||||||
|
| std::mutex | Exclusive access | Kernel call |
|
||||||
|
| std::shared_mutex | Read-heavy workload | Better than mutex |
|
||||||
|
| Lock-free structures | High contention | Best throughput |
|
||||||
|
| Thread pool | Task parallelism | Avoid thread overhead |
|
||||||
|
| Parallel STL | Data parallelism | Automatic scaling |
|
||||||
|
| std::async | Simple async tasks | Thread pool |
|
||||||
|
| Coroutines | Async I/O | Minimal overhead |
|
||||||
|
|
||||||
|
## Memory Ordering Guide
|
||||||
|
|
||||||
|
| Ordering | Guarantees | Use Case |
|
||||||
|
|----------|-----------|----------|
|
||||||
|
| relaxed | No synchronization | Counters |
|
||||||
|
| acquire | Load barrier | Consumer |
|
||||||
|
| release | Store barrier | Producer |
|
||||||
|
| acq_rel | Both | RMW operations |
|
||||||
|
| seq_cst | Total order | Default |
|
||||||
400
.claude/skills/cpp-pro/references/memory-performance.md
Normal file
400
.claude/skills/cpp-pro/references/memory-performance.md
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# Memory Management & Performance
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Custom allocators, SIMD, cache optimization, move semantics, memory pools
|
||||||
|
|
||||||
|
## Smart Pointers
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// unique_ptr - exclusive ownership
|
||||||
|
auto create_resource() {
|
||||||
|
return std::make_unique<Resource>("data");
|
||||||
|
}
|
||||||
|
|
||||||
|
// shared_ptr - reference counting
|
||||||
|
std::shared_ptr<Data> shared = std::make_shared<Data>(42);
|
||||||
|
std::weak_ptr<Data> weak = shared; // Non-owning reference
|
||||||
|
|
||||||
|
// Custom deleters
|
||||||
|
auto file_deleter = [](FILE* fp) { if (fp) fclose(fp); };
|
||||||
|
std::unique_ptr<FILE, decltype(file_deleter)> file(
|
||||||
|
fopen("data.txt", "r"),
|
||||||
|
file_deleter
|
||||||
|
);
|
||||||
|
|
||||||
|
// enable_shared_from_this
|
||||||
|
class Node : public std::enable_shared_from_this<Node> {
|
||||||
|
public:
|
||||||
|
std::shared_ptr<Node> get_shared() {
|
||||||
|
return shared_from_this();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Allocators
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Pool allocator for fixed-size objects
|
||||||
|
template<typename T, size_t PoolSize = 1024>
|
||||||
|
class PoolAllocator {
|
||||||
|
struct Block {
|
||||||
|
alignas(T) std::byte data[sizeof(T)];
|
||||||
|
Block* next;
|
||||||
|
};
|
||||||
|
|
||||||
|
Block pool_[PoolSize];
|
||||||
|
Block* free_list_ = nullptr;
|
||||||
|
|
||||||
|
public:
|
||||||
|
using value_type = T;
|
||||||
|
|
||||||
|
PoolAllocator() {
|
||||||
|
// Initialize free list
|
||||||
|
for (size_t i = 0; i < PoolSize - 1; ++i) {
|
||||||
|
pool_[i].next = &pool_[i + 1];
|
||||||
|
}
|
||||||
|
pool_[PoolSize - 1].next = nullptr;
|
||||||
|
free_list_ = &pool_[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
T* allocate(size_t n) {
|
||||||
|
if (n != 1 || !free_list_) {
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
Block* block = free_list_;
|
||||||
|
free_list_ = free_list_->next;
|
||||||
|
return reinterpret_cast<T*>(block->data);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate(T* p, size_t n) {
|
||||||
|
if (n != 1) return;
|
||||||
|
Block* block = reinterpret_cast<Block*>(p);
|
||||||
|
block->next = free_list_;
|
||||||
|
free_list_ = block;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
std::vector<int, PoolAllocator<int>> vec;
|
||||||
|
|
||||||
|
// Arena allocator - bump allocator
|
||||||
|
class Arena {
|
||||||
|
std::byte* buffer_;
|
||||||
|
size_t size_;
|
||||||
|
size_t offset_ = 0;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Arena(size_t size) : size_(size) {
|
||||||
|
buffer_ = new std::byte[size];
|
||||||
|
}
|
||||||
|
|
||||||
|
~Arena() {
|
||||||
|
delete[] buffer_;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
T* allocate(size_t n = 1) {
|
||||||
|
size_t alignment = alignof(T);
|
||||||
|
size_t space = size_ - offset_;
|
||||||
|
void* ptr = buffer_ + offset_;
|
||||||
|
|
||||||
|
if (std::align(alignment, sizeof(T) * n, ptr, space)) {
|
||||||
|
offset_ = size_ - space + sizeof(T) * n;
|
||||||
|
return static_cast<T*>(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
|
||||||
|
void reset() {
|
||||||
|
offset_ = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Move Semantics
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <utility>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
class Buffer {
|
||||||
|
size_t size_;
|
||||||
|
char* data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
// Constructor
|
||||||
|
Buffer(size_t size) : size_(size), data_(new char[size]) {}
|
||||||
|
|
||||||
|
// Destructor
|
||||||
|
~Buffer() { delete[] data_; }
|
||||||
|
|
||||||
|
// Copy constructor
|
||||||
|
Buffer(const Buffer& other) : size_(other.size_), data_(new char[size_]) {
|
||||||
|
std::copy(other.data_, other.data_ + size_, data_);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assignment
|
||||||
|
Buffer& operator=(const Buffer& other) {
|
||||||
|
if (this != &other) {
|
||||||
|
delete[] data_;
|
||||||
|
size_ = other.size_;
|
||||||
|
data_ = new char[size_];
|
||||||
|
std::copy(other.data_, other.data_ + size_, data_);
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move constructor
|
||||||
|
Buffer(Buffer&& other) noexcept
|
||||||
|
: size_(other.size_), data_(other.data_) {
|
||||||
|
other.size_ = 0;
|
||||||
|
other.data_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Move assignment
|
||||||
|
Buffer& operator=(Buffer&& other) noexcept {
|
||||||
|
if (this != &other) {
|
||||||
|
delete[] data_;
|
||||||
|
size_ = other.size_;
|
||||||
|
data_ = other.data_;
|
||||||
|
other.size_ = 0;
|
||||||
|
other.data_ = nullptr;
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Perfect forwarding
|
||||||
|
template<typename T>
|
||||||
|
void wrapper(T&& arg) {
|
||||||
|
process(std::forward<T>(arg)); // Preserves lvalue/rvalue
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SIMD Optimization
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <immintrin.h> // AVX/AVX2
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// Vectorized sum using AVX2
|
||||||
|
float simd_sum(const float* data, size_t size) {
|
||||||
|
__m256 sum_vec = _mm256_setzero_ps();
|
||||||
|
|
||||||
|
size_t i = 0;
|
||||||
|
// Process 8 floats at a time
|
||||||
|
for (; i + 8 <= size; i += 8) {
|
||||||
|
__m256 vec = _mm256_loadu_ps(&data[i]);
|
||||||
|
sum_vec = _mm256_add_ps(sum_vec, vec);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal sum
|
||||||
|
alignas(32) float temp[8];
|
||||||
|
_mm256_store_ps(temp, sum_vec);
|
||||||
|
float result = 0.0f;
|
||||||
|
for (int j = 0; j < 8; ++j) {
|
||||||
|
result += temp[j];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle remaining elements
|
||||||
|
for (; i < size; ++i) {
|
||||||
|
result += data[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vectorized multiply-add
|
||||||
|
void fma_operation(float* result, const float* a, const float* b,
|
||||||
|
const float* c, size_t size) {
|
||||||
|
for (size_t i = 0; i + 8 <= size; i += 8) {
|
||||||
|
__m256 va = _mm256_loadu_ps(&a[i]);
|
||||||
|
__m256 vb = _mm256_loadu_ps(&b[i]);
|
||||||
|
__m256 vc = _mm256_loadu_ps(&c[i]);
|
||||||
|
|
||||||
|
// result[i] = a[i] * b[i] + c[i]
|
||||||
|
__m256 vr = _mm256_fmadd_ps(va, vb, vc);
|
||||||
|
_mm256_storeu_ps(&result[i], vr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Cache-Friendly Design
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Structure of Arrays (SoA) - better cache locality
|
||||||
|
struct ParticlesAoS {
|
||||||
|
struct Particle {
|
||||||
|
float x, y, z;
|
||||||
|
float vx, vy, vz;
|
||||||
|
};
|
||||||
|
std::vector<Particle> particles;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ParticlesSoA {
|
||||||
|
std::vector<float> x, y, z;
|
||||||
|
std::vector<float> vx, vy, vz;
|
||||||
|
|
||||||
|
void update_positions(float dt) {
|
||||||
|
// All x coordinates are contiguous - better cache usage
|
||||||
|
for (size_t i = 0; i < x.size(); ++i) {
|
||||||
|
x[i] += vx[i] * dt;
|
||||||
|
y[i] += vy[i] * dt;
|
||||||
|
z[i] += vz[i] * dt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Cache line padding to avoid false sharing
|
||||||
|
struct alignas(64) CacheLinePadded {
|
||||||
|
std::atomic<int> counter;
|
||||||
|
char padding[64 - sizeof(std::atomic<int>)];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prefetching
|
||||||
|
void process_with_prefetch(const int* data, size_t size) {
|
||||||
|
for (size_t i = 0; i < size; ++i) {
|
||||||
|
// Prefetch data for next iteration
|
||||||
|
if (i + 8 < size) {
|
||||||
|
__builtin_prefetch(&data[i + 8], 0, 1);
|
||||||
|
}
|
||||||
|
// Process current data
|
||||||
|
process(data[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Memory Pool
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <vector>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
template<typename T, size_t ChunkSize = 256>
|
||||||
|
class MemoryPool {
|
||||||
|
struct Chunk {
|
||||||
|
alignas(T) std::byte data[sizeof(T) * ChunkSize];
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::unique_ptr<Chunk>> chunks_;
|
||||||
|
std::vector<T*> free_list_;
|
||||||
|
size_t current_chunk_offset_ = ChunkSize;
|
||||||
|
|
||||||
|
public:
|
||||||
|
T* allocate() {
|
||||||
|
if (!free_list_.empty()) {
|
||||||
|
T* ptr = free_list_.back();
|
||||||
|
free_list_.pop_back();
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current_chunk_offset_ >= ChunkSize) {
|
||||||
|
chunks_.push_back(std::make_unique<Chunk>());
|
||||||
|
current_chunk_offset_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Chunk* chunk = chunks_.back().get();
|
||||||
|
T* ptr = reinterpret_cast<T*>(
|
||||||
|
&chunk->data[sizeof(T) * current_chunk_offset_++]
|
||||||
|
);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void deallocate(T* ptr) {
|
||||||
|
free_list_.push_back(ptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
T* construct(Args&&... args) {
|
||||||
|
T* ptr = allocate();
|
||||||
|
new (ptr) T(std::forward<Args>(args)...);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void destroy(T* ptr) {
|
||||||
|
ptr->~T();
|
||||||
|
deallocate(ptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Copy Elision and RVO
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Return Value Optimization (RVO)
|
||||||
|
std::vector<int> create_vector() {
|
||||||
|
std::vector<int> vec{1, 2, 3, 4, 5};
|
||||||
|
return vec; // RVO applies, no copy/move
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named Return Value Optimization (NRVO)
|
||||||
|
std::string build_string(bool condition) {
|
||||||
|
std::string result;
|
||||||
|
if (condition) {
|
||||||
|
result = "condition true";
|
||||||
|
} else {
|
||||||
|
result = "condition false";
|
||||||
|
}
|
||||||
|
return result; // NRVO may apply
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guaranteed copy elision (C++17)
|
||||||
|
struct NonMovable {
|
||||||
|
NonMovable() = default;
|
||||||
|
NonMovable(const NonMovable&) = delete;
|
||||||
|
NonMovable(NonMovable&&) = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
NonMovable create() {
|
||||||
|
return NonMovable{}; // Guaranteed no copy/move in C++17
|
||||||
|
}
|
||||||
|
|
||||||
|
auto obj = create(); // OK in C++17
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alignment and Memory Layout
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <cstddef>
|
||||||
|
|
||||||
|
// Control alignment
|
||||||
|
struct alignas(64) CacheAligned {
|
||||||
|
int data[16];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check alignment
|
||||||
|
static_assert(alignof(CacheAligned) == 64);
|
||||||
|
|
||||||
|
// Aligned allocation
|
||||||
|
void* aligned_alloc_wrapper(size_t alignment, size_t size) {
|
||||||
|
void* ptr = nullptr;
|
||||||
|
if (posix_memalign(&ptr, alignment, size) != 0) {
|
||||||
|
throw std::bad_alloc();
|
||||||
|
}
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Placement new with alignment
|
||||||
|
alignas(32) std::byte buffer[sizeof(Data)];
|
||||||
|
Data* obj = new (buffer) Data();
|
||||||
|
obj->~Data(); // Manual destruction needed
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Technique | Use Case | Benefit |
|
||||||
|
|-----------|----------|---------|
|
||||||
|
| Smart Pointers | Ownership management | Memory safety |
|
||||||
|
| Move Semantics | Avoid copies | Performance |
|
||||||
|
| Custom Allocators | Specialized allocation | Speed + control |
|
||||||
|
| SIMD | Parallel computation | 4-8x speedup |
|
||||||
|
| SoA Layout | Sequential access | Cache efficiency |
|
||||||
|
| Memory Pools | Frequent alloc/dealloc | Reduced fragmentation |
|
||||||
|
| Alignment | SIMD/cache optimization | Performance |
|
||||||
|
| RVO/NRVO | Return objects | Zero-copy |
|
||||||
307
.claude/skills/cpp-pro/references/modern-cpp.md
Normal file
307
.claude/skills/cpp-pro/references/modern-cpp.md
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
# Modern C++20/23 Features
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Using C++20/23 features, concepts, ranges, coroutines, modules
|
||||||
|
|
||||||
|
## Concepts and Constraints
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <concepts>
|
||||||
|
|
||||||
|
// Define custom concepts
|
||||||
|
template<typename T>
|
||||||
|
concept Numeric = std::integral<T> || std::floating_point<T>;
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
concept Hashable = requires(T a) {
|
||||||
|
{ std::hash<T>{}(a) } -> std::convertible_to<std::size_t>;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
concept Container = requires(T c) {
|
||||||
|
typename T::value_type;
|
||||||
|
typename T::iterator;
|
||||||
|
{ c.begin() } -> std::same_as<typename T::iterator>;
|
||||||
|
{ c.end() } -> std::same_as<typename T::iterator>;
|
||||||
|
{ c.size() } -> std::convertible_to<std::size_t>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use concepts for function constraints
|
||||||
|
template<Numeric T>
|
||||||
|
T add(T a, T b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Concept-based overloading
|
||||||
|
template<std::integral T>
|
||||||
|
void process(T value) {
|
||||||
|
std::cout << "Processing integer: " << value << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
template<std::floating_point T>
|
||||||
|
void process(T value) {
|
||||||
|
std::cout << "Processing float: " << value << '\n';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ranges and Views
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <ranges>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// Ranges-based algorithms
|
||||||
|
std::vector<int> numbers = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||||||
|
|
||||||
|
// Filter, transform, take - all lazy evaluation
|
||||||
|
auto result = numbers
|
||||||
|
| std::views::filter([](int n) { return n % 2 == 0; })
|
||||||
|
| std::views::transform([](int n) { return n * n; })
|
||||||
|
| std::views::take(3);
|
||||||
|
|
||||||
|
// Copy to vector only when needed
|
||||||
|
std::vector<int> materialized(result.begin(), result.end());
|
||||||
|
|
||||||
|
// Custom range adaptor
|
||||||
|
auto is_even = [](int n) { return n % 2 == 0; };
|
||||||
|
auto square = [](int n) { return n * n; };
|
||||||
|
|
||||||
|
auto pipeline = std::views::filter(is_even)
|
||||||
|
| std::views::transform(square);
|
||||||
|
|
||||||
|
auto processed = numbers | pipeline;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Coroutines
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <coroutine>
|
||||||
|
#include <iostream>
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
// Generator coroutine
|
||||||
|
template<typename T>
|
||||||
|
struct Generator {
|
||||||
|
struct promise_type {
|
||||||
|
T current_value;
|
||||||
|
|
||||||
|
auto get_return_object() {
|
||||||
|
return Generator{std::coroutine_handle<promise_type>::from_promise(*this)};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::suspend_always initial_suspend() { return {}; }
|
||||||
|
std::suspend_always final_suspend() noexcept { return {}; }
|
||||||
|
|
||||||
|
std::suspend_always yield_value(T value) {
|
||||||
|
current_value = value;
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void return_void() {}
|
||||||
|
void unhandled_exception() { std::terminate(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
|
||||||
|
Generator(std::coroutine_handle<promise_type> h) : handle(h) {}
|
||||||
|
~Generator() { if (handle) handle.destroy(); }
|
||||||
|
|
||||||
|
bool move_next() {
|
||||||
|
handle.resume();
|
||||||
|
return !handle.done();
|
||||||
|
}
|
||||||
|
|
||||||
|
T current_value() {
|
||||||
|
return handle.promise().current_value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage
|
||||||
|
Generator<int> fibonacci() {
|
||||||
|
int a = 0, b = 1;
|
||||||
|
while (true) {
|
||||||
|
co_yield a;
|
||||||
|
auto next = a + b;
|
||||||
|
a = b;
|
||||||
|
b = next;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Async coroutine
|
||||||
|
#include <future>
|
||||||
|
|
||||||
|
struct Task {
|
||||||
|
struct promise_type {
|
||||||
|
Task get_return_object() {
|
||||||
|
return Task{std::coroutine_handle<promise_type>::from_promise(*this)};
|
||||||
|
}
|
||||||
|
std::suspend_never initial_suspend() { return {}; }
|
||||||
|
std::suspend_never final_suspend() noexcept { return {}; }
|
||||||
|
void return_void() {}
|
||||||
|
void unhandled_exception() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::coroutine_handle<promise_type> handle;
|
||||||
|
};
|
||||||
|
|
||||||
|
Task async_operation() {
|
||||||
|
std::cout << "Starting async work\n";
|
||||||
|
co_await std::suspend_always{};
|
||||||
|
std::cout << "Resuming async work\n";
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Three-Way Comparison (Spaceship)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <compare>
|
||||||
|
|
||||||
|
struct Point {
|
||||||
|
int x, y;
|
||||||
|
|
||||||
|
// Auto-generate all comparison operators
|
||||||
|
auto operator<=>(const Point&) const = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Custom spaceship operator
|
||||||
|
struct Version {
|
||||||
|
int major, minor, patch;
|
||||||
|
|
||||||
|
std::strong_ordering operator<=>(const Version& other) const {
|
||||||
|
if (auto cmp = major <=> other.major; cmp != 0) return cmp;
|
||||||
|
if (auto cmp = minor <=> other.minor; cmp != 0) return cmp;
|
||||||
|
return patch <=> other.patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool operator==(const Version& other) const = default;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Designated Initializers
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
struct Config {
|
||||||
|
std::string host = "localhost";
|
||||||
|
int port = 8080;
|
||||||
|
bool ssl_enabled = false;
|
||||||
|
int timeout_ms = 5000;
|
||||||
|
};
|
||||||
|
|
||||||
|
// C++20 designated initializers
|
||||||
|
Config cfg {
|
||||||
|
.host = "example.com",
|
||||||
|
.port = 443,
|
||||||
|
.ssl_enabled = true
|
||||||
|
// timeout_ms uses default
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modules (C++20)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// math.cppm - module interface
|
||||||
|
export module math;
|
||||||
|
|
||||||
|
export namespace math {
|
||||||
|
template<typename T>
|
||||||
|
T add(T a, T b) {
|
||||||
|
return a + b;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Calculator {
|
||||||
|
public:
|
||||||
|
int multiply(int a, int b);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implementation
|
||||||
|
module math;
|
||||||
|
|
||||||
|
int math::Calculator::multiply(int a, int b) {
|
||||||
|
return a * b;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage in other files
|
||||||
|
import math;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
auto result = math::add(5, 3);
|
||||||
|
math::Calculator calc;
|
||||||
|
auto product = calc.multiply(4, 7);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## constexpr Enhancements
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
// C++20: constexpr std::string and std::vector
|
||||||
|
constexpr auto compute_at_compile_time() {
|
||||||
|
std::vector<int> vec{1, 2, 3, 4, 5};
|
||||||
|
std::ranges::reverse(vec);
|
||||||
|
return vec[0]; // Returns 5
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int value = compute_at_compile_time();
|
||||||
|
|
||||||
|
// constexpr virtual functions (C++20)
|
||||||
|
struct Base {
|
||||||
|
constexpr virtual int get_value() const { return 42; }
|
||||||
|
constexpr virtual ~Base() = default;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Derived : Base {
|
||||||
|
constexpr int get_value() const override { return 100; }
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## std::format (C++20)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <format>
|
||||||
|
#include <iostream>
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::string msg = std::format("Hello, {}!", "World");
|
||||||
|
|
||||||
|
// Positional arguments
|
||||||
|
auto text = std::format("{1} {0}", "World", "Hello");
|
||||||
|
|
||||||
|
// Formatting options
|
||||||
|
double pi = 3.14159265;
|
||||||
|
auto formatted = std::format("Pi: {:.2f}", pi); // "Pi: 3.14"
|
||||||
|
|
||||||
|
// Custom types
|
||||||
|
struct Point { int x, y; };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom formatter
|
||||||
|
template<>
|
||||||
|
struct std::formatter<Point> {
|
||||||
|
constexpr auto parse(format_parse_context& ctx) {
|
||||||
|
return ctx.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto format(const Point& p, format_context& ctx) const {
|
||||||
|
return std::format_to(ctx.out(), "({}, {})", p.x, p.y);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Feature | C++17 | C++20 | C++23 |
|
||||||
|
|---------|-------|-------|-------|
|
||||||
|
| Concepts | - | ✓ | ✓ |
|
||||||
|
| Ranges | - | ✓ | ✓ |
|
||||||
|
| Coroutines | - | ✓ | ✓ |
|
||||||
|
| Modules | - | ✓ | ✓ |
|
||||||
|
| Spaceship | - | ✓ | ✓ |
|
||||||
|
| std::format | - | ✓ | ✓ |
|
||||||
|
| std::expected | - | - | ✓ |
|
||||||
|
| std::print | - | - | ✓ |
|
||||||
|
| Deducing this | - | - | ✓ |
|
||||||
360
.claude/skills/cpp-pro/references/templates.md
Normal file
360
.claude/skills/cpp-pro/references/templates.md
Normal file
@@ -0,0 +1,360 @@
|
|||||||
|
# Template Metaprogramming
|
||||||
|
|
||||||
|
> Reference for: C++ Pro
|
||||||
|
> Load when: Variadic templates, SFINAE, type traits, CRTP, compile-time programming
|
||||||
|
|
||||||
|
## Variadic Templates
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <iostream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
// Fold expressions (C++17)
|
||||||
|
template<typename... Args>
|
||||||
|
auto sum(Args... args) {
|
||||||
|
return (args + ...); // Unary right fold
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
void print(Args&&... args) {
|
||||||
|
((std::cout << args << ' '), ...); // Binary left fold
|
||||||
|
std::cout << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive variadic template
|
||||||
|
template<typename T>
|
||||||
|
void log(T&& value) {
|
||||||
|
std::cout << value << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T, typename... Args>
|
||||||
|
void log(T&& first, Args&&... rest) {
|
||||||
|
std::cout << first << ", ";
|
||||||
|
log(std::forward<Args>(rest)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parameter pack expansion
|
||||||
|
template<typename... Types>
|
||||||
|
struct TypeList {
|
||||||
|
static constexpr size_t size = sizeof...(Types);
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename... Args>
|
||||||
|
auto make_tuple_advanced(Args&&... args) {
|
||||||
|
return std::tuple<std::decay_t<Args>...>(std::forward<Args>(args)...);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## SFINAE and if constexpr
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// SFINAE with std::enable_if (older style)
|
||||||
|
template<typename T>
|
||||||
|
std::enable_if_t<std::is_integral_v<T>, T>
|
||||||
|
double_value(T value) {
|
||||||
|
return value * 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
std::enable_if_t<std::is_floating_point_v<T>, T>
|
||||||
|
double_value(T value) {
|
||||||
|
return value * 2.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modern: if constexpr (C++17)
|
||||||
|
template<typename T>
|
||||||
|
auto process(T value) {
|
||||||
|
if constexpr (std::is_integral_v<T>) {
|
||||||
|
return value * 2;
|
||||||
|
} else if constexpr (std::is_floating_point_v<T>) {
|
||||||
|
return value * 2.0;
|
||||||
|
} else {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detection idiom
|
||||||
|
template<typename T, typename = void>
|
||||||
|
struct has_serialize : std::false_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct has_serialize<T, std::void_t<decltype(std::declval<T>().serialize())>>
|
||||||
|
: std::true_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
constexpr bool has_serialize_v = has_serialize<T>::value;
|
||||||
|
|
||||||
|
// Use with if constexpr
|
||||||
|
template<typename T>
|
||||||
|
void save(const T& obj) {
|
||||||
|
if constexpr (has_serialize_v<T>) {
|
||||||
|
obj.serialize();
|
||||||
|
} else {
|
||||||
|
// Default serialization
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Type Traits
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <type_traits>
|
||||||
|
|
||||||
|
// Custom type traits
|
||||||
|
template<typename T>
|
||||||
|
struct remove_all_pointers {
|
||||||
|
using type = T;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct remove_all_pointers<T*> {
|
||||||
|
using type = typename remove_all_pointers<T>::type;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
using remove_all_pointers_t = typename remove_all_pointers<T>::type;
|
||||||
|
|
||||||
|
// Conditional types
|
||||||
|
template<bool Condition, typename T, typename F>
|
||||||
|
struct conditional_type {
|
||||||
|
using type = T;
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T, typename F>
|
||||||
|
struct conditional_type<false, T, F> {
|
||||||
|
using type = F;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Compile-time type selection
|
||||||
|
template<size_t N>
|
||||||
|
struct best_integral_type {
|
||||||
|
using type = std::conditional_t<N <= 8, uint8_t,
|
||||||
|
std::conditional_t<N <= 16, uint16_t,
|
||||||
|
std::conditional_t<N <= 32, uint32_t, uint64_t>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check for member functions
|
||||||
|
template<typename T, typename = void>
|
||||||
|
struct has_reserve : std::false_type {};
|
||||||
|
|
||||||
|
template<typename T>
|
||||||
|
struct has_reserve<T, std::void_t<decltype(std::declval<T>().reserve(size_t{}))>>
|
||||||
|
: std::true_type {};
|
||||||
|
```
|
||||||
|
|
||||||
|
## CRTP (Curiously Recurring Template Pattern)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Static polymorphism with CRTP
|
||||||
|
template<typename Derived>
|
||||||
|
class Shape {
|
||||||
|
public:
|
||||||
|
double area() const {
|
||||||
|
return static_cast<const Derived*>(this)->area_impl();
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw() const {
|
||||||
|
static_cast<const Derived*>(this)->draw_impl();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Circle : public Shape<Circle> {
|
||||||
|
double radius_;
|
||||||
|
public:
|
||||||
|
Circle(double r) : radius_(r) {}
|
||||||
|
|
||||||
|
double area_impl() const {
|
||||||
|
return 3.14159 * radius_ * radius_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_impl() const {
|
||||||
|
std::cout << "Drawing circle\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Rectangle : public Shape<Rectangle> {
|
||||||
|
double width_, height_;
|
||||||
|
public:
|
||||||
|
Rectangle(double w, double h) : width_(w), height_(h) {}
|
||||||
|
|
||||||
|
double area_impl() const {
|
||||||
|
return width_ * height_;
|
||||||
|
}
|
||||||
|
|
||||||
|
void draw_impl() const {
|
||||||
|
std::cout << "Drawing rectangle\n";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// CRTP for mixin capabilities
|
||||||
|
template<typename Derived>
|
||||||
|
class Printable {
|
||||||
|
public:
|
||||||
|
void print() const {
|
||||||
|
std::cout << static_cast<const Derived*>(this)->to_string() << '\n';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class User : public Printable<User> {
|
||||||
|
std::string name_;
|
||||||
|
public:
|
||||||
|
User(std::string name) : name_(std::move(name)) {}
|
||||||
|
|
||||||
|
std::string to_string() const {
|
||||||
|
return "User: " + name_;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## Template Template Parameters
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <vector>
|
||||||
|
#include <list>
|
||||||
|
#include <deque>
|
||||||
|
|
||||||
|
// Template template parameter
|
||||||
|
template<typename T, template<typename, typename> class Container>
|
||||||
|
class Stack {
|
||||||
|
Container<T, std::allocator<T>> data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
void push(const T& value) {
|
||||||
|
data_.push_back(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
T pop() {
|
||||||
|
T value = data_.back();
|
||||||
|
data_.pop_back();
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const {
|
||||||
|
return data_.size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Usage with different containers
|
||||||
|
Stack<int, std::vector> vector_stack;
|
||||||
|
Stack<int, std::deque> deque_stack;
|
||||||
|
Stack<int, std::list> list_stack;
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile-Time Computation
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include <array>
|
||||||
|
|
||||||
|
// Compile-time factorial
|
||||||
|
constexpr int factorial(int n) {
|
||||||
|
return n <= 1 ? 1 : n * factorial(n - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr int fact_5 = factorial(5); // Computed at compile time
|
||||||
|
|
||||||
|
// Compile-time prime checking
|
||||||
|
constexpr bool is_prime(int n) {
|
||||||
|
if (n < 2) return false;
|
||||||
|
for (int i = 2; i * i <= n; ++i) {
|
||||||
|
if (n % i == 0) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate compile-time array of primes
|
||||||
|
template<size_t N>
|
||||||
|
constexpr auto generate_primes() {
|
||||||
|
std::array<int, N> primes{};
|
||||||
|
int count = 0;
|
||||||
|
int candidate = 2;
|
||||||
|
|
||||||
|
while (count < N) {
|
||||||
|
if (is_prime(candidate)) {
|
||||||
|
primes[count++] = candidate;
|
||||||
|
}
|
||||||
|
++candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primes;
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto first_10_primes = generate_primes<10>();
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expression Templates
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Lazy evaluation with expression templates
|
||||||
|
template<typename E>
|
||||||
|
class VecExpression {
|
||||||
|
public:
|
||||||
|
double operator[](size_t i) const {
|
||||||
|
return static_cast<const E&>(*this)[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const {
|
||||||
|
return static_cast<const E&>(*this).size();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Vec : public VecExpression<Vec> {
|
||||||
|
std::vector<double> data_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
Vec(size_t n) : data_(n) {}
|
||||||
|
|
||||||
|
double operator[](size_t i) const { return data_[i]; }
|
||||||
|
double& operator[](size_t i) { return data_[i]; }
|
||||||
|
size_t size() const { return data_.size(); }
|
||||||
|
|
||||||
|
// Evaluate expression template
|
||||||
|
template<typename E>
|
||||||
|
Vec& operator=(const VecExpression<E>& expr) {
|
||||||
|
for (size_t i = 0; i < size(); ++i) {
|
||||||
|
data_[i] = expr[i];
|
||||||
|
}
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Binary operation expression
|
||||||
|
template<typename E1, typename E2>
|
||||||
|
class VecSum : public VecExpression<VecSum<E1, E2>> {
|
||||||
|
const E1& lhs_;
|
||||||
|
const E2& rhs_;
|
||||||
|
|
||||||
|
public:
|
||||||
|
VecSum(const E1& lhs, const E2& rhs) : lhs_(lhs), rhs_(rhs) {}
|
||||||
|
|
||||||
|
double operator[](size_t i) const {
|
||||||
|
return lhs_[i] + rhs_[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t size() const { return lhs_.size(); }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Operator overload
|
||||||
|
template<typename E1, typename E2>
|
||||||
|
VecSum<E1, E2> operator+(const VecExpression<E1>& lhs,
|
||||||
|
const VecExpression<E2>& rhs) {
|
||||||
|
return VecSum<E1, E2>(static_cast<const E1&>(lhs),
|
||||||
|
static_cast<const E2&>(rhs));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: a = b + c + d (no temporaries created!)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Technique | Use Case | Performance |
|
||||||
|
|-----------|----------|-------------|
|
||||||
|
| Variadic Templates | Variable arguments | Zero overhead |
|
||||||
|
| SFINAE | Conditional compilation | Compile-time |
|
||||||
|
| if constexpr | Type-based branching | Zero overhead |
|
||||||
|
| CRTP | Static polymorphism | No vtable cost |
|
||||||
|
| Expression Templates | Lazy evaluation | Eliminates temps |
|
||||||
|
| Type Traits | Type introspection | Compile-time |
|
||||||
|
| Fold Expressions | Parameter pack ops | Optimal |
|
||||||
|
| Template Specialization | Type-specific impl | Zero overhead |
|
||||||
12
.claude/skills/requirements/SKILL.md
Normal file
12
.claude/skills/requirements/SKILL.md
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
name: requirements
|
||||||
|
description: Update the Dota Factory requirements document with new or changed requirements
|
||||||
|
argument-hint: <description of new requirement>
|
||||||
|
disable-model-invocation: true
|
||||||
|
---
|
||||||
|
|
||||||
|
Read `docs/requirements.md`, then help the user update the requirements with the following change:
|
||||||
|
|
||||||
|
$ARGUMENTS
|
||||||
|
|
||||||
|
Ask any clarifying questions if the request is ambiguous, or flag any conflicts with existing requirements before making changes. Do not make changes to the requirements file before the answers are clear.
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1 +1,5 @@
|
|||||||
/build/
|
/build/
|
||||||
|
|
||||||
|
# local Claude Code config (machine-specific; .mcp.json holds credentials)
|
||||||
|
/.mcp.json
|
||||||
|
/.claude/settings.local.json
|
||||||
|
|||||||
@@ -136,17 +136,43 @@ Belts and splitters are their own specialized subsystem. Belt items are **not**
|
|||||||
|
|
||||||
### Public Interface
|
### Public Interface
|
||||||
|
|
||||||
Narrow and representation-agnostic:
|
`BeltSystem.h` is authoritative. The surface is wider than the original design sketch — 15 public methods in five groups, not the 5-method port interface this section used to describe:
|
||||||
|
|
||||||
```cpp
|
```cpp
|
||||||
class BeltSystem {
|
class BeltSystem {
|
||||||
public:
|
public:
|
||||||
bool tryPutItem(Port port, Item item);
|
// Placement — belts/splitters/tunnels are Buildings for cost and
|
||||||
std::optional<Item> tryTakeItem(Port port);
|
// construction, so BuildingSystem registers and unregisters their tiles.
|
||||||
|
void placeBelt(QPoint tile, Rotation direction);
|
||||||
|
void placeTunnelEntry(QPoint tile, Rotation direction, int maxDistance);
|
||||||
|
void placeTunnelExit(QPoint tile, Rotation direction);
|
||||||
|
void placeSplitter(QPoint tile, Rotation outputA, Rotation outputB);
|
||||||
|
void removeTile(QPoint tile);
|
||||||
|
|
||||||
|
// Splitter filter configuration (REQ-BLD-SPLITTER). A splitter's filters
|
||||||
|
// live here, not on Building, so callers that re-register a tile must
|
||||||
|
// carry them across (see BuildingSystem::reregisterBeltTile).
|
||||||
|
void setSplitterFilters(QPoint tile, const std::vector<ItemType>& filterA,
|
||||||
|
const std::vector<ItemType>& filterB);
|
||||||
|
std::optional<SplitterInfo> getSplitterInfo(QPoint tile) const;
|
||||||
|
|
||||||
|
// Port interface (buildings <-> belts)
|
||||||
|
bool tryPutItem(QPoint tile, Item item, Rotation fromDir = Rotation::West);
|
||||||
|
std::optional<Item> tryTakeItem(Port port);
|
||||||
|
std::optional<ItemType> peekItem(Port port) const;
|
||||||
|
double getProgressPerTick_tpt() const; // shared so building output items
|
||||||
|
// travel at belt speed (REQ-MAT-OUTPUT-EMERGE)
|
||||||
|
|
||||||
|
// Maintenance
|
||||||
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
void clearTiles(const std::vector<QPoint>& tiles); // REQ-UI-BELT-CLEAR
|
||||||
void tick();
|
void tick();
|
||||||
|
|
||||||
|
// Rendering
|
||||||
void forEachVisualItem(QRect viewportTiles,
|
void forEachVisualItem(QRect viewportTiles,
|
||||||
std::function<void(VisualItem)> visit) const;
|
std::function<void(VisualItem)> visit) const;
|
||||||
|
|
||||||
|
// Determinism (docs/replay_design.md)
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct VisualItem {
|
struct VisualItem {
|
||||||
@@ -155,12 +181,12 @@ struct VisualItem {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Buildings interact with belts only through port-level push and pull. Rendering reads only through `forEachVisualItem`. No other system ever asks "what is on tile X".
|
Item *transport* is still reached only through push and pull: `tryPutItem` / `tryTakeItem` move items, `peekItem` reveals the leading item's type but never an identity, and rendering reads only through `forEachVisualItem`. The growth is in tile **topology** — placement, removal and splitter filters — which `BuildingSystem` drives because belts are `Building`s for cost, construction and deconstruction. That coupling is real and is not going away.
|
||||||
|
|
||||||
### Implementation Strategy
|
### Implementation Strategy
|
||||||
|
|
||||||
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
- v1: per-tile representation. Each belt tile stores up to 2 items with a progress value in `[0, 1]` along the tile's belt direction. Sufficient for the scale this game targets.
|
||||||
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. Because the public interface never exposes tile-level item identity, migration is internal to the subsystem.
|
- v2 (optional, only if v1 profiles poorly): Factorio-style belt-segment compression. The migration argument still holds for the item representation, since no method exposes tile-level item identity — but a v2 would have to keep the placement and splitter-filter methods working per tile, which is a stronger constraint than this section originally implied.
|
||||||
|
|
||||||
### Rendering Note
|
### Rendering Note
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
set(TARGET_BASE_NAME "${PRODUCT_NAME}")
|
||||||
|
|
||||||
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
set(TARGET_APP_NAME "${TARGET_BASE_NAME}")
|
||||||
|
|||||||
@@ -46,6 +46,8 @@ ArenaSimulation::ArenaSimulation(const GameConfig& gameConfig,
|
|||||||
, m_finished(false)
|
, m_finished(false)
|
||||||
, m_stopRequested(false)
|
, m_stopRequested(false)
|
||||||
{
|
{
|
||||||
|
m_factoryState = makeFactoryState(m_gameConfig);
|
||||||
|
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||||
m_gameConfig,
|
m_gameConfig,
|
||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
@@ -162,7 +164,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
hp, hp, false);
|
hp, hp, false);
|
||||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||||
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
m_admin.addComponent<HqProxyComponent>(m_team1HqEntity);
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
// Team 2 HQ — ECS proxy entity, enemy faction (isEnemy=true). No weapon.
|
||||||
@@ -183,7 +185,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
hp, hp, true);
|
hp, hp, true);
|
||||||
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
// Tag as an HQ so it is excluded from repair targeting (REQ-SHP-REPAIR).
|
||||||
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
m_admin.addComponent<HqProxyComponent>(m_team2HqEntity);
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
auto placeArenaStation = [&](const ArenaStationEntry& entry, bool isEnemy)
|
||||||
@@ -237,7 +239,7 @@ void ArenaSimulation::placeStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{stationEntity});
|
ModuleOwnerComponent{stationEntity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
for (const ArenaStationEntry& entry : m_arenaConfig.teams[0].stations)
|
||||||
@@ -322,13 +324,13 @@ void ArenaSimulation::tick()
|
|||||||
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
// Ship behavior systems (tick step 7): evaluate, select winner, execute.
|
||||||
// Module + combat systems emit their tool beams into a shared buffer.
|
// Module + combat systems emit their tool beams into a shared buffer.
|
||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
|
m_aiSystem->tick(m_admin, m_factoryState);
|
||||||
std::vector<BeamFiredEvent> beamFiredEvents;
|
std::vector<BeamFiredEvent> beamFiredEvents;
|
||||||
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, beamFiredEvents);
|
m_salvagerSystem->tick(m_currentTick, m_factoryState, beamFiredEvents);
|
||||||
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
m_repairSystem->tick(m_currentTick, beamFiredEvents);
|
||||||
|
|
||||||
// Combat resolution (tick step 8).
|
// Combat resolution (tick step 8).
|
||||||
m_combatSystem->tick(m_currentTick, m_admin, *m_buildingSystem, beamFiredEvents);
|
m_combatSystem->tick(m_currentTick, m_admin, beamFiredEvents);
|
||||||
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
m_beamFiredEvents.insert(m_beamFiredEvents.end(), beamFiredEvents.begin(), beamFiredEvents.end());
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
|
|
||||||
@@ -392,7 +394,7 @@ void ArenaSimulation::tickDeaths()
|
|||||||
for (entt::entity deadEntity : deadStations)
|
for (entt::entity deadEntity : deadStations)
|
||||||
{
|
{
|
||||||
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
const StationBodyComponent& sb = m_admin.get<StationBodyComponent>(deadEntity);
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||||
{
|
{
|
||||||
std::vector<entt::entity> stationChildren;
|
std::vector<entt::entity> stationChildren;
|
||||||
m_admin.forEach<ModuleOwnerComponent>(
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
@@ -487,6 +489,11 @@ const ArenaConfig& ArenaSimulation::getArenaConfig() const
|
|||||||
return m_arenaConfig;
|
return m_arenaConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FactoryState& ArenaSimulation::getFactoryState() const
|
||||||
|
{
|
||||||
|
return m_factoryState;
|
||||||
|
}
|
||||||
|
|
||||||
const BuildingSystem& ArenaSimulation::getBuildings() const
|
const BuildingSystem& ArenaSimulation::getBuildings() const
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
#include "BalancingConfig.h"
|
#include "BalancingConfig.h"
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
|
|
||||||
@@ -85,6 +86,7 @@ public:
|
|||||||
|
|
||||||
const ArenaConfig& getArenaConfig() const;
|
const ArenaConfig& getArenaConfig() const;
|
||||||
const BuildingSystem& getBuildings() const;
|
const BuildingSystem& getBuildings() const;
|
||||||
|
const FactoryState& getFactoryState() const;
|
||||||
const ShipSystem& getShips() const;
|
const ShipSystem& getShips() const;
|
||||||
const DebrisSystem& getDebrisSystem() const;
|
const DebrisSystem& getDebrisSystem() const;
|
||||||
EntityAdmin& getAdmin();
|
EntityAdmin& getAdmin();
|
||||||
@@ -107,6 +109,7 @@ private:
|
|||||||
BuildingId m_nextBuildingId;
|
BuildingId m_nextBuildingId;
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
|
FactoryState m_factoryState;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "ArenaView.h"
|
#include "ArenaView.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -306,7 +307,7 @@ void ArenaView::drawTiles(QPainter& painter)
|
|||||||
|
|
||||||
void ArenaView::drawBuildings(QPainter& painter)
|
void ArenaView::drawBuildings(QPainter& painter)
|
||||||
{
|
{
|
||||||
for (const Building& b : m_sim->getBuildings().getAllBuildings())
|
for (const Building& b : getAllBuildings(m_sim->getFactoryState()))
|
||||||
{
|
{
|
||||||
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
const std::map<BuildingType, BuildingVisuals>::const_iterator it =
|
||||||
m_visuals->buildings.find(b.type);
|
m_visuals->buildings.find(b.type);
|
||||||
@@ -339,7 +340,7 @@ void ArenaView::drawBuildings(QPainter& painter)
|
|||||||
void ArenaView::drawDebris(QPainter& painter)
|
void ArenaView::drawDebris(QPainter& painter)
|
||||||
{
|
{
|
||||||
const float r = getTilePx() * 0.2f;
|
const float r = getTilePx() * 0.2f;
|
||||||
for (const DebrisInfo& debris : m_sim->getDebrisSystem().getAllDebrisInfo())
|
for (const DebrisInfo& debris : getAllDebrisInfo(m_sim->getAdmin()))
|
||||||
{
|
{
|
||||||
const QPointF center = worldToWidget(debris.position);
|
const QPointF center = worldToWidget(debris.position);
|
||||||
painter.setBrush(QColor(128, 110, 90));
|
painter.setBrush(QColor(128, 110, 90));
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TomlHelpers.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,9 +21,17 @@ SET(SRCS
|
|||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/Formula.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoader.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderWorld.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderBuildings.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderRecipes.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderShips.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderStations.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderModules.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConfigLoaderUnlocks.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/SurfaceMask.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BlueprintSerializer.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayoutBlueprintSerializer.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/TomlHelpers.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1,779 +1,10 @@
|
|||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
#include <cstdint>
|
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
#include "toml.hpp"
|
|
||||||
|
|
||||||
#include "Rotation.h"
|
|
||||||
#include "ShipLayout.h"
|
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
|
|
||||||
// --- Error helpers --------------------------------------------------------
|
|
||||||
|
|
||||||
std::runtime_error makeError(const std::string& file,
|
|
||||||
const std::string& path,
|
|
||||||
const std::string& why)
|
|
||||||
{
|
|
||||||
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
|
||||||
|
|
||||||
int64_t requireInt(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const std::optional<int64_t> value = node.value<int64_t>();
|
|
||||||
if (!value)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, "missing or not an integer");
|
|
||||||
}
|
|
||||||
return *value;
|
|
||||||
}
|
|
||||||
|
|
||||||
double requireDouble(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
if (const std::optional<double> v = node.value<double>())
|
|
||||||
{
|
|
||||||
return *v;
|
|
||||||
}
|
|
||||||
if (const std::optional<int64_t> v = node.value<int64_t>())
|
|
||||||
{
|
|
||||||
return static_cast<double>(*v);
|
|
||||||
}
|
|
||||||
throw makeError(file, path, "missing or not a number");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string requireString(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const std::optional<std::string> value = node.value<std::string>();
|
|
||||||
if (!value)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, "missing or not a string");
|
|
||||||
}
|
|
||||||
return *value;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool requireBool(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const std::optional<bool> value = node.value<bool>();
|
|
||||||
if (!value)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, "missing or not a boolean");
|
|
||||||
}
|
|
||||||
return *value;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const toml::array* arr = node.as_array();
|
|
||||||
if (arr == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, "missing or not an array");
|
|
||||||
}
|
|
||||||
return *arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const toml::table* tbl = node.as_table();
|
|
||||||
if (tbl == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, "missing or not a table");
|
|
||||||
}
|
|
||||||
return *tbl;
|
|
||||||
}
|
|
||||||
|
|
||||||
Formula requireFormula(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string source = requireString(node, file, path);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Formula::compile(source);
|
|
||||||
}
|
|
||||||
catch (const std::exception& e)
|
|
||||||
{
|
|
||||||
throw makeError(file, path, std::string("formula error: ") + e.what());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
const toml::array& arr = requireArray(node, file, path);
|
|
||||||
std::vector<std::string> result;
|
|
||||||
result.reserve(arr.size());
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
|
||||||
const std::optional<std::string> s = arr[i].value<std::string>();
|
|
||||||
if (!s)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a string");
|
|
||||||
}
|
|
||||||
result.push_back(*s);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
std::vector<RecipeIngredient> result;
|
|
||||||
result.reserve(arr.size());
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* t = arr[i].as_table();
|
|
||||||
if (t == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
|
|
||||||
// We need a mutable node_view to reuse our helpers, which is fine
|
|
||||||
// because the helpers never mutate.
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*t);
|
|
||||||
|
|
||||||
RecipeIngredient ing;
|
|
||||||
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
|
||||||
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
|
||||||
result.push_back(std::move(ing));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
std::vector<RecipeOutput> result;
|
|
||||||
result.reserve(arr.size());
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* t = arr[i].as_table();
|
|
||||||
if (t == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*t);
|
|
||||||
|
|
||||||
RecipeOutput out;
|
|
||||||
out.item = requireString(mt["item"], file, elemPath + ".item");
|
|
||||||
out.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
|
||||||
if (const std::optional<double> p = mt["probability"].value<double>())
|
|
||||||
{
|
|
||||||
out.probability = *p;
|
|
||||||
}
|
|
||||||
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
|
||||||
{
|
|
||||||
out.probability = static_cast<double>(*p);
|
|
||||||
}
|
|
||||||
result.push_back(std::move(out));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
toml::table parseFile(const std::string& path, const std::string& file)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return toml::parse_file(path);
|
|
||||||
}
|
|
||||||
catch (const toml::parse_error& e)
|
|
||||||
{
|
|
||||||
std::ostringstream oss;
|
|
||||||
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
|
||||||
<< " at " << e.source().begin;
|
|
||||||
throw std::runtime_error(oss.str());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rotation parseRotationString(const std::string& s)
|
|
||||||
{
|
|
||||||
if (s == "east") { return Rotation::East; }
|
|
||||||
if (s == "south") { return Rotation::South; }
|
|
||||||
if (s == "west") { return Rotation::West; }
|
|
||||||
return Rotation::North;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
|
||||||
const std::string& file,
|
|
||||||
const std::string& path)
|
|
||||||
{
|
|
||||||
std::vector<PlacedModule> result;
|
|
||||||
result.reserve(arr.size());
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* t = arr[i].as_table();
|
|
||||||
if (t == nullptr) { continue; }
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*t);
|
|
||||||
|
|
||||||
const std::optional<std::string> type = mt["type"].value<std::string>();
|
|
||||||
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
|
||||||
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
|
||||||
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
|
||||||
if (!type || !x || !y || !rot) { continue; }
|
|
||||||
|
|
||||||
PlacedModule pm;
|
|
||||||
pm.moduleId = *type;
|
|
||||||
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
|
||||||
pm.rotation = parseRotationString(*rot);
|
|
||||||
result.push_back(std::move(pm));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
|
|
||||||
// --- Per-file loaders -----------------------------------------------------
|
|
||||||
|
|
||||||
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "world.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
WorldConfig cfg;
|
|
||||||
|
|
||||||
cfg.heightTiles = static_cast<int>(requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
|
||||||
cfg.refundPercentage = static_cast<int>(requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
|
||||||
cfg.deconstructionTimeSeconds = requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
|
|
||||||
cfg.startingBuildingBlocks = static_cast<int>(requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
|
||||||
cfg.debrisDespawnSeconds = requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
|
|
||||||
cfg.scrapPerThreat = requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
|
||||||
cfg.tileSize_m = requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
|
|
||||||
cfg.beltSpeed_tps = requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
|
||||||
cfg.tunnelMaxDistance_tiles = static_cast<int>(requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
|
||||||
cfg.departureIntervalSeconds = requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
|
||||||
cfg.orbitFactor = requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
|
||||||
cfg.rallyOrbitRadius_tiles = requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
|
||||||
|
|
||||||
if (const std::optional<std::string> tip =
|
|
||||||
tbl["world"]["building_blocks_tooltip"].value<std::string>())
|
|
||||||
{
|
|
||||||
cfg.buildingBlocksTooltip = *tip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const std::optional<std::string> tip =
|
|
||||||
tbl["world"]["artifact_tooltip"].value<std::string>())
|
|
||||||
{
|
|
||||||
cfg.artifactTooltip = *tip;
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.regions.asteroidWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
|
||||||
cfg.regions.playerBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
|
||||||
cfg.regions.contestZoneWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
|
||||||
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
|
||||||
|
|
||||||
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
|
||||||
cfg.expansion.costBuildingBlocksFormula = requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
|
|
||||||
|
|
||||||
cfg.push.pushExpandColumns_tiles = static_cast<int>(requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
|
||||||
cfg.push.bossAdvanceSeconds = requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
|
||||||
|
|
||||||
cfg.waves.threatRateFormula = requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
|
||||||
cfg.waves.gapMinSeconds = requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
|
||||||
cfg.waves.gapMaxSeconds = requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
|
||||||
cfg.waves.spawnDurationSeconds = requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
|
||||||
cfg.waves.bossCountdownSeconds = requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
|
|
||||||
cfg.waves.bossThreatDurationSeconds = requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
|
|
||||||
cfg.waves.bossQuietBeforeSeconds = requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
|
|
||||||
cfg.waves.bossQuietAfterSeconds = requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
|
|
||||||
|
|
||||||
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
|
||||||
{
|
|
||||||
throw makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.targeting.targetScoreFormula = requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
|
|
||||||
cfg.targeting.overclaimPenaltyFormula = requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
|
||||||
cfg.targeting.hysteresis = requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
|
||||||
|
|
||||||
cfg.artifacts.artifactChanceFormula = requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
|
||||||
cfg.artifacts.artifactWinCount = static_cast<int>(requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
|
|
||||||
|
|
||||||
cfg.scroll.panSpeedSlow_tps = requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second");
|
|
||||||
cfg.scroll.panSpeedFast_tps = requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second");
|
|
||||||
cfg.scroll.panRampBandWidth_tiles = static_cast<int>(requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles"));
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "buildings.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
BuildingsConfig cfg;
|
|
||||||
const toml::array& arr = requireArray(tbl["building"], file, "building");
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* bt = arr[i].as_table();
|
|
||||||
if (bt == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*bt);
|
|
||||||
|
|
||||||
BuildingDef def;
|
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
||||||
def.cost = static_cast<int>(requireInt(mt["cost"], file, elemPath + ".cost"));
|
|
||||||
def.playerPlaceable = requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
|
||||||
def.constructionTimeSeconds = requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
|
||||||
|
|
||||||
if (mt.contains("output_buffer_capacity"))
|
|
||||||
{
|
|
||||||
def.outputBufferCapacity = static_cast<int>(
|
|
||||||
requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mt.contains("tooltip"))
|
|
||||||
{
|
|
||||||
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
|
||||||
if (!parsedType)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
|
||||||
}
|
|
||||||
def.type = *parsedType;
|
|
||||||
|
|
||||||
cfg.buildings.push_back(std::move(def));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "recipes.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
RecipesConfig cfg;
|
|
||||||
const toml::array& arr = requireArray(tbl["recipe"], file, "recipe");
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = "recipe[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* rt = arr[i].as_table();
|
|
||||||
if (rt == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*rt);
|
|
||||||
|
|
||||||
RecipeDef def;
|
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
||||||
def.durationSeconds = requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
|
|
||||||
|
|
||||||
const std::string buildingId = requireString(mt["building"], file, elemPath + ".building");
|
|
||||||
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
|
|
||||||
if (!parsedType)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath + ".building",
|
|
||||||
"unknown building id '" + buildingId + "'");
|
|
||||||
}
|
|
||||||
def.building = *parsedType;
|
|
||||||
|
|
||||||
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
|
|
||||||
{
|
|
||||||
def.unlockedAtStart = requireBool(mt["unlocked_at_start"], file,
|
|
||||||
elemPath + ".unlocked_at_start");
|
|
||||||
}
|
|
||||||
|
|
||||||
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
|
||||||
if (mt.contains("inputs"))
|
|
||||||
{
|
|
||||||
const toml::array& inputs = requireArray(mt["inputs"], file, elemPath + ".inputs");
|
|
||||||
def.inputs = parseIngredients(inputs, file, elemPath + ".inputs");
|
|
||||||
}
|
|
||||||
|
|
||||||
const toml::array& outputs = requireArray(mt["outputs"], file, elemPath + ".outputs");
|
|
||||||
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
|
||||||
|
|
||||||
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
|
|
||||||
// in the UI when unset. Not validated against known items here — a missing
|
|
||||||
// icon is not an error (REQ-UI-ITEM-ICON).
|
|
||||||
if (mt.contains("icon"))
|
|
||||||
{
|
|
||||||
def.icon = requireString(mt["icon"], file, elemPath + ".icon");
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.recipes.push_back(std::move(def));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "ships.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
ShipsConfig cfg;
|
|
||||||
const toml::array& arr = requireArray(tbl["ship"], file, "ship");
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* st = arr[i].as_table();
|
|
||||||
if (st == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*st);
|
|
||||||
|
|
||||||
ShipDef def;
|
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
||||||
def.layout = requireStringArray(mt["layout"], file, elemPath + ".layout");
|
|
||||||
|
|
||||||
// Schematic
|
|
||||||
{
|
|
||||||
const std::string bpPath = elemPath + ".schematic";
|
|
||||||
const toml::table& bpTable = requireTable(mt["schematic"], file, bpPath);
|
|
||||||
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
|
||||||
|
|
||||||
const toml::array& materials = requireArray(bpMt["materials"], file, bpPath + ".materials");
|
|
||||||
def.schematic.materials = parseIngredients(materials, file, bpPath + ".materials");
|
|
||||||
def.schematic.productionTimeSeconds = requireDouble(
|
|
||||||
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Health
|
|
||||||
{
|
|
||||||
const std::string hPath = elemPath + ".health";
|
|
||||||
const toml::table& hTable = requireTable(mt["health"], file, hPath);
|
|
||||||
toml::table& hMt = const_cast<toml::table&>(hTable);
|
|
||||||
def.health.hp = static_cast<float>(requireDouble(hMt["hp"], file, hPath + ".hp"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Movement
|
|
||||||
{
|
|
||||||
const std::string mPath = elemPath + ".movement";
|
|
||||||
const toml::table& mTable = requireTable(mt["movement"], file, mPath);
|
|
||||||
toml::table& mMt = const_cast<toml::table&>(mTable);
|
|
||||||
def.movement.speed_mps = static_cast<float>(requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
|
||||||
def.movement.mainAcceleration_mpss = static_cast<float>(requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
|
||||||
def.movement.maneuveringAcceleration_mpss = static_cast<float>(requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
|
||||||
def.movement.angularAcceleration_radpss = static_cast<float>(requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
|
||||||
def.movement.maxRotationSpeed_radps = static_cast<float>(requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sensor
|
|
||||||
{
|
|
||||||
const std::string snsPath = elemPath + ".sensor";
|
|
||||||
const toml::table& snsTable = requireTable(mt["sensor"], file, snsPath);
|
|
||||||
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
|
||||||
def.sensor.sensorRange_m = static_cast<float>(requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
|
||||||
if (mt.contains("default_modules"))
|
|
||||||
{
|
|
||||||
const toml::array& modArr = requireArray(mt["default_modules"], file,
|
|
||||||
elemPath + ".default_modules");
|
|
||||||
def.defaultModules = parsePlacedModules(modArr, file,
|
|
||||||
elemPath + ".default_modules");
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.ships.push_back(std::move(def));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "stations.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
StationsConfig cfg;
|
|
||||||
|
|
||||||
// HQ
|
|
||||||
{
|
|
||||||
const std::string p = "hq";
|
|
||||||
cfg.hq.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
||||||
cfg.hq.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Player station
|
|
||||||
{
|
|
||||||
const std::string p = "player_station";
|
|
||||||
cfg.playerStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
||||||
cfg.playerStation.level = static_cast<int>(requireInt(tbl[p]["level"], file, p + ".level"));
|
|
||||||
cfg.playerStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
||||||
cfg.playerStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
|
||||||
cfg.playerStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
|
||||||
cfg.playerStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
|
||||||
cfg.playerStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enemy station
|
|
||||||
{
|
|
||||||
const std::string p = "enemy_station";
|
|
||||||
cfg.enemyStation.surfaceMask = requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
|
||||||
cfg.enemyStation.hpFormula = requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
|
||||||
cfg.enemyStation.damageFormula = requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
|
||||||
cfg.enemyStation.rangeFormula = requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
|
||||||
cfg.enemyStation.fireRateFormula = requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
|
||||||
cfg.enemyStation.scrapDropFormula = requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Known category→stat mappings for module stat modifier discovery.
|
|
||||||
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
|
|
||||||
// Multiplicative modifier keys are always dimensionless and carry no suffix.
|
|
||||||
struct StatEntry
|
|
||||||
{
|
|
||||||
const char* category;
|
|
||||||
const char* stat;
|
|
||||||
const char* addedKeySuffix;
|
|
||||||
};
|
|
||||||
|
|
||||||
static const StatEntry kKnownStats[] = {
|
|
||||||
{"health", "hp", ""},
|
|
||||||
{"movement", "speed", "_mps"},
|
|
||||||
{"movement", "main_acceleration", "_mpss"},
|
|
||||||
{"movement", "maneuvering_acceleration", "_mpss"},
|
|
||||||
{"sensor", "sensor_range", "_m"},
|
|
||||||
{"weapon", "damage", ""},
|
|
||||||
{"weapon", "attack_range", "_m"},
|
|
||||||
{"weapon", "attack_rate", "_hz"},
|
|
||||||
{"salvage", "collection_range", "_m"},
|
|
||||||
{"salvage", "collection_rate", "_hz"},
|
|
||||||
{"cargo", "cargo_capacity", ""},
|
|
||||||
{"repair", "repair_rate", "_hz"},
|
|
||||||
{"repair", "repair_range", "_m"},
|
|
||||||
};
|
|
||||||
|
|
||||||
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "modules.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
ModulesConfig cfg;
|
|
||||||
|
|
||||||
if (!tbl.contains("module"))
|
|
||||||
{
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toml::array& arr = requireArray(tbl["module"], file, "module");
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = "module[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* st = arr[i].as_table();
|
|
||||||
if (st == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*st);
|
|
||||||
|
|
||||||
ModuleDef def;
|
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
||||||
def.surfaceMask = requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
|
||||||
def.productionTimeSeconds = requireDouble(
|
|
||||||
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
|
||||||
def.fillColor = requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
|
||||||
def.glyph = requireString(mt["glyph"], file, elemPath + ".glyph");
|
|
||||||
|
|
||||||
if (mt.contains("tooltip"))
|
|
||||||
{
|
|
||||||
def.tooltip = requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Materials
|
|
||||||
{
|
|
||||||
const toml::array& materials = requireArray(mt["materials"], file, elemPath + ".materials");
|
|
||||||
def.materials = parseIngredients(materials, file, elemPath + ".materials");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stat modifiers from [module.<category>] sub-tables
|
|
||||||
for (const StatEntry& se : kKnownStats)
|
|
||||||
{
|
|
||||||
if (!mt.contains(se.category))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const toml::table& catTable = requireTable(mt[se.category], file,
|
|
||||||
elemPath + "." + se.category);
|
|
||||||
toml::table& catMt = const_cast<toml::table&>(catTable);
|
|
||||||
|
|
||||||
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
|
||||||
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
|
||||||
|
|
||||||
if (catMt.contains(addedKey))
|
|
||||||
{
|
|
||||||
ModuleStatModifier mod;
|
|
||||||
mod.stat = se.stat;
|
|
||||||
mod.modifierType = "additive";
|
|
||||||
mod.value = requireDouble(catMt[addedKey], file,
|
|
||||||
elemPath + "." + se.category + "." + addedKey);
|
|
||||||
def.statModifiers.push_back(std::move(mod));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (catMt.contains(multipliedKey))
|
|
||||||
{
|
|
||||||
ModuleStatModifier mod;
|
|
||||||
mod.stat = se.stat;
|
|
||||||
mod.modifierType = "multiplicative";
|
|
||||||
mod.value = requireDouble(catMt[multipliedKey], file,
|
|
||||||
elemPath + "." + se.category + "." + multipliedKey);
|
|
||||||
def.statModifiers.push_back(std::move(mod));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Weapon capability section: [module.weapon] with base stat formulas
|
|
||||||
if (mt.contains("weapon"))
|
|
||||||
{
|
|
||||||
const std::string wPath = elemPath + ".weapon";
|
|
||||||
const toml::table& wTable = requireTable(mt["weapon"], file, wPath);
|
|
||||||
toml::table& wMt = const_cast<toml::table&>(wTable);
|
|
||||||
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
|
||||||
|| wMt.contains("attack_rate_hz"))
|
|
||||||
{
|
|
||||||
ModuleWeaponCapability cap;
|
|
||||||
cap.damage = static_cast<float>(requireDouble(wMt["damage"],
|
|
||||||
file, wPath + ".damage"));
|
|
||||||
cap.attackRange_m = static_cast<float>(requireDouble(wMt["attack_range_m"],
|
|
||||||
file, wPath + ".attack_range_m"));
|
|
||||||
cap.attackRate_hz = static_cast<float>(requireDouble(wMt["attack_rate_hz"],
|
|
||||||
file, wPath + ".attack_rate_hz"));
|
|
||||||
def.weaponCapability = std::move(cap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Salvage capability section: [module.salvage] with base stat formulas
|
|
||||||
if (mt.contains("salvage"))
|
|
||||||
{
|
|
||||||
const std::string sPath = elemPath + ".salvage";
|
|
||||||
const toml::table& sTable = requireTable(mt["salvage"], file, sPath);
|
|
||||||
toml::table& sMt = const_cast<toml::table&>(sTable);
|
|
||||||
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
|
||||||
|| sMt.contains("collection_rate_hz"))
|
|
||||||
{
|
|
||||||
ModuleSalvageCapability cap;
|
|
||||||
cap.collectionRange_m = static_cast<float>(requireDouble(sMt["collection_range_m"],
|
|
||||||
file, sPath + ".collection_range_m"));
|
|
||||||
cap.cargoCapacity = static_cast<float>(requireDouble(sMt["cargo_capacity"],
|
|
||||||
file, sPath + ".cargo_capacity"));
|
|
||||||
cap.collectionRate_hz = static_cast<float>(requireDouble(sMt["collection_rate_hz"],
|
|
||||||
file, sPath + ".collection_rate_hz"));
|
|
||||||
def.salvageCapability = std::move(cap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Repair capability section: [module.repair] with base stat formulas
|
|
||||||
if (mt.contains("repair"))
|
|
||||||
{
|
|
||||||
const std::string rPath = elemPath + ".repair";
|
|
||||||
const toml::table& rTable = requireTable(mt["repair"], file, rPath);
|
|
||||||
toml::table& rMt = const_cast<toml::table&>(rTable);
|
|
||||||
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
|
||||||
{
|
|
||||||
ModuleRepairCapability cap;
|
|
||||||
cap.repairRate_hz = static_cast<float>(requireDouble(rMt["repair_rate_hz"],
|
|
||||||
file, rPath + ".repair_rate_hz"));
|
|
||||||
cap.repairAmountHp = static_cast<float>(requireDouble(rMt["repair_amount_hp"],
|
|
||||||
file, rPath + ".repair_amount_hp"));
|
|
||||||
cap.repairRange_m = static_cast<float>(requireDouble(rMt["repair_range_m"],
|
|
||||||
file, rPath + ".repair_range_m"));
|
|
||||||
def.repairCapability = std::move(cap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.modules.push_back(std::move(def));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
|
|
||||||
{
|
|
||||||
const std::string file = "unlocks.toml";
|
|
||||||
toml::table tbl = parseFile(path, file);
|
|
||||||
|
|
||||||
UnlocksConfig cfg;
|
|
||||||
if (!tbl.contains("unlock"))
|
|
||||||
{
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toml::array& arr = requireArray(tbl["unlock"], file, "unlock");
|
|
||||||
|
|
||||||
for (std::size_t i = 0; i < arr.size(); ++i)
|
|
||||||
{
|
|
||||||
const std::string elemPath = "unlock[" + std::to_string(i) + "]";
|
|
||||||
const toml::table* ut = arr[i].as_table();
|
|
||||||
if (ut == nullptr)
|
|
||||||
{
|
|
||||||
throw makeError(file, elemPath, "not a table");
|
|
||||||
}
|
|
||||||
toml::table& mt = const_cast<toml::table&>(*ut);
|
|
||||||
|
|
||||||
UnlockGroupDef def;
|
|
||||||
def.id = requireString(mt["id"], file, elemPath + ".id");
|
|
||||||
def.stationLevel = static_cast<int>(
|
|
||||||
requireInt(mt["station_level"], file, elemPath + ".station_level"));
|
|
||||||
if (mt.contains("requires"))
|
|
||||||
{
|
|
||||||
def.requiredGroupIds = requireStringArray(mt["requires"], file, elemPath + ".requires");
|
|
||||||
}
|
|
||||||
if (mt.contains("ships"))
|
|
||||||
{
|
|
||||||
def.ships = requireStringArray(mt["ships"], file, elemPath + ".ships");
|
|
||||||
}
|
|
||||||
if (mt.contains("modules"))
|
|
||||||
{
|
|
||||||
def.modules = requireStringArray(mt["modules"], file, elemPath + ".modules");
|
|
||||||
}
|
|
||||||
if (mt.contains("buildings"))
|
|
||||||
{
|
|
||||||
def.buildings = requireStringArray(mt["buildings"], file, elemPath + ".buildings");
|
|
||||||
}
|
|
||||||
if (mt.contains("recipes"))
|
|
||||||
{
|
|
||||||
def.recipes = requireStringArray(mt["recipes"], file, elemPath + ".recipes");
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.groups.push_back(std::move(def));
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -815,11 +46,11 @@ void validateUnlocks(const GameConfig& cfg)
|
|||||||
{
|
{
|
||||||
if (valid.count(id) == 0)
|
if (valid.count(id) == 0)
|
||||||
{
|
{
|
||||||
throw makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
|
throw utility::makeError(file, gPath, "grants unknown " + kind + " '" + id + "'");
|
||||||
}
|
}
|
||||||
if (!granted.insert(id).second)
|
if (!granted.insert(id).second)
|
||||||
{
|
{
|
||||||
throw makeError(file, gPath,
|
throw utility::makeError(file, gPath,
|
||||||
"grants " + kind + " '" + id + "' which is already granted by another unlock group");
|
"grants " + kind + " '" + id + "' which is already granted by another unlock group");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -830,13 +61,13 @@ void validateUnlocks(const GameConfig& cfg)
|
|||||||
const std::string gPath = "unlock '" + group.id + "'";
|
const std::string gPath = "unlock '" + group.id + "'";
|
||||||
if (!groupIds.insert(group.id).second)
|
if (!groupIds.insert(group.id).second)
|
||||||
{
|
{
|
||||||
throw makeError(file, gPath, "duplicate unlock group id");
|
throw utility::makeError(file, gPath, "duplicate unlock group id");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.ships.empty() && group.modules.empty()
|
if (group.ships.empty() && group.modules.empty()
|
||||||
&& group.buildings.empty() && group.recipes.empty())
|
&& group.buildings.empty() && group.recipes.empty())
|
||||||
{
|
{
|
||||||
throw makeError(file, gPath, "grants no items (must grant at least one)");
|
throw utility::makeError(file, gPath, "grants no items (must grant at least one)");
|
||||||
}
|
}
|
||||||
|
|
||||||
checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath);
|
checkGrants(group.ships, shipIds, grantedShipIds, "ship", gPath);
|
||||||
@@ -852,7 +83,7 @@ void validateUnlocks(const GameConfig& cfg)
|
|||||||
{
|
{
|
||||||
if (groupIds.count(req) == 0)
|
if (groupIds.count(req) == 0)
|
||||||
{
|
{
|
||||||
throw makeError(file, "unlock '" + group.id + "'.requires",
|
throw utility::makeError(file, "unlock '" + group.id + "'.requires",
|
||||||
"references unknown unlock group '" + req + "'");
|
"references unknown unlock group '" + req + "'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
58
src/lib/config/ConfigLoaderBuildings.cpp
Normal file
58
src/lib/config/ConfigLoaderBuildings.cpp
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
BuildingsConfig ConfigLoader::loadBuildings(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "buildings.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
BuildingsConfig cfg;
|
||||||
|
const toml::array& arr = utility::requireArray(tbl["building"], file, "building");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "building[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* bt = arr[i].as_table();
|
||||||
|
if (bt == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*bt);
|
||||||
|
|
||||||
|
BuildingDef def;
|
||||||
|
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.cost = static_cast<int>(utility::requireInt(mt["cost"], file, elemPath + ".cost"));
|
||||||
|
def.playerPlaceable = utility::requireBool(mt["player_placeable"], file, elemPath + ".player_placeable");
|
||||||
|
def.constructionTimeSeconds = utility::requireDouble(mt["construction_time_seconds"], file, elemPath + ".construction_time_seconds");
|
||||||
|
def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||||
|
|
||||||
|
if (mt.contains("output_buffer_capacity"))
|
||||||
|
{
|
||||||
|
def.outputBufferCapacity = static_cast<int>(
|
||||||
|
utility::requireInt(mt["output_buffer_capacity"], file, elemPath + ".output_buffer_capacity"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mt.contains("tooltip"))
|
||||||
|
{
|
||||||
|
def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::optional<BuildingType> parsedType = parseBuildingType(def.id);
|
||||||
|
if (!parsedType)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath + ".id", "unknown building id '" + def.id + "'");
|
||||||
|
}
|
||||||
|
def.type = *parsedType;
|
||||||
|
|
||||||
|
cfg.buildings.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
182
src/lib/config/ConfigLoaderModules.cpp
Normal file
182
src/lib/config/ConfigLoaderModules.cpp
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
// Known category→stat mappings for module stat modifier discovery.
|
||||||
|
// addedKeySuffix: unit suffix appended before "_formula" for additive modifier keys only.
|
||||||
|
// Multiplicative modifier keys are always dimensionless and carry no suffix.
|
||||||
|
struct StatEntry
|
||||||
|
{
|
||||||
|
const char* category;
|
||||||
|
const char* stat;
|
||||||
|
const char* addedKeySuffix;
|
||||||
|
};
|
||||||
|
|
||||||
|
static const StatEntry kKnownStats[] = {
|
||||||
|
{"health", "hp", ""},
|
||||||
|
{"movement", "speed", "_mps"},
|
||||||
|
{"movement", "main_acceleration", "_mpss"},
|
||||||
|
{"movement", "maneuvering_acceleration", "_mpss"},
|
||||||
|
{"sensor", "sensor_range", "_m"},
|
||||||
|
{"weapon", "damage", ""},
|
||||||
|
{"weapon", "attack_range", "_m"},
|
||||||
|
{"weapon", "attack_rate", "_hz"},
|
||||||
|
{"salvage", "collection_range", "_m"},
|
||||||
|
{"salvage", "collection_rate", "_hz"},
|
||||||
|
{"cargo", "cargo_capacity", ""},
|
||||||
|
{"repair", "repair_rate", "_hz"},
|
||||||
|
{"repair", "repair_range", "_m"},
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ModulesConfig ConfigLoader::loadModules(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "modules.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
ModulesConfig cfg;
|
||||||
|
|
||||||
|
if (!tbl.contains("module"))
|
||||||
|
{
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array& arr = utility::requireArray(tbl["module"], file, "module");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "module[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* st = arr[i].as_table();
|
||||||
|
if (st == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*st);
|
||||||
|
|
||||||
|
ModuleDef def;
|
||||||
|
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.surfaceMask = utility::requireStringArray(mt["surface_mask"], file, elemPath + ".surface_mask");
|
||||||
|
def.productionTimeSeconds = utility::requireDouble(
|
||||||
|
mt["production_time_seconds"], file, elemPath + ".production_time_seconds");
|
||||||
|
def.fillColor = utility::requireString(mt["fill_color"], file, elemPath + ".fill_color");
|
||||||
|
def.glyph = utility::requireString(mt["glyph"], file, elemPath + ".glyph");
|
||||||
|
|
||||||
|
if (mt.contains("tooltip"))
|
||||||
|
{
|
||||||
|
def.tooltip = utility::requireString(mt["tooltip"], file, elemPath + ".tooltip");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Materials
|
||||||
|
{
|
||||||
|
const toml::array& materials = utility::requireArray(mt["materials"], file, elemPath + ".materials");
|
||||||
|
def.materials = utility::parseIngredients(materials, file, elemPath + ".materials");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat modifiers from [module.<category>] sub-tables
|
||||||
|
for (const StatEntry& se : kKnownStats)
|
||||||
|
{
|
||||||
|
if (!mt.contains(se.category))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const toml::table& catTable = utility::requireTable(mt[se.category], file,
|
||||||
|
elemPath + "." + se.category);
|
||||||
|
toml::table& catMt = const_cast<toml::table&>(catTable);
|
||||||
|
|
||||||
|
const std::string addedKey = std::string("added_") + se.stat + se.addedKeySuffix;
|
||||||
|
const std::string multipliedKey = std::string("multiplied_") + se.stat + se.addedKeySuffix;
|
||||||
|
|
||||||
|
if (catMt.contains(addedKey))
|
||||||
|
{
|
||||||
|
ModuleStatModifier mod;
|
||||||
|
mod.stat = se.stat;
|
||||||
|
mod.modifierType = "additive";
|
||||||
|
mod.value = utility::requireDouble(catMt[addedKey], file,
|
||||||
|
elemPath + "." + se.category + "." + addedKey);
|
||||||
|
def.statModifiers.push_back(std::move(mod));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catMt.contains(multipliedKey))
|
||||||
|
{
|
||||||
|
ModuleStatModifier mod;
|
||||||
|
mod.stat = se.stat;
|
||||||
|
mod.modifierType = "multiplicative";
|
||||||
|
mod.value = utility::requireDouble(catMt[multipliedKey], file,
|
||||||
|
elemPath + "." + se.category + "." + multipliedKey);
|
||||||
|
def.statModifiers.push_back(std::move(mod));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weapon capability section: [module.weapon] with base stat formulas
|
||||||
|
if (mt.contains("weapon"))
|
||||||
|
{
|
||||||
|
const std::string wPath = elemPath + ".weapon";
|
||||||
|
const toml::table& wTable = utility::requireTable(mt["weapon"], file, wPath);
|
||||||
|
toml::table& wMt = const_cast<toml::table&>(wTable);
|
||||||
|
if (wMt.contains("damage") || wMt.contains("attack_range_m")
|
||||||
|
|| wMt.contains("attack_rate_hz"))
|
||||||
|
{
|
||||||
|
ModuleWeaponCapability cap;
|
||||||
|
cap.damage = static_cast<float>(utility::requireDouble(wMt["damage"],
|
||||||
|
file, wPath + ".damage"));
|
||||||
|
cap.attackRange_m = static_cast<float>(utility::requireDouble(wMt["attack_range_m"],
|
||||||
|
file, wPath + ".attack_range_m"));
|
||||||
|
cap.attackRate_hz = static_cast<float>(utility::requireDouble(wMt["attack_rate_hz"],
|
||||||
|
file, wPath + ".attack_rate_hz"));
|
||||||
|
def.weaponCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salvage capability section: [module.salvage] with base stat formulas
|
||||||
|
if (mt.contains("salvage"))
|
||||||
|
{
|
||||||
|
const std::string sPath = elemPath + ".salvage";
|
||||||
|
const toml::table& sTable = utility::requireTable(mt["salvage"], file, sPath);
|
||||||
|
toml::table& sMt = const_cast<toml::table&>(sTable);
|
||||||
|
if (sMt.contains("collection_range_m") || sMt.contains("cargo_capacity")
|
||||||
|
|| sMt.contains("collection_rate_hz"))
|
||||||
|
{
|
||||||
|
ModuleSalvageCapability cap;
|
||||||
|
cap.collectionRange_m = static_cast<float>(utility::requireDouble(sMt["collection_range_m"],
|
||||||
|
file, sPath + ".collection_range_m"));
|
||||||
|
cap.cargoCapacity = static_cast<float>(utility::requireDouble(sMt["cargo_capacity"],
|
||||||
|
file, sPath + ".cargo_capacity"));
|
||||||
|
cap.collectionRate_hz = static_cast<float>(utility::requireDouble(sMt["collection_rate_hz"],
|
||||||
|
file, sPath + ".collection_rate_hz"));
|
||||||
|
def.salvageCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repair capability section: [module.repair] with base stat formulas
|
||||||
|
if (mt.contains("repair"))
|
||||||
|
{
|
||||||
|
const std::string rPath = elemPath + ".repair";
|
||||||
|
const toml::table& rTable = utility::requireTable(mt["repair"], file, rPath);
|
||||||
|
toml::table& rMt = const_cast<toml::table&>(rTable);
|
||||||
|
if (rMt.contains("repair_rate_hz") || rMt.contains("repair_range_m"))
|
||||||
|
{
|
||||||
|
ModuleRepairCapability cap;
|
||||||
|
cap.repairRate_hz = static_cast<float>(utility::requireDouble(rMt["repair_rate_hz"],
|
||||||
|
file, rPath + ".repair_rate_hz"));
|
||||||
|
cap.repairAmountHp = static_cast<float>(utility::requireDouble(rMt["repair_amount_hp"],
|
||||||
|
file, rPath + ".repair_amount_hp"));
|
||||||
|
cap.repairRange_m = static_cast<float>(utility::requireDouble(rMt["repair_range_m"],
|
||||||
|
file, rPath + ".repair_range_m"));
|
||||||
|
def.repairCapability = std::move(cap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.modules.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
109
src/lib/config/ConfigLoaderRecipes.cpp
Normal file
109
src/lib/config/ConfigLoaderRecipes.cpp
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
std::vector<RecipeOutput> parseRecipeOutputs(const toml::array& arr,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<RecipeOutput> result;
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* t = arr[i].as_table();
|
||||||
|
if (t == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*t);
|
||||||
|
|
||||||
|
RecipeOutput out;
|
||||||
|
out.item = utility::requireString(mt["item"], file, elemPath + ".item");
|
||||||
|
out.amount = static_cast<int>(utility::requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||||
|
if (const std::optional<double> p = mt["probability"].value<double>())
|
||||||
|
{
|
||||||
|
out.probability = *p;
|
||||||
|
}
|
||||||
|
else if (const std::optional<int64_t> p = mt["probability"].value<int64_t>())
|
||||||
|
{
|
||||||
|
out.probability = static_cast<double>(*p);
|
||||||
|
}
|
||||||
|
result.push_back(std::move(out));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
RecipesConfig ConfigLoader::loadRecipes(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "recipes.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
RecipesConfig cfg;
|
||||||
|
const toml::array& arr = utility::requireArray(tbl["recipe"], file, "recipe");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "recipe[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* rt = arr[i].as_table();
|
||||||
|
if (rt == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*rt);
|
||||||
|
|
||||||
|
RecipeDef def;
|
||||||
|
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.durationSeconds = utility::requireDouble(mt["duration_seconds"], file, elemPath + ".duration_seconds");
|
||||||
|
|
||||||
|
const std::string buildingId = utility::requireString(mt["building"], file, elemPath + ".building");
|
||||||
|
const std::optional<BuildingType> parsedType = parseBuildingType(buildingId);
|
||||||
|
if (!parsedType)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath + ".building",
|
||||||
|
"unknown building id '" + buildingId + "'");
|
||||||
|
}
|
||||||
|
def.building = *parsedType;
|
||||||
|
|
||||||
|
if (def.building == BuildingType::Assembler && mt.contains("unlocked_at_start"))
|
||||||
|
{
|
||||||
|
def.unlockedAtStart = utility::requireBool(mt["unlocked_at_start"], file,
|
||||||
|
elemPath + ".unlocked_at_start");
|
||||||
|
}
|
||||||
|
|
||||||
|
// inputs may be omitted (e.g. miner recipes). An empty array is fine.
|
||||||
|
if (mt.contains("inputs"))
|
||||||
|
{
|
||||||
|
const toml::array& inputs = utility::requireArray(mt["inputs"], file, elemPath + ".inputs");
|
||||||
|
def.inputs = utility::parseIngredients(inputs, file, elemPath + ".inputs");
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array& outputs = utility::requireArray(mt["outputs"], file, elemPath + ".outputs");
|
||||||
|
def.outputs = parseRecipeOutputs(outputs, file, elemPath + ".outputs");
|
||||||
|
|
||||||
|
// Optional icon item id (REQ-UI-RECIPE-ICON); defaults to the first output
|
||||||
|
// in the UI when unset. Not validated against known items here — a missing
|
||||||
|
// icon is not an error (REQ-UI-ITEM-ICON).
|
||||||
|
if (mt.contains("icon"))
|
||||||
|
{
|
||||||
|
def.icon = utility::requireString(mt["icon"], file, elemPath + ".icon");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.recipes.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
133
src/lib/config/ConfigLoaderShips.cpp
Normal file
133
src/lib/config/ConfigLoaderShips.cpp
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "Rotation.h"
|
||||||
|
#include "ShipLayout.h"
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
|
||||||
|
Rotation parseRotationString(const std::string& s)
|
||||||
|
{
|
||||||
|
if (s == "east") { return Rotation::East; }
|
||||||
|
if (s == "south") { return Rotation::South; }
|
||||||
|
if (s == "west") { return Rotation::West; }
|
||||||
|
return Rotation::North;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<PlacedModule> parsePlacedModules(const toml::array& arr,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<PlacedModule> result;
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* t = arr[i].as_table();
|
||||||
|
if (t == nullptr) { continue; }
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*t);
|
||||||
|
|
||||||
|
const std::optional<std::string> type = mt["type"].value<std::string>();
|
||||||
|
const std::optional<int64_t> x = mt["x"].value<int64_t>();
|
||||||
|
const std::optional<int64_t> y = mt["y"].value<int64_t>();
|
||||||
|
const std::optional<std::string> rot = mt["rotation"].value<std::string>();
|
||||||
|
if (!type || !x || !y || !rot) { continue; }
|
||||||
|
|
||||||
|
PlacedModule pm;
|
||||||
|
pm.moduleId = *type;
|
||||||
|
pm.position = QPoint(static_cast<int>(*x), static_cast<int>(*y));
|
||||||
|
pm.rotation = parseRotationString(*rot);
|
||||||
|
result.push_back(std::move(pm));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ShipsConfig ConfigLoader::loadShips(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "ships.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
ShipsConfig cfg;
|
||||||
|
const toml::array& arr = utility::requireArray(tbl["ship"], file, "ship");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "ship[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* st = arr[i].as_table();
|
||||||
|
if (st == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*st);
|
||||||
|
|
||||||
|
ShipDef def;
|
||||||
|
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.layout = utility::requireStringArray(mt["layout"], file, elemPath + ".layout");
|
||||||
|
|
||||||
|
// Schematic
|
||||||
|
{
|
||||||
|
const std::string bpPath = elemPath + ".schematic";
|
||||||
|
const toml::table& bpTable = utility::requireTable(mt["schematic"], file, bpPath);
|
||||||
|
toml::table& bpMt = const_cast<toml::table&>(bpTable);
|
||||||
|
|
||||||
|
const toml::array& materials = utility::requireArray(bpMt["materials"], file, bpPath + ".materials");
|
||||||
|
def.schematic.materials = utility::parseIngredients(materials, file, bpPath + ".materials");
|
||||||
|
def.schematic.productionTimeSeconds = utility::requireDouble(
|
||||||
|
bpMt["production_time_seconds"], file, bpPath + ".production_time_seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Health
|
||||||
|
{
|
||||||
|
const std::string hPath = elemPath + ".health";
|
||||||
|
const toml::table& hTable = utility::requireTable(mt["health"], file, hPath);
|
||||||
|
toml::table& hMt = const_cast<toml::table&>(hTable);
|
||||||
|
def.health.hp = static_cast<float>(utility::requireDouble(hMt["hp"], file, hPath + ".hp"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Movement
|
||||||
|
{
|
||||||
|
const std::string mPath = elemPath + ".movement";
|
||||||
|
const toml::table& mTable = utility::requireTable(mt["movement"], file, mPath);
|
||||||
|
toml::table& mMt = const_cast<toml::table&>(mTable);
|
||||||
|
def.movement.speed_mps = static_cast<float>(utility::requireDouble(mMt["speed_mps"], file, mPath + ".speed_mps"));
|
||||||
|
def.movement.mainAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["main_acceleration_mpss"], file, mPath + ".main_acceleration_mpss"));
|
||||||
|
def.movement.maneuveringAcceleration_mpss = static_cast<float>(utility::requireDouble(mMt["maneuvering_acceleration_mpss"], file, mPath + ".maneuvering_acceleration_mpss"));
|
||||||
|
def.movement.angularAcceleration_radpss = static_cast<float>(utility::requireDouble(mMt["angular_acceleration_radpss"], file, mPath + ".angular_acceleration_radpss"));
|
||||||
|
def.movement.maxRotationSpeed_radps = static_cast<float>(utility::requireDouble(mMt["max_rotation_speed_radps"], file, mPath + ".max_rotation_speed_radps"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sensor
|
||||||
|
{
|
||||||
|
const std::string snsPath = elemPath + ".sensor";
|
||||||
|
const toml::table& snsTable = utility::requireTable(mt["sensor"], file, snsPath);
|
||||||
|
toml::table& snsMt = const_cast<toml::table&>(snsTable);
|
||||||
|
def.sensor.sensorRange_m = static_cast<float>(utility::requireDouble(snsMt["sensor_range_m"], file, snsPath + ".sensor_range_m"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional: default_modules (REQ-WAV-DEFAULT-MODULES)
|
||||||
|
if (mt.contains("default_modules"))
|
||||||
|
{
|
||||||
|
const toml::array& modArr = utility::requireArray(mt["default_modules"], file,
|
||||||
|
elemPath + ".default_modules");
|
||||||
|
def.defaultModules = parsePlacedModules(modArr, file,
|
||||||
|
elemPath + ".default_modules");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.ships.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
47
src/lib/config/ConfigLoaderStations.cpp
Normal file
47
src/lib/config/ConfigLoaderStations.cpp
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
StationsConfig ConfigLoader::loadStations(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "stations.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
StationsConfig cfg;
|
||||||
|
|
||||||
|
// HQ
|
||||||
|
{
|
||||||
|
const std::string p = "hq";
|
||||||
|
cfg.hq.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||||
|
cfg.hq.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Player station
|
||||||
|
{
|
||||||
|
const std::string p = "player_station";
|
||||||
|
cfg.playerStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||||
|
cfg.playerStation.level = static_cast<int>(utility::requireInt(tbl[p]["level"], file, p + ".level"));
|
||||||
|
cfg.playerStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||||
|
cfg.playerStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||||
|
cfg.playerStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||||
|
cfg.playerStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||||
|
cfg.playerStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enemy station
|
||||||
|
{
|
||||||
|
const std::string p = "enemy_station";
|
||||||
|
cfg.enemyStation.surfaceMask = utility::requireStringArray(tbl[p]["surface_mask"], file, p + ".surface_mask");
|
||||||
|
cfg.enemyStation.hpFormula = utility::requireFormula(tbl[p]["hp_formula"], file, p + ".hp_formula");
|
||||||
|
cfg.enemyStation.damageFormula = utility::requireFormula(tbl[p]["damage_formula"], file, p + ".damage_formula");
|
||||||
|
cfg.enemyStation.rangeFormula = utility::requireFormula(tbl[p]["range_m_formula"], file, p + ".range_m_formula");
|
||||||
|
cfg.enemyStation.fireRateFormula = utility::requireFormula(tbl[p]["fire_rate_hz_formula"], file, p + ".fire_rate_hz_formula");
|
||||||
|
cfg.enemyStation.scrapDropFormula = utility::requireFormula(tbl[p]["scrap_drop_formula"], file, p + ".scrap_drop_formula");
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
62
src/lib/config/ConfigLoaderUnlocks.cpp
Normal file
62
src/lib/config/ConfigLoaderUnlocks.cpp
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
UnlocksConfig ConfigLoader::loadUnlocks(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "unlocks.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
UnlocksConfig cfg;
|
||||||
|
if (!tbl.contains("unlock"))
|
||||||
|
{
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array& arr = utility::requireArray(tbl["unlock"], file, "unlock");
|
||||||
|
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = "unlock[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* ut = arr[i].as_table();
|
||||||
|
if (ut == nullptr)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*ut);
|
||||||
|
|
||||||
|
UnlockGroupDef def;
|
||||||
|
def.id = utility::requireString(mt["id"], file, elemPath + ".id");
|
||||||
|
def.stationLevel = static_cast<int>(
|
||||||
|
utility::requireInt(mt["station_level"], file, elemPath + ".station_level"));
|
||||||
|
if (mt.contains("requires"))
|
||||||
|
{
|
||||||
|
def.requiredGroupIds = utility::requireStringArray(mt["requires"], file, elemPath + ".requires");
|
||||||
|
}
|
||||||
|
if (mt.contains("ships"))
|
||||||
|
{
|
||||||
|
def.ships = utility::requireStringArray(mt["ships"], file, elemPath + ".ships");
|
||||||
|
}
|
||||||
|
if (mt.contains("modules"))
|
||||||
|
{
|
||||||
|
def.modules = utility::requireStringArray(mt["modules"], file, elemPath + ".modules");
|
||||||
|
}
|
||||||
|
if (mt.contains("buildings"))
|
||||||
|
{
|
||||||
|
def.buildings = utility::requireStringArray(mt["buildings"], file, elemPath + ".buildings");
|
||||||
|
}
|
||||||
|
if (mt.contains("recipes"))
|
||||||
|
{
|
||||||
|
def.recipes = utility::requireStringArray(mt["recipes"], file, elemPath + ".recipes");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.groups.push_back(std::move(def));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
79
src/lib/config/ConfigLoaderWorld.cpp
Normal file
79
src/lib/config/ConfigLoaderWorld.cpp
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#include "ConfigLoader.h"
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
WorldConfig ConfigLoader::loadWorld(const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string file = "world.toml";
|
||||||
|
toml::table tbl = utility::parseFile(path, file);
|
||||||
|
|
||||||
|
WorldConfig cfg;
|
||||||
|
|
||||||
|
cfg.heightTiles = static_cast<int>(utility::requireInt(tbl["world"]["height_tiles"], file, "world.height_tiles"));
|
||||||
|
cfg.refundPercentage = static_cast<int>(utility::requireInt(tbl["world"]["refund_percentage"], file, "world.refund_percentage"));
|
||||||
|
cfg.deconstructionTimeSeconds = utility::requireDouble(tbl["world"]["deconstruction_time_seconds"], file, "world.deconstruction_time_seconds");
|
||||||
|
cfg.startingBuildingBlocks = static_cast<int>(utility::requireInt(tbl["world"]["starting_building_blocks"], file, "world.starting_building_blocks"));
|
||||||
|
cfg.debrisDespawnSeconds = utility::requireDouble(tbl["world"]["debris_despawn_seconds"], file, "world.debris_despawn_seconds");
|
||||||
|
cfg.scrapPerThreat = utility::requireDouble(tbl["world"]["scrap_per_threat"], file, "world.scrap_per_threat");
|
||||||
|
cfg.tileSize_m = utility::requireDouble(tbl["world"]["tile_size_m"], file, "world.tile_size_m");
|
||||||
|
cfg.beltSpeed_tps = utility::requireDouble(tbl["world"]["belt_speed_mps"], file, "world.belt_speed_mps") / cfg.tileSize_m;
|
||||||
|
cfg.tunnelMaxDistance_tiles = static_cast<int>(utility::requireInt(tbl["world"]["tunnel_max_distance_tiles"], file, "world.tunnel_max_distance_tiles"));
|
||||||
|
cfg.departureIntervalSeconds = utility::requireDouble(tbl["world"]["departure_interval_seconds"], file, "world.departure_interval_seconds");
|
||||||
|
cfg.orbitFactor = utility::requireDouble(tbl["world"]["orbit_factor"], file, "world.orbit_factor");
|
||||||
|
cfg.rallyOrbitRadius_tiles = utility::requireDouble(tbl["world"]["rally_orbit_radius_tiles"], file, "world.rally_orbit_radius_tiles");
|
||||||
|
|
||||||
|
if (const std::optional<std::string> tip =
|
||||||
|
tbl["world"]["building_blocks_tooltip"].value<std::string>())
|
||||||
|
{
|
||||||
|
cfg.buildingBlocksTooltip = *tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const std::optional<std::string> tip =
|
||||||
|
tbl["world"]["artifact_tooltip"].value<std::string>())
|
||||||
|
{
|
||||||
|
cfg.artifactTooltip = *tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.regions.asteroidWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["asteroid_width_tiles"], file, "regions.asteroid_width_tiles"));
|
||||||
|
cfg.regions.playerBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["player_buffer_width_tiles"], file, "regions.player_buffer_width_tiles"));
|
||||||
|
cfg.regions.contestZoneWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["contest_zone_width_tiles"], file, "regions.contest_zone_width_tiles"));
|
||||||
|
cfg.regions.enemyBufferWidth_tiles = static_cast<int>(utility::requireInt(tbl["regions"]["enemy_buffer_width_tiles"], file, "regions.enemy_buffer_width_tiles"));
|
||||||
|
|
||||||
|
cfg.expansion.columnsPerExpansion_tiles = static_cast<int>(utility::requireInt(tbl["expansion"]["columns_per_expansion_tiles"], file, "expansion.columns_per_expansion_tiles"));
|
||||||
|
cfg.expansion.costBuildingBlocksFormula = utility::requireFormula(tbl["expansion"]["cost_building_blocks_formula"], file, "expansion.cost_building_blocks_formula");
|
||||||
|
|
||||||
|
cfg.push.pushExpandColumns_tiles = static_cast<int>(utility::requireInt(tbl["push"]["push_expand_columns_tiles"], file, "push.push_expand_columns_tiles"));
|
||||||
|
cfg.push.bossAdvanceSeconds = utility::requireDouble(tbl["push"]["boss_advance_seconds"], file, "push.boss_advance_seconds");
|
||||||
|
|
||||||
|
cfg.waves.threatRateFormula = utility::requireFormula(tbl["waves"]["threat_rate_formula"], file, "waves.threat_rate_formula");
|
||||||
|
cfg.waves.gapMinSeconds = utility::requireDouble(tbl["waves"]["gap_min_seconds"], file, "waves.gap_min_seconds");
|
||||||
|
cfg.waves.gapMaxSeconds = utility::requireDouble(tbl["waves"]["gap_max_seconds"], file, "waves.gap_max_seconds");
|
||||||
|
cfg.waves.spawnDurationSeconds = utility::requireDouble(tbl["waves"]["spawn_duration_seconds"], file, "waves.spawn_duration_seconds");
|
||||||
|
cfg.waves.bossCountdownSeconds = utility::requireDouble(tbl["waves"]["boss_countdown_seconds"], file, "waves.boss_countdown_seconds");
|
||||||
|
cfg.waves.bossThreatDurationSeconds = utility::requireDouble(tbl["waves"]["boss_threat_duration_seconds"], file, "waves.boss_threat_duration_seconds");
|
||||||
|
cfg.waves.bossQuietBeforeSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_before_seconds"], file, "waves.boss_quiet_before_seconds");
|
||||||
|
cfg.waves.bossQuietAfterSeconds = utility::requireDouble(tbl["waves"]["boss_quiet_after_seconds"], file, "waves.boss_quiet_after_seconds");
|
||||||
|
|
||||||
|
if (cfg.waves.gapMinSeconds > cfg.waves.gapMaxSeconds)
|
||||||
|
{
|
||||||
|
throw utility::makeError(file, "waves", "gap_min_seconds > gap_max_seconds");
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.targeting.targetScoreFormula = utility::requireFormula(tbl["targeting"]["target_score_formula"], file, "targeting.target_score_formula");
|
||||||
|
cfg.targeting.overclaimPenaltyFormula = utility::requireFormula(tbl["targeting"]["overclaim_penalty_formula"], file, "targeting.overclaim_penalty_formula");
|
||||||
|
cfg.targeting.hysteresis = utility::requireDouble(tbl["targeting"]["target_hysteresis"], file, "targeting.target_hysteresis");
|
||||||
|
|
||||||
|
cfg.artifacts.artifactChanceFormula = utility::requireFormula(tbl["artifacts"]["artifact_chance_formula"], file, "artifacts.artifact_chance_formula");
|
||||||
|
cfg.artifacts.artifactWinCount = static_cast<int>(utility::requireInt(tbl["artifacts"]["artifact_win_count"], file, "artifacts.artifact_win_count"));
|
||||||
|
|
||||||
|
cfg.scroll.panSpeedSlow_tps = utility::requireDouble(tbl["scroll"]["pan_speed_slow_tiles_per_second"], file, "scroll.pan_speed_slow_tiles_per_second");
|
||||||
|
cfg.scroll.panSpeedFast_tps = utility::requireDouble(tbl["scroll"]["pan_speed_fast_tiles_per_second"], file, "scroll.pan_speed_fast_tiles_per_second");
|
||||||
|
cfg.scroll.panRampBandWidth_tiles = static_cast<int>(utility::requireInt(tbl["scroll"]["pan_ramp_band_width_tiles"], file, "scroll.pan_ramp_band_width_tiles"));
|
||||||
|
|
||||||
|
return cfg;
|
||||||
|
}
|
||||||
@@ -59,4 +59,18 @@ struct ModuleDef
|
|||||||
struct ModulesConfig
|
struct ModulesConfig
|
||||||
{
|
{
|
||||||
std::vector<ModuleDef> modules;
|
std::vector<ModuleDef> modules;
|
||||||
|
|
||||||
|
// Returns the definition for the given module id, or nullptr if the id has
|
||||||
|
// no entry in modules.toml.
|
||||||
|
const ModuleDef* findModuleDef(const std::string& id) const
|
||||||
|
{
|
||||||
|
for (const ModuleDef& def : modules)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -47,4 +47,32 @@ struct RecipeDef
|
|||||||
struct RecipesConfig
|
struct RecipesConfig
|
||||||
{
|
{
|
||||||
std::vector<RecipeDef> recipes;
|
std::vector<RecipeDef> recipes;
|
||||||
|
|
||||||
|
// Returns the definition for the given recipe id, or nullptr if the id has
|
||||||
|
// no entry in recipes.toml.
|
||||||
|
const RecipeDef* findRecipeDef(const std::string& id) const
|
||||||
|
{
|
||||||
|
for (const RecipeDef& recipe : recipes)
|
||||||
|
{
|
||||||
|
if (recipe.id == id)
|
||||||
|
{
|
||||||
|
return &recipe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same, but additionally requires the recipe to belong to the given building
|
||||||
|
// type — recipe ids are only unique per building type.
|
||||||
|
const RecipeDef* findRecipeDef(const std::string& id, BuildingType building) const
|
||||||
|
{
|
||||||
|
for (const RecipeDef& recipe : recipes)
|
||||||
|
{
|
||||||
|
if (recipe.id == id && recipe.building == building)
|
||||||
|
{
|
||||||
|
return &recipe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,4 +49,18 @@ struct ShipDef
|
|||||||
struct ShipsConfig
|
struct ShipsConfig
|
||||||
{
|
{
|
||||||
std::vector<ShipDef> ships;
|
std::vector<ShipDef> ships;
|
||||||
|
|
||||||
|
// Returns the definition for the given ship schematic id, or nullptr if the
|
||||||
|
// id has no entry in ships.toml.
|
||||||
|
const ShipDef* findShipDef(const std::string& id) const
|
||||||
|
{
|
||||||
|
for (const ShipDef& def : ships)
|
||||||
|
{
|
||||||
|
if (def.id == id)
|
||||||
|
{
|
||||||
|
return &def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
172
src/lib/config/TomlHelpers.cpp
Normal file
172
src/lib/config/TomlHelpers.cpp
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#include "TomlHelpers.h"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace utility
|
||||||
|
{
|
||||||
|
|
||||||
|
// --- Error helpers --------------------------------------------------------
|
||||||
|
|
||||||
|
std::runtime_error makeError(const std::string& file,
|
||||||
|
const std::string& path,
|
||||||
|
const std::string& why)
|
||||||
|
{
|
||||||
|
return std::runtime_error("Config: " + file + ": '" + path + "' " + why);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Typed accessors (throw on missing or wrong type) ---------------------
|
||||||
|
|
||||||
|
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const std::optional<int64_t> value = node.value<int64_t>();
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, "missing or not an integer");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
double requireDouble(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
if (const std::optional<double> v = node.value<double>())
|
||||||
|
{
|
||||||
|
return *v;
|
||||||
|
}
|
||||||
|
if (const std::optional<int64_t> v = node.value<int64_t>())
|
||||||
|
{
|
||||||
|
return static_cast<double>(*v);
|
||||||
|
}
|
||||||
|
throw makeError(file, path, "missing or not a number");
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string requireString(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const std::optional<std::string> value = node.value<std::string>();
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, "missing or not a string");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool requireBool(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const std::optional<bool> value = node.value<bool>();
|
||||||
|
if (!value)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, "missing or not a boolean");
|
||||||
|
}
|
||||||
|
return *value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const toml::array* arr = node.as_array();
|
||||||
|
if (arr == nullptr)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, "missing or not an array");
|
||||||
|
}
|
||||||
|
return *arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const toml::table* tbl = node.as_table();
|
||||||
|
if (tbl == nullptr)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, "missing or not a table");
|
||||||
|
}
|
||||||
|
return *tbl;
|
||||||
|
}
|
||||||
|
|
||||||
|
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const std::string source = requireString(node, file, path);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Formula::compile(source);
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
throw makeError(file, path, std::string("formula error: ") + e.what());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
const toml::array& arr = requireArray(node, file, path);
|
||||||
|
std::vector<std::string> result;
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||||
|
const std::optional<std::string> s = arr[i].value<std::string>();
|
||||||
|
if (!s)
|
||||||
|
{
|
||||||
|
throw makeError(file, elemPath, "not a string");
|
||||||
|
}
|
||||||
|
result.push_back(*s);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path)
|
||||||
|
{
|
||||||
|
std::vector<RecipeIngredient> result;
|
||||||
|
result.reserve(arr.size());
|
||||||
|
for (std::size_t i = 0; i < arr.size(); ++i)
|
||||||
|
{
|
||||||
|
const std::string elemPath = path + "[" + std::to_string(i) + "]";
|
||||||
|
const toml::table* t = arr[i].as_table();
|
||||||
|
if (t == nullptr)
|
||||||
|
{
|
||||||
|
throw makeError(file, elemPath, "not a table");
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need a mutable node_view to reuse our helpers, which is fine
|
||||||
|
// because the helpers never mutate.
|
||||||
|
toml::table& mt = const_cast<toml::table&>(*t);
|
||||||
|
|
||||||
|
RecipeIngredient ing;
|
||||||
|
ing.item = requireString(mt["item"], file, elemPath + ".item");
|
||||||
|
ing.amount = static_cast<int>(requireInt(mt["amount"], file, elemPath + ".amount"));
|
||||||
|
result.push_back(std::move(ing));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
toml::table parseFile(const std::string& path, const std::string& file)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return toml::parse_file(path);
|
||||||
|
}
|
||||||
|
catch (const toml::parse_error& e)
|
||||||
|
{
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "Config: " << file << ": TOML parse error: " << e.description()
|
||||||
|
<< " at " << e.source().begin;
|
||||||
|
throw std::runtime_error(oss.str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace utility
|
||||||
70
src/lib/config/TomlHelpers.h
Normal file
70
src/lib/config/TomlHelpers.h
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "toml.hpp"
|
||||||
|
|
||||||
|
#include "Formula.h"
|
||||||
|
#include "RecipesConfig.h" // for RecipeIngredient
|
||||||
|
|
||||||
|
// Shared TOML-parsing helpers used by two or more ConfigLoader per-domain
|
||||||
|
// loaders. Helpers used by exactly one domain stay local to that domain's
|
||||||
|
// .cpp file instead.
|
||||||
|
//
|
||||||
|
// Namespaced because the names are generic: VisualsLoader.cpp and
|
||||||
|
// BalancingConfig.cpp each have their own same-named helpers in anonymous
|
||||||
|
// namespaces, and unqualified globals here would form an overload set with
|
||||||
|
// them the moment either file includes this header.
|
||||||
|
namespace utility
|
||||||
|
{
|
||||||
|
|
||||||
|
// --- Error helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
std::runtime_error makeError(const std::string& file,
|
||||||
|
const std::string& path,
|
||||||
|
const std::string& why);
|
||||||
|
|
||||||
|
// --- Typed accessors (throw on missing or wrong type) -----------------------
|
||||||
|
|
||||||
|
int64_t requireInt(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
double requireDouble(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
std::string requireString(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
bool requireBool(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
const toml::array& requireArray(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
const toml::table& requireTable(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
Formula requireFormula(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
std::vector<std::string> requireStringArray(const toml::node_view<toml::node>& node,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
std::vector<RecipeIngredient> parseIngredients(const toml::array& arr,
|
||||||
|
const std::string& file,
|
||||||
|
const std::string& path);
|
||||||
|
|
||||||
|
toml::table parseFile(const std::string& path, const std::string& file);
|
||||||
|
|
||||||
|
} // namespace utility
|
||||||
@@ -38,3 +38,32 @@ std::string buildingTypeId(BuildingType type)
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isAutoRecipeBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Smelter
|
||||||
|
|| type == BuildingType::ReprocessingPlant;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isBeltSubsystemType(BuildingType type)
|
||||||
|
{
|
||||||
|
return type == BuildingType::Belt
|
||||||
|
|| type == BuildingType::Splitter
|
||||||
|
|| type == BuildingType::TunnelEntry
|
||||||
|
|| type == BuildingType::TunnelExit;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isProductionBuildingType(BuildingType type)
|
||||||
|
{
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case BuildingType::Miner:
|
||||||
|
case BuildingType::Smelter:
|
||||||
|
case BuildingType::Assembler:
|
||||||
|
case BuildingType::ReprocessingPlant:
|
||||||
|
case BuildingType::Shipyard:
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,3 +29,17 @@ std::optional<BuildingType> parseBuildingType(const std::string& id);
|
|||||||
|
|
||||||
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
// Canonical id string for a BuildingType. The inverse of parseBuildingType.
|
||||||
std::string buildingTypeId(BuildingType type);
|
std::string buildingTypeId(BuildingType type);
|
||||||
|
|
||||||
|
// Smelter and Reprocessing Plant have no player-selected recipe
|
||||||
|
// (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING). They auto-process whatever inputs
|
||||||
|
// they receive, matching against every recipe of their building type.
|
||||||
|
bool isAutoRecipeBuildingType(BuildingType type);
|
||||||
|
|
||||||
|
// Buildings that run a production cycle: Miner, Smelter, Assembler, Reprocessing
|
||||||
|
// Plant and Shipyard (REQ-UI-DEBUG-OVERLAY counts these).
|
||||||
|
bool isProductionBuildingType(BuildingType type);
|
||||||
|
|
||||||
|
// Belts, splitters, and tunnel ends keep their runtime data in the belt subsystem
|
||||||
|
// rather than in the Building instance, so placing/removing them must register or
|
||||||
|
// unregister a tile with BeltSystem.
|
||||||
|
bool isBeltSubsystemType(BuildingType type);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ItemType.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Item.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Port.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
${CMAKE_CURRENT_SOURCE_DIR}/SchematicChoiceOption.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.h
|
||||||
@@ -19,6 +20,7 @@ SET(HDRS
|
|||||||
SET(SRCS
|
SET(SRCS
|
||||||
${SRCS}
|
${SRCS}
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingType.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PortGeometry.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityAdmin.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/DisplayName.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltDragPath.cpp
|
||||||
|
|||||||
@@ -45,11 +45,11 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
|||||||
const std::string& schematicId, bool isEnemy)
|
const std::string& schematicId, bool isEnemy)
|
||||||
{
|
{
|
||||||
entt::entity entity = createEntity();
|
entt::entity entity = createEntity();
|
||||||
add<PositionComponent>(entity, PositionComponent{position});
|
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||||
add<FacingComponent>(entity, FacingComponent{0.0f});
|
addComponent<FacingComponent>(entity, FacingComponent{0.0f});
|
||||||
add<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
addComponent<DynamicBodyComponent>(entity, DynamicBodyComponent{
|
||||||
maxSpeed_tpt,
|
maxSpeed_tpt,
|
||||||
mainAcceleration_tptt,
|
mainAcceleration_tptt,
|
||||||
maneuveringAcceleration_tptt,
|
maneuveringAcceleration_tptt,
|
||||||
@@ -60,9 +60,9 @@ entt::entity EntityAdmin::spawnShip(QVector2D position, float hp, float maxHp,
|
|||||||
QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
|
QVector2D(0.0f, 0.0f), // linearAcceleration_tptt
|
||||||
0.0f // angularAcceleration_rptt
|
0.0f // angularAcceleration_rptt
|
||||||
});
|
});
|
||||||
add<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
addComponent<SensorRangeComponent>(entity, SensorRangeComponent{sensorRange_tiles});
|
||||||
add<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
addComponent<ShipIdentityComponent>(entity, ShipIdentityComponent{schematicId});
|
||||||
add<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
addComponent<MovementIntentComponent>(entity, MovementIntentComponent{0, QVector2D(0.0f, 0.0f)});
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,28 +73,28 @@ entt::entity EntityAdmin::spawnStation(QPoint anchor, QSize footprint,
|
|||||||
entt::entity entity = createEntity();
|
entt::entity entity = createEntity();
|
||||||
QVector2D center(anchor.x() + footprint.width() / 2.0f,
|
QVector2D center(anchor.x() + footprint.width() / 2.0f,
|
||||||
anchor.y() + footprint.height() / 2.0f);
|
anchor.y() + footprint.height() / 2.0f);
|
||||||
add<PositionComponent>(entity, PositionComponent{center});
|
addComponent<PositionComponent>(entity, PositionComponent{center});
|
||||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||||
add<FactionComponent>(entity, FactionComponent{isEnemy});
|
addComponent<FactionComponent>(entity, FactionComponent{isEnemy});
|
||||||
add<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
addComponent<StationBodyComponent>(entity, StationBodyComponent{anchor, footprint, bodyCells});
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
entt::entity EntityAdmin::spawnDebris(QVector2D position, int amount, Tick despawnAt)
|
entt::entity EntityAdmin::spawnDebris(QVector2D position, int amount, Tick despawnAt)
|
||||||
{
|
{
|
||||||
entt::entity entity = createEntity();
|
entt::entity entity = createEntity();
|
||||||
add<PositionComponent>(entity, PositionComponent{position});
|
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||||
add<DebrisComponent>(entity, DebrisComponent{amount});
|
addComponent<DebrisComponent>(entity, DebrisComponent{amount});
|
||||||
add<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
addComponent<DespawnAtComponent>(entity, DespawnAtComponent{despawnAt});
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|
||||||
entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp)
|
entt::entity EntityAdmin::spawnHqProxy(QVector2D position, float hp, float maxHp)
|
||||||
{
|
{
|
||||||
entt::entity entity = createEntity();
|
entt::entity entity = createEntity();
|
||||||
add<PositionComponent>(entity, PositionComponent{position});
|
addComponent<PositionComponent>(entity, PositionComponent{position});
|
||||||
add<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
addComponent<HealthComponent>(entity, HealthComponent{hp, maxHp});
|
||||||
add<FactionComponent>(entity, FactionComponent{false});
|
addComponent<FactionComponent>(entity, FactionComponent{false});
|
||||||
add<HqProxyComponent>(entity);
|
addComponent<HqProxyComponent>(entity);
|
||||||
return entity;
|
return entity;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,9 +73,6 @@ public:
|
|||||||
private:
|
private:
|
||||||
entt::entity createEntity();
|
entt::entity createEntity();
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
|
||||||
void add(entt::entity entity, Args&&... args);
|
|
||||||
|
|
||||||
entt::registry m_registry;
|
entt::registry m_registry;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -133,10 +130,4 @@ void EntityAdmin::removeComponent(entt::entity entity)
|
|||||||
m_registry.remove<T>(entity);
|
m_registry.remove<T>(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename T, typename... Args>
|
|
||||||
void EntityAdmin::add(entt::entity entity, Args&&... args)
|
|
||||||
{
|
|
||||||
m_registry.emplace<T>(entity, std::forward<Args>(args)...);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // ENTITY_ADMIN_H
|
#endif // ENTITY_ADMIN_H
|
||||||
|
|||||||
57
src/lib/core/PortGeometry.cpp
Normal file
57
src/lib/core/PortGeometry.cpp
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#include "PortGeometry.h"
|
||||||
|
|
||||||
|
#include <set>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
std::vector<Port> computeInputPorts(
|
||||||
|
const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts)
|
||||||
|
{
|
||||||
|
// Build lookup sets for quick membership checks.
|
||||||
|
std::set<std::pair<int, int>> bodySet;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
bodySet.insert({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> outputPortTiles;
|
||||||
|
for (const Port& port : outputPorts)
|
||||||
|
{
|
||||||
|
outputPortTiles.insert({port.tile.x(), port.tile.y()});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Neighbour deltas and the corresponding "inward" belt direction.
|
||||||
|
const int dx[4] = {-1, 1, 0, 0};
|
||||||
|
const int dy[4] = { 0, 0, -1, 1};
|
||||||
|
const Rotation inward[4] = {
|
||||||
|
Rotation::East, // neighbour is to the West; belt flows East toward building
|
||||||
|
Rotation::West, // neighbour is to the East; belt flows West toward building
|
||||||
|
Rotation::South, // neighbour is above (row-1); belt flows South toward building
|
||||||
|
Rotation::North // neighbour is below (row+1); belt flows North toward building
|
||||||
|
};
|
||||||
|
|
||||||
|
std::set<std::pair<int, int>> seen;
|
||||||
|
std::vector<Port> inputPorts;
|
||||||
|
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; ++i)
|
||||||
|
{
|
||||||
|
const int nx = cell.x() + dx[i];
|
||||||
|
const int ny = cell.y() + dy[i];
|
||||||
|
const std::pair<int, int> neighbor = {nx, ny};
|
||||||
|
|
||||||
|
if (bodySet.count(neighbor)) { continue; }
|
||||||
|
if (outputPortTiles.count(neighbor)){ continue; }
|
||||||
|
if (seen.count(neighbor)) { continue; }
|
||||||
|
|
||||||
|
seen.insert(neighbor);
|
||||||
|
Port port;
|
||||||
|
port.tile = QPoint(nx, ny);
|
||||||
|
port.direction = inward[i];
|
||||||
|
inputPorts.push_back(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return inputPorts;
|
||||||
|
}
|
||||||
55
src/lib/core/PortGeometry.h
Normal file
55
src/lib/core/PortGeometry.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "Port.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Geometry of a building's input/output ports. A Port names the tile *outside* the
|
||||||
|
// building together with the direction items flow across it; these helpers give the
|
||||||
|
// building body tile on the other side of that edge, which is where the virtual
|
||||||
|
// input/output belt lives.
|
||||||
|
//
|
||||||
|
// Shared by the simulation (which moves items across the edge) and the renderer
|
||||||
|
// (which draws the virtual belt), so the two cannot disagree about which tile a
|
||||||
|
// port belongs to.
|
||||||
|
|
||||||
|
// The building body tile that owns an output port, given the port's outside tile
|
||||||
|
// (port.tile) and its facing direction. The virtual output belt occupies this tile
|
||||||
|
// and flows toward port.tile (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
inline QPoint outputBodyTile(QPoint portTile, Rotation direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Rotation::East: return portTile + QPoint(-1, 0);
|
||||||
|
case Rotation::West: return portTile + QPoint( 1, 0);
|
||||||
|
case Rotation::North: return portTile + QPoint( 0, 1);
|
||||||
|
case Rotation::South: return portTile + QPoint( 0, -1);
|
||||||
|
}
|
||||||
|
return portTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The building body tile an input port feeds into, given the port's outside belt
|
||||||
|
// tile (port.tile) and its inward flow direction. The virtual input belt occupies
|
||||||
|
// this tile and flows from the outer edge (progress 0.0) to the centre (0.5)
|
||||||
|
// (REQ-MAT-INPUT-INTAKE).
|
||||||
|
inline QPoint inputBodyTile(QPoint portTile, Rotation inwardDirection)
|
||||||
|
{
|
||||||
|
switch (inwardDirection)
|
||||||
|
{
|
||||||
|
case Rotation::East: return portTile + QPoint( 1, 0);
|
||||||
|
case Rotation::West: return portTile + QPoint(-1, 0);
|
||||||
|
case Rotation::North: return portTile + QPoint( 0, -1);
|
||||||
|
case Rotation::South: return portTile + QPoint( 0, 1);
|
||||||
|
}
|
||||||
|
return portTile;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every belt-facing edge of a footprint that is not already an output port — the
|
||||||
|
// tiles a belt can feed the building from, with the direction items must flow to
|
||||||
|
// enter (REQ-MAT-INPUT-PORTS, REQ-BLD-BELT-DRAG). bodyCells and outputPorts are
|
||||||
|
// in absolute tile coordinates, and so is the result.
|
||||||
|
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
||||||
|
const std::vector<Port>& outputPorts);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
@@ -42,8 +43,7 @@ AiSystem::AiSystem(const GameConfig& config)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
void AiSystem::tick(EntityAdmin& admin, const FactoryState& state)
|
||||||
const DebrisSystem& debris)
|
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|
||||||
@@ -54,8 +54,8 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
|||||||
m_retreatEvaluator.evaluate(admin);
|
m_retreatEvaluator.evaluate(admin);
|
||||||
m_attackEvaluator.evaluate(admin);
|
m_attackEvaluator.evaluate(admin);
|
||||||
m_repairEvaluator.evaluate(admin);
|
m_repairEvaluator.evaluate(admin);
|
||||||
m_salvageScrapEvaluator.evaluate(admin, debris);
|
m_salvageScrapEvaluator.evaluate(admin);
|
||||||
m_deliverScrapEvaluator.evaluate(admin, buildings);
|
m_deliverScrapEvaluator.evaluate(admin, state);
|
||||||
|
|
||||||
// Phase 2: pick the highest-scoring behavior per ship.
|
// Phase 2: pick the highest-scoring behavior per ship.
|
||||||
selectWinningBehaviors(admin);
|
selectWinningBehaviors(admin);
|
||||||
@@ -68,7 +68,7 @@ void AiSystem::tick(EntityAdmin& admin, const BuildingSystem& buildings,
|
|||||||
m_attackExecutor.execute(admin);
|
m_attackExecutor.execute(admin);
|
||||||
m_repairExecutor.execute(admin);
|
m_repairExecutor.execute(admin);
|
||||||
m_salvageScrapExecutor.execute(admin);
|
m_salvageScrapExecutor.execute(admin);
|
||||||
m_deliverScrapExecutor.execute(admin, buildings);
|
m_deliverScrapExecutor.execute(admin, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
void AiSystem::selectWinningBehaviors(EntityAdmin& admin)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include "AdvanceEvaluator.h"
|
#include "AdvanceEvaluator.h"
|
||||||
#include "AdvanceExecutor.h"
|
#include "AdvanceExecutor.h"
|
||||||
#include "AttackEvaluator.h"
|
#include "AttackEvaluator.h"
|
||||||
@@ -17,9 +19,7 @@
|
|||||||
#include "StandbyEvaluator.h"
|
#include "StandbyEvaluator.h"
|
||||||
#include "StandbyExecutor.h"
|
#include "StandbyExecutor.h"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
struct GameConfig;
|
struct GameConfig;
|
||||||
|
|
||||||
// Orchestrates ship-behavior decision-making in three batched phases:
|
// Orchestrates ship-behavior decision-making in three batched phases:
|
||||||
@@ -34,7 +34,7 @@ class AiSystem
|
|||||||
public:
|
public:
|
||||||
explicit AiSystem(const GameConfig& config);
|
explicit AiSystem(const GameConfig& config);
|
||||||
|
|
||||||
void tick(EntityAdmin& admin, const BuildingSystem& buildings, const DebrisSystem& debris);
|
void tick(EntityAdmin& admin, const FactoryState& state);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void selectWinningBehaviors(EntityAdmin& admin);
|
void selectWinningBehaviors(EntityAdmin& admin);
|
||||||
|
|||||||
@@ -5,8 +5,10 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackEvaluator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/AttackExecutor.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/BehaviorTargeting.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/Centroid.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapEvaluator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/DeliverScrapExecutor.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/OrbitAndAssignExecutor.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyEvaluator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/RallyExecutor.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ai/RepairEvaluator.h
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ CombatSystem::CombatSystem(const GameConfig& config)
|
|||||||
|
|
||||||
void CombatSystem::tick(Tick currentTick,
|
void CombatSystem::tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& /*buildings*/,
|
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
|
|
||||||
class CombatSystem
|
class CombatSystem
|
||||||
@@ -25,7 +24,6 @@ public:
|
|||||||
|
|
||||||
void tick(Tick currentTick,
|
void tick(Tick currentTick,
|
||||||
EntityAdmin& admin,
|
EntityAdmin& admin,
|
||||||
BuildingSystem& buildings,
|
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||||
|
|
||||||
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
void applyPendingDamage(Tick currentTick, EntityAdmin& admin);
|
||||||
|
|||||||
@@ -46,13 +46,13 @@ std::optional<int> DebrisSystem::consume(entt::entity entity)
|
|||||||
return amount;
|
return amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DebrisSystem::collectOne(entt::entity entity)
|
bool collectOne(EntityAdmin& admin, entt::entity entity)
|
||||||
{
|
{
|
||||||
if (!m_admin.isValid(entity) || !m_admin.hasAll<DebrisComponent>(entity))
|
if (!admin.isValid(entity) || !admin.hasAll<DebrisComponent>(entity))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DebrisComponent& data = m_admin.get<DebrisComponent>(entity);
|
DebrisComponent& data = admin.get<DebrisComponent>(entity);
|
||||||
if (data.amount <= 0)
|
if (data.amount <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -60,18 +60,18 @@ bool DebrisSystem::collectOne(entt::entity entity)
|
|||||||
--data.amount;
|
--data.amount;
|
||||||
if (data.amount <= 0)
|
if (data.amount <= 0)
|
||||||
{
|
{
|
||||||
m_admin.destroy(entity);
|
admin.destroy(entity);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<DebrisInfo> DebrisSystem::getAllDebrisInfo() const
|
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
std::vector<DebrisInfo> result;
|
std::vector<DebrisInfo> result;
|
||||||
m_admin.forEach<DebrisComponent>(
|
admin.forEach<DebrisComponent>(
|
||||||
[&result, this](entt::entity e, const DebrisComponent& sd)
|
[&result, &admin](entt::entity e, const DebrisComponent& sd)
|
||||||
{
|
{
|
||||||
result.push_back(DebrisInfo{e, m_admin.get<PositionComponent>(e).value, sd.amount});
|
result.push_back(DebrisInfo{e, admin.get<PositionComponent>(e).value, sd.amount});
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,17 @@ public:
|
|||||||
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||||
bool collectOne(entt::entity entity);
|
bool collectOne(entt::entity entity);
|
||||||
|
|
||||||
// Lightweight snapshot for callers that need to iterate all debris.
|
|
||||||
std::vector<DebrisInfo> getAllDebrisInfo() const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Debris state read and changed straight off the registry — no system needed.
|
||||||
|
|
||||||
|
// Lightweight snapshot for callers that need to iterate all debris.
|
||||||
|
std::vector<DebrisInfo> getAllDebrisInfo(const EntityAdmin& admin);
|
||||||
|
|
||||||
|
// Collects a single scrap unit from the debris: decrements its amount by one,
|
||||||
|
// destroying the entity once depleted. Returns true if a scrap was collected,
|
||||||
|
// false if the entity is invalid or already empty (REQ-SHP-SALVAGE).
|
||||||
|
bool collectOne(EntityAdmin& admin, entt::entity entity);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "SalvagerSystem.h"
|
#include "SalvagerSystem.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -23,14 +24,14 @@ SalvagerSystem::SalvagerSystem(EntityAdmin& admin)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
|
void SalvagerSystem::tick(Tick currentTick, FactoryState& state,
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
std::vector<BeamFiredEvent>& outBeamFiredEvents)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
|
// Apply collections whose mid-beam delay has elapsed (cycles started earlier).
|
||||||
applyPendingCollections(currentTick, debris);
|
applyPendingCollections(currentTick);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(m_admin);
|
||||||
|
|
||||||
// Tick down per-module collection cooldowns.
|
// Tick down per-module collection cooldowns.
|
||||||
m_admin.forEach<SalvagerComponent>(
|
m_admin.forEach<SalvagerComponent>(
|
||||||
@@ -89,7 +90,7 @@ void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem
|
|||||||
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
[&](entt::entity ship, const DeliverScrapBehavior& deliver, const PositionComponent& pos)
|
||||||
{
|
{
|
||||||
if (!deliver.deliveryBay.has_value()) { return; }
|
if (!deliver.deliveryBay.has_value()) { return; }
|
||||||
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
|
const Building* bay = findBuilding(state, *deliver.deliveryBay);
|
||||||
if (!bay) { return; }
|
if (!bay) { return; }
|
||||||
|
|
||||||
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
const QVector2D bayCenter(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||||
@@ -100,14 +101,14 @@ void SalvagerSystem::tick(Tick currentTick, DebrisSystem& debris, BuildingSystem
|
|||||||
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
|
if (!m_admin.hasAll<CargoComponent>(ship)) { return; }
|
||||||
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
|
CargoComponent& cargo = m_admin.get<CargoComponent>(ship);
|
||||||
if (cargo.current <= 0) { return; }
|
if (cargo.current <= 0) { return; }
|
||||||
if (buildings.deliverScrapToSalvageBay(*deliver.deliveryBay))
|
if (deliverScrapToSalvageBay(state, *deliver.deliveryBay))
|
||||||
{
|
{
|
||||||
--cargo.current;
|
--cargo.current;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& debris)
|
void SalvagerSystem::applyPendingCollections(Tick currentTick)
|
||||||
{
|
{
|
||||||
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
|
std::vector<PendingCollection>::iterator it = m_pendingCollections.begin();
|
||||||
while (it != m_pendingCollections.end())
|
while (it != m_pendingCollections.end())
|
||||||
@@ -117,7 +118,7 @@ void SalvagerSystem::applyPendingCollections(Tick currentTick, DebrisSystem& deb
|
|||||||
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
if (m_admin.isValid(it->ship) && m_admin.hasAll<CargoComponent>(it->ship))
|
||||||
{
|
{
|
||||||
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
CargoComponent& cargo = m_admin.get<CargoComponent>(it->ship);
|
||||||
if (cargo.current < cargo.maxCapacity && debris.collectOne(it->debris))
|
if (cargo.current < cargo.maxCapacity && collectOne(m_admin, it->debris))
|
||||||
{
|
{
|
||||||
++cargo.current;
|
++cargo.current;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "BeamFiredEvent.h"
|
#include "BeamFiredEvent.h"
|
||||||
@@ -7,9 +9,7 @@
|
|||||||
|
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
class BuildingSystem;
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
|
|
||||||
// World-mutation system for salvage modules: each module runs a collection cycle
|
// World-mutation system for salvage modules: each module runs a collection cycle
|
||||||
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
|
// on its own cooldown. When a cycle starts it emits a salvage beam toward an
|
||||||
@@ -21,7 +21,7 @@ class SalvagerSystem
|
|||||||
public:
|
public:
|
||||||
explicit SalvagerSystem(EntityAdmin& admin);
|
explicit SalvagerSystem(EntityAdmin& admin);
|
||||||
|
|
||||||
void tick(Tick currentTick, DebrisSystem& debris, BuildingSystem& buildings,
|
void tick(Tick currentTick, FactoryState& state,
|
||||||
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
std::vector<BeamFiredEvent>& outBeamFiredEvents);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -32,7 +32,7 @@ private:
|
|||||||
Tick appliesAt;
|
Tick appliesAt;
|
||||||
};
|
};
|
||||||
|
|
||||||
void applyPendingCollections(Tick currentTick, DebrisSystem& debris);
|
void applyPendingCollections(Tick currentTick);
|
||||||
|
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
std::vector<PendingCollection> m_pendingCollections;
|
std::vector<PendingCollection> m_pendingCollections;
|
||||||
|
|||||||
@@ -41,35 +41,11 @@ ShipSystem::ShipSystem(const GameConfig& config, EntityAdmin& admin)
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipDef* ShipSystem::findShipDef(const std::string& schematicId) const
|
|
||||||
{
|
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
|
||||||
{
|
|
||||||
if (def.id == schematicId)
|
|
||||||
{
|
|
||||||
return &def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ModuleDef* ShipSystem::findModuleDef(const std::string& id) const
|
|
||||||
{
|
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
|
||||||
{
|
|
||||||
if (def.id == id)
|
|
||||||
{
|
|
||||||
return &def;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
entt::entity ShipSystem::spawn(const std::string& schematicId,
|
entt::entity ShipSystem::spawn(const std::string& schematicId,
|
||||||
QVector2D position, bool isEnemy,
|
QVector2D position, bool isEnemy,
|
||||||
const std::optional<ShipLayoutConfig>& layout)
|
const std::optional<ShipLayoutConfig>& layout)
|
||||||
{
|
{
|
||||||
const ShipDef* def = findShipDef(schematicId);
|
const ShipDef* def = m_config.ships.findShipDef(schematicId);
|
||||||
assert(def != nullptr);
|
assert(def != nullptr);
|
||||||
|
|
||||||
const float tickRate = static_cast<float>(kTickRateHz);
|
const float tickRate = static_cast<float>(kTickRateHz);
|
||||||
@@ -116,7 +92,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
|
|||||||
|
|
||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
if (modDef->weaponCapability)
|
if (modDef->weaponCapability)
|
||||||
@@ -184,7 +160,7 @@ entt::entity ShipSystem::spawn(const std::string& schematicId,
|
|||||||
|
|
||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* modDef = findModuleDef(pm.moduleId);
|
const ModuleDef* modDef = m_config.modules.findModuleDef(pm.moduleId);
|
||||||
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!modDef) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
for (const ModuleStatModifier& sm : modDef->statModifiers)
|
||||||
|
|||||||
@@ -38,9 +38,6 @@ public:
|
|||||||
void setRetreatEnabled(bool enabled);
|
void setRetreatEnabled(bool enabled);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const ShipDef* findShipDef(const std::string& schematicId) const;
|
|
||||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
EntityAdmin& m_admin;
|
EntityAdmin& m_admin;
|
||||||
QVector2D m_rallyPoint;
|
QVector2D m_rallyPoint;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include "AdvanceBehavior.h"
|
#include "AdvanceBehavior.h"
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
|
#include "Centroid.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
@@ -16,28 +17,6 @@
|
|||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Accumulates positions to produce their centroid (the center between them).
|
|
||||||
struct Centroid
|
|
||||||
{
|
|
||||||
QVector2D sum;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
void add(const QVector2D& point)
|
|
||||||
{
|
|
||||||
sum += point;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QVector2D> value() const
|
|
||||||
{
|
|
||||||
if (count == 0) { return std::nullopt; }
|
|
||||||
return sum / static_cast<float>(count);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void AdvanceExecutor::execute(EntityAdmin& admin)
|
void AdvanceExecutor::execute(EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
|
|
||||||
#include "AttackBehavior.h"
|
#include "AttackBehavior.h"
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
#include "DynamicBodyComponent.h"
|
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "OrbitAndAssignExecutor.h"
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
|
|
||||||
@@ -15,55 +11,7 @@ void AttackExecutor::execute(EntityAdmin& admin)
|
|||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|
||||||
// Ships: move toward the behavior target.
|
// Orbit the attack target and hand it to every weapon that can reach it
|
||||||
admin.forEach<AttackBehavior, SelectedBehaviorComponent, PositionComponent,
|
// (REQ-SHP-ORBIT).
|
||||||
MovementIntentComponent>(
|
executeOrbitAndAssign<AttackBehavior, WeaponComponent>(admin, BehaviorKind::Attack);
|
||||||
[&](entt::entity /*e*/, const AttackBehavior& attack,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
|
||||||
if (!attack.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *attack.currentTarget;
|
|
||||||
QVector2D center = pos.value;
|
|
||||||
float radius = 0.0f;
|
|
||||||
QVector2D centerVelocity;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
|
||||||
{
|
|
||||||
center = admin.get<PositionComponent>(t).value;
|
|
||||||
radius = attack.orbitRadius_tiles;
|
|
||||||
if (admin.hasAll<DynamicBodyComponent>(t))
|
|
||||||
{
|
|
||||||
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Weapons: assign the behavior target only if it is within this weapon's range.
|
|
||||||
admin.forEach<WeaponComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*we*/, WeaponComponent& weapon, const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
if (!admin.hasAll<AttackBehavior, SelectedBehaviorComponent>(owner.owner))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const SelectedBehaviorComponent& selected =
|
|
||||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
|
||||||
if (selected.winner != BehaviorKind::Attack) { return; }
|
|
||||||
|
|
||||||
const AttackBehavior& attack = admin.get<AttackBehavior>(owner.owner);
|
|
||||||
if (!attack.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *attack.currentTarget;
|
|
||||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
|
||||||
|
|
||||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
|
||||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
|
||||||
if (dist <= weapon.range_tiles)
|
|
||||||
{
|
|
||||||
weapon.currentTarget = t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/lib/ecs/system/ai/Centroid.h
Normal file
26
src/lib/ecs/system/ai/Centroid.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
// Accumulates positions to produce their centroid (the center between them).
|
||||||
|
// Shared by the behavior executors that steer toward the middle of a group of
|
||||||
|
// entities (AdvanceExecutor: defence stations; StandbyExecutor: friendly ships).
|
||||||
|
struct Centroid
|
||||||
|
{
|
||||||
|
QVector2D sum;
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
void add(const QVector2D& point)
|
||||||
|
{
|
||||||
|
sum += point;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<QVector2D> value() const
|
||||||
|
{
|
||||||
|
if (count == 0) { return std::nullopt; }
|
||||||
|
return sum / static_cast<float>(count);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "DeliverScrapEvaluator.h"
|
#include "DeliverScrapEvaluator.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
#include "PositionComponent.h"
|
#include "PositionComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& buildings)
|
void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const FactoryState& state)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
||||||
@@ -34,7 +35,7 @@ void DeliverScrapEvaluator::evaluate(EntityAdmin& admin, const BuildingSystem& b
|
|||||||
if (!deliver.deliveryBay.has_value())
|
if (!deliver.deliveryBay.has_value())
|
||||||
{
|
{
|
||||||
const Building* bay =
|
const Building* bay =
|
||||||
buildings.findNearestBuilding(pos.value, BuildingType::SalvageBay);
|
findNearestBuilding(state, pos.value, BuildingType::SalvageBay);
|
||||||
if (bay) { deliver.deliveryBay = bay->id; }
|
if (bay) { deliver.deliveryBay = bay->id; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Scores high only when the ship's cargo is full, and assigns the nearest
|
// Scores high only when the ship's cargo is full, and assigns the nearest
|
||||||
// SalvageBay as the delivery destination.
|
// SalvageBay as the delivery destination.
|
||||||
class DeliverScrapEvaluator
|
class DeliverScrapEvaluator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void evaluate(EntityAdmin& admin, const BuildingSystem& buildings);
|
void evaluate(EntityAdmin& admin, const FactoryState& state);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "DeliverScrapExecutor.h"
|
#include "DeliverScrapExecutor.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
@@ -12,7 +13,7 @@
|
|||||||
#include "SelectedBehaviorComponent.h"
|
#include "SelectedBehaviorComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& buildings)
|
void DeliverScrapExecutor::execute(EntityAdmin& admin, const FactoryState& state)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
admin.forEach<DeliverScrapBehavior, SelectedBehaviorComponent, PositionComponent,
|
||||||
@@ -26,7 +27,7 @@ void DeliverScrapExecutor::execute(EntityAdmin& admin, const BuildingSystem& bui
|
|||||||
QVector2D dest = pos.value;
|
QVector2D dest = pos.value;
|
||||||
if (deliver.deliveryBay.has_value())
|
if (deliver.deliveryBay.has_value())
|
||||||
{
|
{
|
||||||
const Building* bay = buildings.findBuilding(*deliver.deliveryBay);
|
const Building* bay = findBuilding(state, *deliver.deliveryBay);
|
||||||
if (bay)
|
if (bay)
|
||||||
{
|
{
|
||||||
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
dest = QVector2D(bay->anchor.x() + bay->footprint.width() / 2.0f,
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class BuildingSystem;
|
|
||||||
|
|
||||||
// Moves a ship toward its delivery bay when DeliverScrap is the winning
|
// Moves a ship toward its delivery bay when DeliverScrap is the winning
|
||||||
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
|
// behavior. Never decrements cargo — SalvagerSystem performs the delivery.
|
||||||
class DeliverScrapExecutor
|
class DeliverScrapExecutor
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void execute(EntityAdmin& admin, const BuildingSystem& buildings);
|
void execute(EntityAdmin& admin, const FactoryState& state);
|
||||||
};
|
};
|
||||||
|
|||||||
89
src/lib/ecs/system/ai/OrbitAndAssignExecutor.h
Normal file
89
src/lib/ecs/system/ai/OrbitAndAssignExecutor.h
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "entt/entity/entity.hpp"
|
||||||
|
|
||||||
|
#include "BehaviorKind.h"
|
||||||
|
#include "DynamicBodyComponent.h"
|
||||||
|
#include "EntityAdmin.h"
|
||||||
|
#include "ModuleOwnerComponent.h"
|
||||||
|
#include "MovementIntentComponent.h"
|
||||||
|
#include "PositionComponent.h"
|
||||||
|
#include "SelectedBehaviorComponent.h"
|
||||||
|
|
||||||
|
// Shared executor body for the behaviors that orbit a single target entity and then
|
||||||
|
// hand that target to the ship's in-range modules (REQ-SHP-ORBIT): Attack (with
|
||||||
|
// WeaponComponent) and Repair (with RepairToolComponent).
|
||||||
|
//
|
||||||
|
// Two passes, in this order — the order and the exact sequence of component writes
|
||||||
|
// are load-bearing for determinism (see the Tick Order section of
|
||||||
|
// docs/architecture.md):
|
||||||
|
// 1. Ships that have `Behavior` and won with `kind` write their MovementIntent to
|
||||||
|
// orbit the behavior's target at the behavior's orbit radius. A target that is
|
||||||
|
// gone (or has no position) degenerates to "hold position": the ship's own
|
||||||
|
// position with a zero radius.
|
||||||
|
// 2. Modules of type `ModuleComponent` whose owner won with `kind` adopt the
|
||||||
|
// behavior's target, but only when it lies within that module's own range.
|
||||||
|
// Out-of-range modules keep whatever target they already had, which
|
||||||
|
// CombatSystem/RepairSystem re-validate.
|
||||||
|
//
|
||||||
|
// `Behavior` must expose `std::optional<entt::entity> currentTarget` and
|
||||||
|
// `float orbitRadius_tiles`; `ModuleComponent` must expose `float range_tiles` and
|
||||||
|
// `std::optional<entt::entity> currentTarget`.
|
||||||
|
template <typename Behavior, typename ModuleComponent>
|
||||||
|
void executeOrbitAndAssign(EntityAdmin& admin, BehaviorKind kind)
|
||||||
|
{
|
||||||
|
// Ships: move toward the behavior target.
|
||||||
|
admin.forEach<Behavior, SelectedBehaviorComponent, PositionComponent,
|
||||||
|
MovementIntentComponent>(
|
||||||
|
[&](entt::entity /*e*/, const Behavior& behavior,
|
||||||
|
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
||||||
|
MovementIntentComponent& intent)
|
||||||
|
{
|
||||||
|
if (selected.winner != kind) { return; }
|
||||||
|
if (!behavior.currentTarget) { return; }
|
||||||
|
|
||||||
|
const entt::entity t = *behavior.currentTarget;
|
||||||
|
QVector2D center = pos.value;
|
||||||
|
float radius = 0.0f;
|
||||||
|
QVector2D centerVelocity;
|
||||||
|
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
||||||
|
{
|
||||||
|
center = admin.get<PositionComponent>(t).value;
|
||||||
|
radius = behavior.orbitRadius_tiles;
|
||||||
|
if (admin.hasAll<DynamicBodyComponent>(t))
|
||||||
|
{
|
||||||
|
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Modules: assign the behavior target only if it is within this module's range.
|
||||||
|
admin.forEach<ModuleComponent, ModuleOwnerComponent>(
|
||||||
|
[&](entt::entity /*me*/, ModuleComponent& module,
|
||||||
|
const ModuleOwnerComponent& owner)
|
||||||
|
{
|
||||||
|
if (!admin.hasAll<Behavior, SelectedBehaviorComponent>(owner.owner))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const SelectedBehaviorComponent& selected =
|
||||||
|
admin.get<SelectedBehaviorComponent>(owner.owner);
|
||||||
|
if (selected.winner != kind) { return; }
|
||||||
|
|
||||||
|
const Behavior& behavior = admin.get<Behavior>(owner.owner);
|
||||||
|
if (!behavior.currentTarget) { return; }
|
||||||
|
|
||||||
|
const entt::entity t = *behavior.currentTarget;
|
||||||
|
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
||||||
|
|
||||||
|
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
||||||
|
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
||||||
|
if (dist <= module.range_tiles)
|
||||||
|
{
|
||||||
|
module.currentTarget = t;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,69 +1,17 @@
|
|||||||
#include "RepairExecutor.h"
|
#include "RepairExecutor.h"
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
#include "DynamicBodyComponent.h"
|
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "ModuleOwnerComponent.h"
|
#include "OrbitAndAssignExecutor.h"
|
||||||
#include "MovementIntentComponent.h"
|
|
||||||
#include "PositionComponent.h"
|
|
||||||
#include "RepairBehavior.h"
|
#include "RepairBehavior.h"
|
||||||
#include "RepairToolComponent.h"
|
#include "RepairToolComponent.h"
|
||||||
#include "SelectedBehaviorComponent.h"
|
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void RepairExecutor::execute(EntityAdmin& admin)
|
void RepairExecutor::execute(EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|
||||||
// Ships: move toward the repair target.
|
// Orbit the repair target and hand it to every repair tool that can reach it
|
||||||
admin.forEach<RepairBehavior, SelectedBehaviorComponent, PositionComponent,
|
// (REQ-SHP-ORBIT).
|
||||||
MovementIntentComponent>(
|
executeOrbitAndAssign<RepairBehavior, RepairToolComponent>(admin, BehaviorKind::Repair);
|
||||||
[&](entt::entity /*e*/, const RepairBehavior& repair,
|
|
||||||
const SelectedBehaviorComponent& selected, const PositionComponent& pos,
|
|
||||||
MovementIntentComponent& intent)
|
|
||||||
{
|
|
||||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
|
||||||
if (!repair.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *repair.currentTarget;
|
|
||||||
QVector2D center = pos.value;
|
|
||||||
float radius = 0.0f;
|
|
||||||
QVector2D centerVelocity;
|
|
||||||
if (admin.isValid(t) && admin.hasAll<PositionComponent>(t))
|
|
||||||
{
|
|
||||||
center = admin.get<PositionComponent>(t).value;
|
|
||||||
radius = repair.orbitRadius_tiles;
|
|
||||||
if (admin.hasAll<DynamicBodyComponent>(t))
|
|
||||||
{
|
|
||||||
centerVelocity = admin.get<DynamicBodyComponent>(t).velocity_tpt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
intent = MovementIntentComponent{true, center, radius, centerVelocity};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Repair tools: prefer the behavior target if it is within tool range.
|
|
||||||
admin.forEach<RepairToolComponent, ModuleOwnerComponent>(
|
|
||||||
[&](entt::entity /*re*/, RepairToolComponent& tool, const ModuleOwnerComponent& owner)
|
|
||||||
{
|
|
||||||
if (!admin.hasAll<RepairBehavior, SelectedBehaviorComponent>(owner.owner))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const SelectedBehaviorComponent& selected =
|
|
||||||
admin.get<SelectedBehaviorComponent>(owner.owner);
|
|
||||||
if (selected.winner != BehaviorKind::Repair) { return; }
|
|
||||||
|
|
||||||
const RepairBehavior& repair = admin.get<RepairBehavior>(owner.owner);
|
|
||||||
if (!repair.currentTarget) { return; }
|
|
||||||
|
|
||||||
const entt::entity t = *repair.currentTarget;
|
|
||||||
if (!admin.isValid(t) || !admin.hasAll<PositionComponent>(t)) { return; }
|
|
||||||
|
|
||||||
const QVector2D ownerPos = admin.get<PositionComponent>(owner.owner).value;
|
|
||||||
const float dist = (admin.get<PositionComponent>(t).value - ownerPos).length();
|
|
||||||
if (dist <= tool.range_tiles)
|
|
||||||
{
|
|
||||||
tool.currentTarget = t;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@
|
|||||||
#include "SensorRangeComponent.h"
|
#include "SensorRangeComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin, const DebrisSystem& debris)
|
void SalvageScrapEvaluator::evaluate(EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
const std::unordered_map<entt::entity, CargoState> cargoByShip = buildCargoByShip(admin);
|
||||||
const std::vector<DebrisInfo> allDebris = debris.getAllDebrisInfo();
|
const std::vector<DebrisInfo> allDebris = getAllDebrisInfo(admin);
|
||||||
|
|
||||||
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
admin.forEach<SalvageScrapBehavior, PositionComponent, SensorRangeComponent>(
|
||||||
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
[&](entt::entity e, SalvageScrapBehavior& salvage, const PositionComponent& pos,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
class EntityAdmin;
|
class EntityAdmin;
|
||||||
class DebrisSystem;
|
|
||||||
|
|
||||||
// When cargo is not full, finds the nearest debris within sensor range and sets
|
// When cargo is not full, finds the nearest debris within sensor range and sets
|
||||||
// it as the target, scoring high. Scores inactive when cargo is full or no debris
|
// it as the target, scoring high. Scores inactive when cargo is full or no debris
|
||||||
@@ -9,5 +8,5 @@ class DebrisSystem;
|
|||||||
class SalvageScrapEvaluator
|
class SalvageScrapEvaluator
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void evaluate(EntityAdmin& admin, const DebrisSystem& debris);
|
void evaluate(EntityAdmin& admin);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
#include "BehaviorKind.h"
|
#include "BehaviorKind.h"
|
||||||
|
#include "Centroid.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "FactionComponent.h"
|
#include "FactionComponent.h"
|
||||||
#include "HealthComponent.h"
|
#include "HealthComponent.h"
|
||||||
@@ -16,28 +17,6 @@
|
|||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "tracing.h"
|
#include "tracing.h"
|
||||||
|
|
||||||
namespace
|
|
||||||
{
|
|
||||||
// Accumulates positions to produce their centroid (the center between them).
|
|
||||||
struct Centroid
|
|
||||||
{
|
|
||||||
QVector2D sum;
|
|
||||||
int count = 0;
|
|
||||||
|
|
||||||
void add(const QVector2D& point)
|
|
||||||
{
|
|
||||||
sum += point;
|
|
||||||
count += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::optional<QVector2D> value() const
|
|
||||||
{
|
|
||||||
if (count == 0) { return std::nullopt; }
|
|
||||||
return sum / static_cast<float>(count);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
void StandbyExecutor::execute(EntityAdmin& admin)
|
void StandbyExecutor::execute(EntityAdmin& admin)
|
||||||
{
|
{
|
||||||
TRACE();
|
TRACE();
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Fired when the collected artifact count changes. Carries no payload —
|
||||||
|
// subscribers re-read Simulation::getArtifactCount(), and the win count from
|
||||||
|
// world.artifacts.artifactWinCount.
|
||||||
class ArtifactCountChangedEvent : public Event
|
class ArtifactCountChangedEvent : public Event
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
ArtifactCountChangedEvent(int count, int winCount) : count(count), winCount(winCount) {}
|
|
||||||
const int count;
|
|
||||||
const int winCount;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,16 +2,11 @@
|
|||||||
#define BOSS_WAVE_UPDATED_EVENT_H
|
#define BOSS_WAVE_UPDATED_EVENT_H
|
||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
#include "Tick.h"
|
|
||||||
|
|
||||||
|
// Fired when the boss wave counter or its countdown changes. Carries no payload —
|
||||||
|
// subscribers re-read Simulation::getBossWaveCounter() and getBossCountdownTicks().
|
||||||
class BossWaveUpdatedEvent : public Event
|
class BossWaveUpdatedEvent : public Event
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
BossWaveUpdatedEvent(int counter, Tick countdownTicks)
|
|
||||||
: counter(counter), countdownTicks(countdownTicks) {}
|
|
||||||
const int counter;
|
|
||||||
const Tick countdownTicks;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BOSS_WAVE_UPDATED_EVENT_H
|
#endif // BOSS_WAVE_UPDATED_EVENT_H
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,10 @@
|
|||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
|
||||||
|
// Fired when the building block stock changes. Carries no payload — subscribers
|
||||||
|
// re-read Simulation::getBuildingBlocksStock().
|
||||||
class BuildingBlocksChangedEvent : public Event
|
class BuildingBlocksChangedEvent : public Event
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
explicit BuildingBlocksChangedEvent(int blocks) : blocks(blocks) {}
|
|
||||||
const int blocks;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // BUILDING_BLOCKS_CHANGED_EVENT_H
|
#endif // BUILDING_BLOCKS_CHANGED_EVENT_H
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,11 @@
|
|||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
|
|
||||||
// Fired when the current asteroid-expansion cost changes (REQ-EXP-COST): once at
|
// Fired when the current asteroid-expansion cost changes (REQ-EXP-COST): once at
|
||||||
// startup and again after each expansion is purchased. Carries the cost in
|
// startup and again after each expansion is purchased. Carries no payload — the
|
||||||
// building blocks so the header Expand button can update its caption/enabled
|
// header Expand button re-reads Simulation::getCurrentExpansionCost() to update
|
||||||
// state (REQ-UI-EXPAND-BUTTON).
|
// its caption and enabled state (REQ-UI-EXPAND-BUTTON).
|
||||||
class ExpansionCostChangedEvent : public Event
|
class ExpansionCostChangedEvent : public Event
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
explicit ExpansionCostChangedEvent(int cost) : cost(cost) {}
|
|
||||||
const int cost;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // EXPANSION_COST_CHANGED_EVENT_H
|
#endif // EXPANSION_COST_CHANGED_EVENT_H
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
#define TICK_ADVANCED_EVENT_H
|
#define TICK_ADVANCED_EVENT_H
|
||||||
|
|
||||||
#include "Event.h"
|
#include "Event.h"
|
||||||
#include "Tick.h"
|
|
||||||
|
|
||||||
|
// Fired when the simulation tick advances. Carries no payload — subscribers
|
||||||
|
// re-read Simulation::getCurrentTick().
|
||||||
class TickAdvancedEvent : public Event
|
class TickAdvancedEvent : public Event
|
||||||
{
|
{
|
||||||
public:
|
|
||||||
explicit TickAdvancedEvent(Tick tick) : tick(tick) {}
|
|
||||||
const Tick tick;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // TICK_ADVANCED_EVENT_H
|
#endif // TICK_ADVANCED_EVENT_H
|
||||||
|
|
||||||
|
|||||||
173
src/lib/sim/BuildingBuffers.cpp
Normal file
173
src/lib/sim/BuildingBuffers.cpp
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#include "BuildingBuffers.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cassert>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipsConfig.h"
|
||||||
|
|
||||||
|
void initBuffers(Building& b, const RecipeDef& recipe)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
if (b.type == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
// 1× max-per-roll (REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||||
|
int maxAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (out.amount > maxAmount)
|
||||||
|
{
|
||||||
|
maxAmount = out.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.outputBuffer.capacity = maxAmount;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 2× per-cycle output.
|
||||||
|
int totalAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
totalAmount += out.amount;
|
||||||
|
}
|
||||||
|
b.outputBuffer.capacity = 2 * totalAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initAutoBuffers(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
|
||||||
|
// Union the inputs of every recipe of this building type; the cap for each
|
||||||
|
// item is twice the largest per-cycle requirement across those recipes.
|
||||||
|
// Output capacity follows the same rules as initBuffers: the Reprocessing
|
||||||
|
// Plant holds one cycle's max output (REQ-MAT-OUTPUT-BUFFER-REPROCESSING),
|
||||||
|
// other auto buildings hold twice the largest per-cycle output.
|
||||||
|
int outputCapacity = 0;
|
||||||
|
for (const RecipeDef& recipe : config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != b.type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] =
|
||||||
|
std::max(b.inputBuffer.caps[type], 2 * ing.amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b.type == BuildingType::ReprocessingPlant)
|
||||||
|
{
|
||||||
|
int maxAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
maxAmount = std::max(maxAmount, out.amount);
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, maxAmount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
int totalAmount = 0;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
totalAmount += out.amount;
|
||||||
|
}
|
||||||
|
outputCapacity = std::max(outputCapacity, 2 * totalAmount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
b.outputBuffer.capacity = outputCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initShipyardBuffers(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
b.inputBuffer.counts.clear();
|
||||||
|
b.inputBuffer.caps.clear();
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
b.outputBuffer.capacity = 0;
|
||||||
|
const ShipDef* def = config.ships.findShipDef(b.recipeId);
|
||||||
|
if (!def)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : def->schematic.materials)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts[type] = 0;
|
||||||
|
b.inputBuffer.caps[type] = 2 * ing.amount;
|
||||||
|
}
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
const ItemType type{ing.item};
|
||||||
|
b.inputBuffer.counts.try_emplace(type, 0);
|
||||||
|
b.inputBuffer.caps[type] += 2 * ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initSalvageBayBuffer(const GameConfig& config, Building& b)
|
||||||
|
{
|
||||||
|
// Salvage Bay has no recipe-driven buffer; its output-buffer holding size for
|
||||||
|
// ship drop-off is config-defined (REQ-BLD-SALVAGE-BAY).
|
||||||
|
b.outputBuffer.items.clear();
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(BuildingType::SalvageBay);
|
||||||
|
b.outputBuffer.capacity =
|
||||||
|
(def && def->outputBufferCapacity) ? *def->outputBufferCapacity : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||||
|
const Building& building,
|
||||||
|
const std::vector<ItemType>& splitterFilterA,
|
||||||
|
const std::vector<ItemType>& splitterFilterB)
|
||||||
|
{
|
||||||
|
switch (building.type)
|
||||||
|
{
|
||||||
|
case BuildingType::Belt:
|
||||||
|
belts.placeBelt(building.anchor, building.rotation);
|
||||||
|
break;
|
||||||
|
case BuildingType::Splitter:
|
||||||
|
assert(building.outputPorts.size() >= 2);
|
||||||
|
belts.placeSplitter(building.anchor,
|
||||||
|
building.outputPorts[0].direction,
|
||||||
|
building.outputPorts[1].direction);
|
||||||
|
belts.setSplitterFilters(building.anchor, splitterFilterA, splitterFilterB);
|
||||||
|
break;
|
||||||
|
case BuildingType::TunnelEntry:
|
||||||
|
belts.placeTunnelEntry(building.anchor, building.rotation,
|
||||||
|
config.world.tunnelMaxDistance_tiles);
|
||||||
|
break;
|
||||||
|
case BuildingType::TunnelExit:
|
||||||
|
belts.placeTunnelExit(building.anchor, building.rotation);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
39
src/lib/sim/BuildingBuffers.h
Normal file
39
src/lib/sim/BuildingBuffers.h
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
|
// Setting a building up when it starts existing or is reconfigured: sizing its
|
||||||
|
// input/output buffers from what it will produce, and handing belt-like types back
|
||||||
|
// to BeltSystem. Free functions over the config and the building — they read no
|
||||||
|
// factory state, so both BuildingSystem and ConstructionSystem can use them.
|
||||||
|
|
||||||
|
// Buffers for a building running one known recipe: inputs capped at twice each
|
||||||
|
// ingredient's per-cycle amount, output at twice the per-cycle total (one cycle's
|
||||||
|
// max for a Reprocessing Plant, REQ-MAT-OUTPUT-BUFFER-REPROCESSING).
|
||||||
|
void initBuffers(Building& b, const RecipeDef& recipe);
|
||||||
|
|
||||||
|
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant), unioned over
|
||||||
|
// every recipe of its type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
void initAutoBuffers(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// Buffers for a shipyard: its schematic's materials plus those of every placed
|
||||||
|
// module (REQ-BLD-SHIPYARD).
|
||||||
|
void initShipyardBuffers(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// The Salvage Bay holds no recipe inputs; its output capacity is config-defined
|
||||||
|
// (REQ-BLD-SALVAGE-BAY).
|
||||||
|
void initSalvageBayBuffer(const GameConfig& config, Building& b);
|
||||||
|
|
||||||
|
// Registers a belt, splitter or tunnel end with BeltSystem. A splitter's filters
|
||||||
|
// live in BeltSystem and are lost by removeTile, so they are passed back in
|
||||||
|
// (REQ-BLD-SPLITTER). No-op for every other building type.
|
||||||
|
void reregisterBeltTile(BeltSystem& belts, const GameConfig& config,
|
||||||
|
const Building& building,
|
||||||
|
const std::vector<ItemType>& splitterFilterA,
|
||||||
|
const std::vector<ItemType>& splitterFilterB);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "BuildingConfig.h"
|
#include "BuildingConfig.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -26,8 +27,8 @@ struct SelectedBuilding
|
|||||||
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
|
// (the HQ and defence stations, per REQ-UI-BLUEPRINT-CREATE).
|
||||||
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
|
std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, BuildingId id)
|
||||||
{
|
{
|
||||||
const Building* building = sim.getBuildings().findBuilding(id);
|
const Building* building = findBuilding(sim.getFactoryState(), id);
|
||||||
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
|
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
|
||||||
if (!building && !site)
|
if (!building && !site)
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
@@ -52,8 +53,8 @@ std::optional<SelectedBuilding> resolvePlaceable(const Simulation& sim, Building
|
|||||||
|
|
||||||
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
std::optional<BuildingConfig> readBuildingConfig(const Simulation& sim, BuildingId id)
|
||||||
{
|
{
|
||||||
const Building* building = sim.getBuildings().findBuilding(id);
|
const Building* building = findBuilding(sim.getFactoryState(), id);
|
||||||
const ConstructionSite* site = building ? nullptr : sim.getBuildings().findSite(id);
|
const ConstructionSite* site = building ? nullptr : findSite(sim.getFactoryState(), id);
|
||||||
if (!building && !site)
|
if (!building && !site)
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
|
|||||||
52
src/lib/sim/BuildingGrid.cpp
Normal file
52
src/lib/sim/BuildingGrid.cpp
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#include "BuildingGrid.h"
|
||||||
|
|
||||||
|
#include "StateChecksum.h"
|
||||||
|
|
||||||
|
void BuildingGrid::occupy(QPoint cell, BuildingId id)
|
||||||
|
{
|
||||||
|
m_owners[{cell.x(), cell.y()}] = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::occupy(const std::vector<QPoint>& cells, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : cells)
|
||||||
|
{
|
||||||
|
occupy(cell, id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::release(const std::vector<QPoint>& cells)
|
||||||
|
{
|
||||||
|
for (const QPoint& cell : cells)
|
||||||
|
{
|
||||||
|
m_owners.erase({cell.x(), cell.y()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool BuildingGrid::isOccupied(QPoint tile) const
|
||||||
|
{
|
||||||
|
return m_owners.count({tile.x(), tile.y()}) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<BuildingId> BuildingGrid::findOwner(QPoint tile) const
|
||||||
|
{
|
||||||
|
const std::map<std::pair<int, int>, BuildingId>::const_iterator it =
|
||||||
|
m_owners.find({tile.x(), tile.y()});
|
||||||
|
if (it == m_owners.end())
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
void BuildingGrid::appendChecksum(Hasher& hasher) const
|
||||||
|
{
|
||||||
|
// std::map iterates in sorted key order.
|
||||||
|
hasher.append(m_owners.size());
|
||||||
|
for (const std::pair<const std::pair<int, int>, BuildingId>& entry : m_owners)
|
||||||
|
{
|
||||||
|
hasher.append(entry.first.first);
|
||||||
|
hasher.append(entry.first.second);
|
||||||
|
hasher.append(entry.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/lib/sim/BuildingGrid.h
Normal file
46
src/lib/sim/BuildingGrid.h
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
|
||||||
|
class Hasher;
|
||||||
|
|
||||||
|
// The authority on which building owns which world tile.
|
||||||
|
//
|
||||||
|
// Every building and construction site claims its body cells here when it is placed
|
||||||
|
// and releases them when it is removed, so the map is the single place that knows
|
||||||
|
// whether a tile is free. It is a plain index owned by BuildingSystem, not a system:
|
||||||
|
// it has no per-tick behaviour and nothing outside BuildingSystem touches it.
|
||||||
|
//
|
||||||
|
// Keys are deliberately std::pair<int, int> rather than QPoint: the checksum folds the
|
||||||
|
// entries in map iteration order (docs/replay_design.md), so the comparator is part of
|
||||||
|
// the determinism contract and is not changed casually.
|
||||||
|
class BuildingGrid
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Records absolute body cells as owned by id. Re-occupying a cell overwrites its
|
||||||
|
// previous owner, matching the placement paths that reserve cells for a site and
|
||||||
|
// then hand them to the building it becomes.
|
||||||
|
void occupy(QPoint cell, BuildingId id);
|
||||||
|
void occupy(const std::vector<QPoint>& cells, BuildingId id);
|
||||||
|
|
||||||
|
// Releases absolute body cells. Cells that are not occupied are ignored.
|
||||||
|
void release(const std::vector<QPoint>& cells);
|
||||||
|
|
||||||
|
bool isOccupied(QPoint tile) const;
|
||||||
|
|
||||||
|
// The building owning the tile, or nullopt when the tile is free.
|
||||||
|
std::optional<BuildingId> findOwner(QPoint tile) const;
|
||||||
|
|
||||||
|
// Folds the occupancy into the hasher in deterministic order.
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::map<std::pair<int, int>, BuildingId> m_owners;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,11 @@
|
|||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "BuildingBuffers.h"
|
||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
#include "ProductionRules.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "BuildingId.h"
|
#include "BuildingId.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
@@ -26,18 +31,6 @@
|
|||||||
|
|
||||||
class Hasher;
|
class Hasher;
|
||||||
|
|
||||||
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
|
|
||||||
// The simulation owns the classification so it stays in sync with the
|
|
||||||
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
|
|
||||||
// color.
|
|
||||||
enum class ProductionStatus
|
|
||||||
{
|
|
||||||
Unconfigured, // no recipe/schematic selected (grey)
|
|
||||||
Producing, // a production cycle is active (green)
|
|
||||||
Starved, // idle: a required input is missing / Salvage Bay empty (red)
|
|
||||||
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
|
|
||||||
};
|
|
||||||
|
|
||||||
// Manages building placement, construction queuing, and the per-tick
|
// Manages building placement, construction queuing, and the per-tick
|
||||||
// production loop (belt→building pull, production, building→belt push).
|
// production loop (belt→building pull, production, building→belt push).
|
||||||
// All types including Belt and Splitter are stored as Building instances;
|
// All types including Belt and Splitter are stored as Building instances;
|
||||||
@@ -61,7 +54,7 @@ public:
|
|||||||
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
|
// queue. Terrain type (A vs S) is NOT checked here so that tests can stage
|
||||||
// arbitrary layouts; the player-facing entry point
|
// arbitrary layouts; the player-facing entry point
|
||||||
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
|
// (Simulation::tryPlaceBuilding) enforces the full rule via isPlacementValid.
|
||||||
std::optional<BuildingId> place(BuildingType type, QPoint anchor, Rotation rotation,
|
std::optional<BuildingId> place(FactoryState& state, BuildingType type, QPoint anchor, Rotation rotation,
|
||||||
Tick currentTick);
|
Tick currentTick);
|
||||||
|
|
||||||
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
|
// Returns true if the placement satisfies REQ-BLD-PLACE-VALID terrain and
|
||||||
@@ -69,13 +62,12 @@ public:
|
|||||||
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
// other body (A) cell sits on the asteroid (x < 0 and x >= the left edge),
|
||||||
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
// and every cell has 0 <= y < world.height_tiles. There is no right-side
|
||||||
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
// bound — space extends rightward. Tile occupancy is NOT checked here.
|
||||||
bool isPlacementValid(BuildingType type, QPoint anchor,
|
|
||||||
Rotation rotation) const;
|
|
||||||
|
|
||||||
// Sets the current buildable asteroid width in tiles. Grows the left
|
// Sets the current buildable asteroid width in tiles. Grows the left
|
||||||
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
// placement bound as the player unlocks asteroid expansions (REQ-EXP-UNLOCK).
|
||||||
// Defaults to world.regions.asteroid_width_tiles at construction.
|
// Defaults to world.regions.asteroid_width_tiles at construction.
|
||||||
void setAsteroidWidth_tiles(int widthTiles) { m_asteroidWidth_tiles = widthTiles; }
|
void setAsteroidWidth_tiles(FactoryState& state, int widthTiles) const
|
||||||
|
{ state.asteroidWidth_tiles = widthTiles; }
|
||||||
|
|
||||||
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
|
// Mark a building or construction site for demolition (REQ-BLD-DECONSTRUCT).
|
||||||
// A construction site is removed instantly and the full cost is returned.
|
// A construction site is removed instantly and the full cost is returned.
|
||||||
@@ -83,24 +75,23 @@ public:
|
|||||||
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
|
// (REQ-BLD-DECON-QUEUE) and stops operating at once; its (partial) refund is
|
||||||
// credited later, on completion in tickDeconstruction, so this returns 0 for
|
// credited later, on completion in tickDeconstruction, so this returns 0 for
|
||||||
// it. Returns 0 for unknown ids and for a building already queued.
|
// it. Returns 0 for unknown ids and for a building already queued.
|
||||||
int deconstruct(BuildingId id, Tick currentTick);
|
int deconstruct(FactoryState& state, BuildingId id, Tick currentTick);
|
||||||
|
|
||||||
// Take a building back out of the deconstruction queue before it is removed
|
// Take a building back out of the deconstruction queue before it is removed
|
||||||
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
|
// (REQ-BLD-DECON-QUEUE). Clears its queued flag and resumes operation
|
||||||
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
|
// (re-registering belt/tunnel/splitter tiles); discards deconstruction
|
||||||
// progress and credits no refund. No-op if the id is not queued.
|
// progress and credits no refund. No-op if the id is not queued.
|
||||||
void cancelDeconstruction(BuildingId id);
|
void cancelDeconstruction(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
// True if the building is currently in the deconstruction queue.
|
// True if the building is currently in the deconstruction queue.
|
||||||
bool isQueuedForDeconstruction(BuildingId id) const;
|
|
||||||
|
|
||||||
// Set the recipe (or schematic id for shipyard) on a building or queued
|
// Set the recipe (or schematic id for shipyard) on a building or queued
|
||||||
// construction site. Clears both buffers on an operational building.
|
// construction site. Clears both buffers on an operational building.
|
||||||
void setRecipe(BuildingId id, const std::string& recipeId);
|
void setRecipe(FactoryState& state, BuildingId id, const std::string& recipeId);
|
||||||
|
|
||||||
// Set the module layout for a shipyard. Cancels in-progress production
|
// Set the module layout for a shipyard. Cancels in-progress production
|
||||||
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
// (materials discarded) and reinitializes input buffers (REQ-BLD-SHIPYARD).
|
||||||
void setShipLayout(BuildingId id, const ShipLayoutConfig& layout);
|
void setShipLayout(FactoryState& state, BuildingId id, const ShipLayoutConfig& layout);
|
||||||
|
|
||||||
// Splitter filter configuration for a queued/under-construction Splitter
|
// Splitter filter configuration for a queued/under-construction Splitter
|
||||||
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
|
// site (REQ-BLD-SITE-CONFIG). Operational splitters are configured through
|
||||||
@@ -109,130 +100,94 @@ public:
|
|||||||
// output directions (derived from its surface mask) and stored filters, or
|
// output directions (derived from its surface mask) and stored filters, or
|
||||||
// nullopt if the id is not a Splitter site. The stored filters are applied
|
// nullopt if the id is not a Splitter site. The stored filters are applied
|
||||||
// to BeltSystem when the splitter finishes building (tickConstruction).
|
// to BeltSystem when the splitter finishes building (tickConstruction).
|
||||||
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(BuildingId id) const;
|
void setSiteSplitterFilters(FactoryState& state, BuildingId id,
|
||||||
void setSiteSplitterFilters(BuildingId id,
|
|
||||||
const std::vector<ItemType>& filterA,
|
const std::vector<ItemType>& filterA,
|
||||||
const std::vector<ItemType>& filterB);
|
const std::vector<ItemType>& filterB);
|
||||||
|
|
||||||
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
// -- Tick hooks (called from Simulation::tick in the documented order) ---
|
||||||
void tickConstruction(Tick currentTick);
|
|
||||||
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
|
// Advances the deconstruction queue (REQ-BLD-DECON-QUEUE): one building at a
|
||||||
// time, in parallel with tickConstruction. Removes the front building and
|
// time, in parallel with tickConstruction. Removes the front building and
|
||||||
// credits its refund when its timer elapses.
|
// credits its refund when its timer elapses.
|
||||||
void tickDeconstruction(Tick currentTick);
|
void tickBeltPull(FactoryState& state);
|
||||||
void tickBeltPull();
|
void tickProduction(FactoryState& state, Tick currentTick);
|
||||||
void tickProduction(Tick currentTick);
|
void tickShipyardProduction(FactoryState& state, Tick currentTick);
|
||||||
void tickShipyardProduction(Tick currentTick);
|
|
||||||
// Advances each building's virtual output belts, hands finished items off onto
|
// Advances each building's virtual output belts, hands finished items off onto
|
||||||
// the adjacent real belt, and feeds new buffered items into them
|
// the adjacent real belt, and feeds new buffered items into them
|
||||||
// (REQ-MAT-OUTPUT-EMERGE).
|
// (REQ-MAT-OUTPUT-EMERGE).
|
||||||
void tickOutputBelts();
|
void tickOutputBelts(FactoryState& state);
|
||||||
|
|
||||||
// -- Queries -------------------------------------------------------------
|
// -- Queries -------------------------------------------------------------
|
||||||
struct BeltTileInfo
|
|
||||||
{
|
|
||||||
BuildingId buildingId;
|
|
||||||
QPoint tile;
|
|
||||||
BuildingType type; // Belt or Splitter
|
|
||||||
Rotation directionA; // Belt: its direction; Splitter: first output
|
|
||||||
Rotation directionB; // Splitter: second output; Belt: same as directionA
|
|
||||||
};
|
|
||||||
|
|
||||||
const Building* findBuilding(BuildingId id) const;
|
|
||||||
const ConstructionSite* findSite(BuildingId id) const;
|
|
||||||
std::vector<Building> getAllBuildings() const;
|
|
||||||
std::vector<ConstructionSite> getAllSites() const;
|
|
||||||
|
|
||||||
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
int getProductionBuildingCount() const;
|
|
||||||
|
|
||||||
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above
|
||||||
// that currently has an active production cycle.
|
// that currently has an active production cycle.
|
||||||
int getActiveProductionBuildingCount() const;
|
|
||||||
|
|
||||||
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
|
// Production state for the UI status light (REQ-UI-STATUS-LIGHT). Returns
|
||||||
// nullopt for building types that show no light (belts, splitters, tunnels,
|
// nullopt for building types that show no light (belts, splitters, tunnels,
|
||||||
// HQ, defence stations). The Salvage Bay is a two-state special case:
|
// HQ, defence stations). The Salvage Bay is a two-state special case:
|
||||||
// Producing while its output buffer holds scrap, Starved when empty.
|
// Producing while its output buffer holds scrap, Starved when empty.
|
||||||
std::optional<ProductionStatus> getProductionStatus(const Building& building) const;
|
|
||||||
std::vector<BeltTileInfo> getAllBeltTiles() const;
|
|
||||||
bool isTileOccupied(QPoint tile) const;
|
|
||||||
|
|
||||||
// Visits every item currently emerging from a building output port on its
|
// Visits every item currently emerging from a building output port on its
|
||||||
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
// virtual output belt (REQ-MAT-OUTPUT-EMERGE), passing the item type and its
|
||||||
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
// world-space centre (in tile units). Least-progressed first (drawn bottom) so
|
||||||
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
// callers can paint in visit order (REQ-GW-TILE-SIZE ordering).
|
||||||
void forEachEmergingItem(
|
void forEachEmergingItem(const FactoryState& state,
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Visits every item currently travelling inward on a building input port's
|
// Visits every item currently travelling inward on a building input port's
|
||||||
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
// virtual input belt (REQ-MAT-INPUT-INTAKE), passing the item type and its
|
||||||
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
// world-space centre (in tile units). Least-progressed first (drawn bottom).
|
||||||
void forEachIncomingItem(
|
void forEachIncomingItem(const FactoryState& state,
|
||||||
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
const std::function<void(const ItemType&, QPointF)>& visit) const;
|
||||||
|
|
||||||
// Returns the entity id of the building or construction site whose footprint
|
|
||||||
// exactly coincides with the ghost (type, anchor, rot) and is of the same
|
|
||||||
// building type. Returns nullopt otherwise.
|
|
||||||
std::optional<BuildingId> findRotateInPlaceTarget(BuildingType type,
|
|
||||||
QPoint anchor,
|
|
||||||
Rotation rot) const;
|
|
||||||
|
|
||||||
// Rotate an existing building or construction site to newRotation in place.
|
// Rotate an existing building or construction site to newRotation in place.
|
||||||
// For belt-type operational buildings, re-registers with BeltSystem (items
|
// For belt-type operational buildings, re-registers with BeltSystem (items
|
||||||
// currently on the tile are discarded by BeltSystem::removeTile).
|
// currently on the tile are discarded by BeltSystem::removeTile).
|
||||||
void rotateInPlace(BuildingId id, Rotation newRotation);
|
void rotateInPlace(FactoryState& state, BuildingId id, Rotation newRotation);
|
||||||
|
|
||||||
// Find nearest operational building of the given type; nullptr if none.
|
|
||||||
const Building* findNearestBuilding(QVector2D worldPos, BuildingType type) const;
|
|
||||||
|
|
||||||
// Input-capable adjacent tiles for a building or construction site
|
// Input-capable adjacent tiles for a building or construction site
|
||||||
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
// (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS): each returned Port.tile is the
|
||||||
// outside adjacent tile and Port.direction is the belt facing that points into
|
// outside adjacent tile and Port.direction is the belt facing that points into
|
||||||
// the target. Output-port edges are excluded. Empty for an unknown id.
|
// the target. Output-port edges are excluded. Empty for an unknown id.
|
||||||
std::vector<Port> getInputPorts(BuildingId id) const;
|
|
||||||
|
|
||||||
// Register / unregister tile occupancy for ECS station entities.
|
// Register / unregister tile occupancy for ECS station entities.
|
||||||
void registerTileOccupancy(const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
void registerTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells, BuildingId ownerPlaceholder);
|
||||||
void unregisterTileOccupancy(const std::vector<QPoint>& cells);
|
void unregisterTileOccupancy(FactoryState& state, const std::vector<QPoint>& cells);
|
||||||
|
|
||||||
// Place one "scrap" item into a SalvageBay's output buffer.
|
// Place one "scrap" item into a SalvageBay's output buffer.
|
||||||
// Returns false if bay not found, wrong type, or output buffer is full.
|
// Returns false if bay not found, wrong type, or output buffer is full.
|
||||||
bool deliverScrapToSalvageBay(BuildingId bayId);
|
|
||||||
|
|
||||||
// Bypass the construction queue and create a fully-operational Building
|
// Bypass the construction queue and create a fully-operational Building
|
||||||
// immediately. Used for pre-placed structures (HQ, defence stations).
|
// immediately. Used for pre-placed structures (HQ, defence stations).
|
||||||
// surfaceMask comes from the relevant config struct.
|
// surfaceMask comes from the relevant config struct.
|
||||||
BuildingId placeImmediate(BuildingType type,
|
BuildingId placeImmediate(FactoryState& state, BuildingType type,
|
||||||
const std::vector<std::string>& surfaceMask,
|
const std::vector<std::string>& surfaceMask,
|
||||||
QPoint anchor, Rotation rotation);
|
QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
// Remove an operational building by id without refund (used for deaths).
|
// Remove an operational building by id without refund (used for deaths).
|
||||||
// Returns true if found and removed.
|
// Returns true if found and removed.
|
||||||
bool removeBuilding(BuildingId id);
|
bool removeBuilding(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
// Mutable iteration over all operational buildings.
|
// Mutable iteration over all operational buildings.
|
||||||
void forEachBuilding(std::function<void(Building&)> fn);
|
void forEachBuilding(FactoryState& state, std::function<void(Building&)> fn);
|
||||||
|
|
||||||
// -- Determinism ---------------------------------------------------------
|
// -- Determinism ---------------------------------------------------------
|
||||||
// Folds all building, construction-site, and tile-occupancy state into the
|
// Folds all building, construction-site, and tile-occupancy state into the
|
||||||
// hasher in deterministic order (see docs/replay_design.md).
|
// hasher in deterministic order (see docs/replay_design.md).
|
||||||
void appendChecksum(Hasher& hasher) const;
|
void appendChecksum(const FactoryState& state, Hasher& hasher) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Starts the front deconstruction-queue entry's timer if not yet started
|
// Starts the front deconstruction-queue entry's timer if not yet started
|
||||||
// (mirrors how tickConstruction starts a queued construction site).
|
// (mirrors how tickConstruction starts a queued construction site).
|
||||||
void startFrontDeconstruction(Tick currentTick);
|
|
||||||
|
|
||||||
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
|
// Registers a belt/splitter/tunnel building's tile with the belt subsystem
|
||||||
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
// (on construction completion, or when un-queuing a deconstruction). No-op for
|
||||||
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
// non-belt-subsystem types. Splitter filters are (re)applied after placement.
|
||||||
void reregisterBeltTile(const Building& building,
|
|
||||||
const std::vector<ItemType>& splitterFilterA,
|
|
||||||
const std::vector<ItemType>& splitterFilterB);
|
|
||||||
|
|
||||||
Building* findBuildingMutable(BuildingId id);
|
|
||||||
// True if the consumer would accept `type` at the given input port right now:
|
// True if the consumer would accept `type` at the given input port right now:
|
||||||
// it is a required input (or a building block for the HQ), the reservation-aware
|
// it is a required input (or a building block for the HQ), the reservation-aware
|
||||||
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
|
// buffer has room, and the input belt entry is free (REQ-MAT-INPUT-INTAKE).
|
||||||
@@ -247,7 +202,7 @@ private:
|
|||||||
// Attempts to hand an emerging output item straight into a directly adjacent
|
// Attempts to hand an emerging output item straight into a directly adjacent
|
||||||
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
// building whose input edge meets the producer's output port (REQ-MAT-DIRECT-COUPLE).
|
||||||
// Returns true if the item was accepted onto the consumer's input belt.
|
// Returns true if the item was accepted onto the consumer's input belt.
|
||||||
bool tryDirectCoupleDeposit(BuildingId producerId,
|
bool tryDirectCoupleDeposit(FactoryState& state, BuildingId producerId,
|
||||||
const Port& outputPort,
|
const Port& outputPort,
|
||||||
const Item& item);
|
const Item& item);
|
||||||
|
|
||||||
@@ -255,39 +210,22 @@ private:
|
|||||||
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
|
// building (Smelter, Reprocessing Plant) offers every recipe of its type with
|
||||||
// inputs; other buildings offer only their selected recipe. Shared by
|
// inputs; other buildings offer only their selected recipe. Shared by
|
||||||
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
|
// tickProduction and the status classifier (REQ-MAT-CYCLE, REQ-UI-STATUS-LIGHT).
|
||||||
std::vector<const RecipeDef*> gatherCandidateRecipes(const Building& b) const;
|
|
||||||
// True if every input of `recipe` is present in `b`'s input buffers in the
|
// True if every input of `recipe` is present in `b`'s input buffers in the
|
||||||
// required per-cycle amount (REQ-MAT-CYCLE input check).
|
// required per-cycle amount (REQ-MAT-CYCLE input check).
|
||||||
bool recipeInputsAvailable(const Building& b,
|
|
||||||
const RecipeDef& recipe) const;
|
|
||||||
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
|
// Combined base + module materials a shipyard needs per ship (REQ-BLD-SHIPYARD).
|
||||||
std::map<std::string, int> computeShipyardRequiredMaterials(const Building& b) const;
|
|
||||||
// True if the building currently has all inputs/materials to start a cycle
|
// True if the building currently has all inputs/materials to start a cycle
|
||||||
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
// (ignoring output-buffer space); drives the Starved/Blocked distinction of
|
||||||
// the status light (REQ-UI-STATUS-LIGHT).
|
// the status light (REQ-UI-STATUS-LIGHT).
|
||||||
bool hasInputsToStart(const Building& b) const;
|
|
||||||
|
|
||||||
const BuildingDef* findBuildingDef(BuildingType type) const;
|
|
||||||
const RecipeDef* findRecipe(const std::string& id, BuildingType type) const;
|
|
||||||
const ShipDef* findShipDef(const std::string& id) const;
|
|
||||||
const ModuleDef* findModuleDef(const std::string& id) const;
|
|
||||||
void initBuffers(Building& b, const RecipeDef& recipe) const;
|
|
||||||
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
// Buffers for an auto-recipe building (Smelter, Reprocessing Plant): input
|
||||||
// caps span the union of every recipe of the building's type; no player
|
// caps span the union of every recipe of the building's type; no player
|
||||||
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
// recipe is selected (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
void initAutoBuffers(Building& b) const;
|
|
||||||
void initShipyardBuffers(Building& b) const;
|
|
||||||
void initSalvageBayBuffer(Building& b) const;
|
|
||||||
std::vector<Port> computeInputPorts(const Building& b) const;
|
|
||||||
// Core input-edge scan shared by operational buildings and construction sites.
|
// Core input-edge scan shared by operational buildings and construction sites.
|
||||||
std::vector<Port> computeInputPorts(const std::vector<QPoint>& bodyCells,
|
|
||||||
const std::vector<Port>& outputPorts) const;
|
|
||||||
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
std::vector<Item> rollReprocessingOutput(const RecipeDef& recipe);
|
||||||
bool bodyCellsWithinWorldBounds(
|
|
||||||
const std::vector<QPoint>& bodyCells,
|
|
||||||
QPoint anchor) const;
|
|
||||||
|
|
||||||
const GameConfig& m_config;
|
const GameConfig& m_config;
|
||||||
|
|
||||||
|
|
||||||
BeltSystem& m_belts;
|
BeltSystem& m_belts;
|
||||||
std::function<BuildingId()> m_allocateBuildingId;
|
std::function<BuildingId()> m_allocateBuildingId;
|
||||||
std::function<void(int)> m_addBuildingBlocks;
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
@@ -295,24 +233,4 @@ private:
|
|||||||
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
const std::optional<ShipLayoutConfig>&)> m_spawnShip;
|
||||||
std::function<bool(const std::string&)> m_isItemUnlocked;
|
std::function<bool(const std::string&)> m_isItemUnlocked;
|
||||||
std::mt19937& m_rng;
|
std::mt19937& m_rng;
|
||||||
int m_asteroidWidth_tiles;
|
|
||||||
|
|
||||||
std::vector<Building> m_buildings;
|
|
||||||
std::deque<ConstructionSite> m_constructionQueue;
|
|
||||||
|
|
||||||
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
|
|
||||||
// completesAt == 0 means "queued but its timer has not started yet"
|
|
||||||
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
|
|
||||||
// here so cancelDeconstruction can restore them on re-registration.
|
|
||||||
struct DeconstructionEntry
|
|
||||||
{
|
|
||||||
BuildingId id = kInvalidBuildingId;
|
|
||||||
Tick completesAt = 0;
|
|
||||||
std::vector<ItemType> splitterFilterA;
|
|
||||||
std::vector<ItemType> splitterFilterB;
|
|
||||||
};
|
|
||||||
std::deque<DeconstructionEntry> m_deconstructionQueue;
|
|
||||||
|
|
||||||
// Maps every occupied body-cell coordinate to the entity that owns it.
|
|
||||||
std::map<std::pair<int, int>, BuildingId> m_tileOccupancy;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,6 +12,14 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
${CMAKE_CURRENT_SOURCE_DIR}/Building.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryState.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipLayout.h
|
||||||
@@ -19,6 +27,7 @@ SET(HDRS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.h
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.h
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.h
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
@@ -35,11 +44,19 @@ SET(SRCS
|
|||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSlot.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BeltSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingConfig.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingGrid.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ConstructionSystem.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/DeconstructionSystem.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingBuffers.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/FactoryQueries.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/ProductionRules.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/PlacementRules.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/BuildingSystem.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/EntityHitTest.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ShipStatsCalculator.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/StateChecksum.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/ThreatCostCalculator.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/UnlockState.cpp
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
${CMAKE_CURRENT_SOURCE_DIR}/WaveSystem.cpp
|
||||||
PARENT_SCOPE
|
PARENT_SCOPE
|
||||||
)
|
)
|
||||||
|
|||||||
112
src/lib/sim/ConstructionSystem.cpp
Normal file
112
src/lib/sim/ConstructionSystem.cpp
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
#include "ConstructionSystem.h"
|
||||||
|
|
||||||
|
#include "BuildingBuffers.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
#include "tracing.h"
|
||||||
|
|
||||||
|
void ConstructionSystem::tick(FactoryState& state, BeltSystem& belts, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
if (state.constructionQueue.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConstructionSite& front = state.constructionQueue.front();
|
||||||
|
|
||||||
|
// Guard: if somehow the front site was never started, start it now.
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
front.completesAt = currentTick + secondsToTicks(def->constructionTimeSeconds);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick < front.completesAt)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Promote construction site to an operational Building.
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(front.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{},
|
||||||
|
front.rotation);
|
||||||
|
|
||||||
|
Building building;
|
||||||
|
building.id = front.id;
|
||||||
|
building.anchor = front.anchor;
|
||||||
|
building.footprint = front.footprint;
|
||||||
|
building.rotation = front.rotation;
|
||||||
|
building.type = front.type;
|
||||||
|
building.recipeId = front.recipeId;
|
||||||
|
building.shipLayout = front.shipLayout;
|
||||||
|
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
building.bodyCells.push_back(front.anchor + cell);
|
||||||
|
}
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
Port absPort;
|
||||||
|
absPort.tile = front.anchor + port.tile;
|
||||||
|
absPort.direction = port.direction;
|
||||||
|
building.outputPorts.push_back(absPort);
|
||||||
|
}
|
||||||
|
building.emergingItems.resize(building.outputPorts.size());
|
||||||
|
building.inputPorts = computeInputPorts(building.bodyCells, building.outputPorts);
|
||||||
|
building.incomingItems.assign(building.inputPorts.size(), {});
|
||||||
|
|
||||||
|
if (building.type == BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
initSalvageBayBuffer(m_config, building);
|
||||||
|
}
|
||||||
|
else if (isAutoRecipeBuildingType(building.type))
|
||||||
|
{
|
||||||
|
// Smelter/Reprocessing Plant need no recipe selection; buffers are set
|
||||||
|
// up from all recipes of the type (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING).
|
||||||
|
initAutoBuffers(m_config, building);
|
||||||
|
}
|
||||||
|
else if (!building.recipeId.empty())
|
||||||
|
{
|
||||||
|
if (building.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
initShipyardBuffers(m_config, building);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = m_config.recipes.findRecipeDef(building.recipeId, building.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
initBuffers(building, *recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register with BeltSystem before the move (mask/building stays valid). Any
|
||||||
|
// filters configured while under construction carry over (REQ-BLD-SITE-CONFIG).
|
||||||
|
reregisterBeltTile(belts, m_config, building, front.splitterFilterA, front.splitterFilterB);
|
||||||
|
|
||||||
|
state.buildings.push_back(std::move(building));
|
||||||
|
|
||||||
|
state.constructionQueue.pop_front();
|
||||||
|
|
||||||
|
// Start next queued site if present.
|
||||||
|
if (!state.constructionQueue.empty() && state.constructionQueue.front().completesAt == 0)
|
||||||
|
{
|
||||||
|
const BuildingDef* nextDef =
|
||||||
|
m_config.buildings.findBuildingDef(state.constructionQueue.front().type);
|
||||||
|
if (nextDef)
|
||||||
|
{
|
||||||
|
state.constructionQueue.front().completesAt =
|
||||||
|
currentTick + secondsToTicks(nextDef->constructionTimeSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
30
src/lib/sim/ConstructionSystem.h
Normal file
30
src/lib/sim/ConstructionSystem.h
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// Advances the construction queue and turns a finished site into an operational
|
||||||
|
// building (REQ-BLD-CONSTRUCTION). One site is built at a time, in queue order:
|
||||||
|
// the front site's timer runs, and when it elapses the site becomes a Building —
|
||||||
|
// its ports and buffers are derived from its definition, its belt tile is handed
|
||||||
|
// back to BeltSystem, and the next queued site starts.
|
||||||
|
//
|
||||||
|
// It completes the building itself rather than handing the finished site back to
|
||||||
|
// BuildingSystem: everything materialisation needs is either in FactoryState, the
|
||||||
|
// config, or a free function (see BuildingBuffers.h, PortGeometry.h), so there is
|
||||||
|
// no intermediate value to pass and no ordering rule between two calls.
|
||||||
|
//
|
||||||
|
// Holds only the config; the world it works on arrives per tick, like the other
|
||||||
|
// systems in lib/ecs/system.
|
||||||
|
class ConstructionSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit ConstructionSystem(const GameConfig& config) : m_config(config) {}
|
||||||
|
|
||||||
|
void tick(FactoryState& state, BeltSystem& belts, Tick currentTick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const GameConfig& m_config;
|
||||||
|
};
|
||||||
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
67
src/lib/sim/DeconstructionSystem.cpp
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "tracing.h"
|
||||||
|
|
||||||
|
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||||
|
Tick currentTick)
|
||||||
|
{
|
||||||
|
if (state.deconstructionQueue.empty()) { return; }
|
||||||
|
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
front.completesAt =
|
||||||
|
currentTick + secondsToTicks(config.world.deconstructionTimeSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void DeconstructionSystem::tick(FactoryState& state, Tick currentTick)
|
||||||
|
{
|
||||||
|
TRACE();
|
||||||
|
if (state.deconstructionQueue.empty())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeconstructionEntry& front = state.deconstructionQueue.front();
|
||||||
|
|
||||||
|
// Guard: if the front entry's timer was never started, start it now.
|
||||||
|
if (front.completesAt == 0)
|
||||||
|
{
|
||||||
|
startFrontDeconstruction(state, m_config, currentTick);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentTick < front.completesAt)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the building from the world and credit its refund (REQ-BLD-DECONSTRUCT).
|
||||||
|
// Belt/tunnel/splitter tiles were already unregistered when the building was
|
||||||
|
// queued (see deconstruct), so only tile occupancy and the record remain.
|
||||||
|
for (std::vector<Building>::iterator it = state.buildings.begin();
|
||||||
|
it != state.buildings.end();
|
||||||
|
++it)
|
||||||
|
{
|
||||||
|
if (it->id != front.id) { continue; }
|
||||||
|
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(it->type);
|
||||||
|
state.grid.release(it->bodyCells);
|
||||||
|
state.buildings.erase(it);
|
||||||
|
if (def)
|
||||||
|
{
|
||||||
|
m_addBuildingBlocks(def->cost * m_config.world.refundPercentage / 100);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.deconstructionQueue.pop_front();
|
||||||
|
|
||||||
|
// Start the next queued deconstruction, if any.
|
||||||
|
startFrontDeconstruction(state, m_config, currentTick);
|
||||||
|
}
|
||||||
|
|
||||||
36
src/lib/sim/DeconstructionSystem.h
Normal file
36
src/lib/sim/DeconstructionSystem.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// The queue timer for pending demolitions (REQ-BLD-DECON-QUEUE): one building at a
|
||||||
|
// time, in parallel with construction. When the front entry's timer elapses the
|
||||||
|
// building is removed from the world, its tiles are released, and its partial refund
|
||||||
|
// is credited.
|
||||||
|
//
|
||||||
|
// It needs no BeltSystem: a belt, splitter or tunnel end is unregistered the moment
|
||||||
|
// it is queued (see BuildingSystem::deconstruct), not when the timer completes.
|
||||||
|
//
|
||||||
|
// Holds the config and the refund sink; the world arrives per tick.
|
||||||
|
class DeconstructionSystem
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
DeconstructionSystem(const GameConfig& config,
|
||||||
|
std::function<void(int)> addBuildingBlocks)
|
||||||
|
: m_config(config), m_addBuildingBlocks(std::move(addBuildingBlocks)) {}
|
||||||
|
|
||||||
|
void tick(FactoryState& state, Tick currentTick);
|
||||||
|
|
||||||
|
private:
|
||||||
|
const GameConfig& m_config;
|
||||||
|
std::function<void(int)> m_addBuildingBlocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Starts the timer on the front entry of the deconstruction queue, if it has one and
|
||||||
|
// it has not started yet. Shared: BuildingSystem::deconstruct starts the timer when it
|
||||||
|
// queues the first entry, and DeconstructionSystem restarts it after each completion.
|
||||||
|
void startFrontDeconstruction(FactoryState& state, const GameConfig& config,
|
||||||
|
Tick currentTick);
|
||||||
181
src/lib/sim/FactoryQueries.cpp
Normal file
181
src/lib/sim/FactoryQueries.cpp
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
#include "PortGeometry.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
|
#include "Item.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
|
||||||
|
const Building* findBuilding(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
if (building.id == id)
|
||||||
|
{
|
||||||
|
return &building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
Building* findBuilding(FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (Building& building : state.buildings)
|
||||||
|
{
|
||||||
|
if (building.id == id)
|
||||||
|
{
|
||||||
|
return &building;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConstructionSite* findSite(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id == id)
|
||||||
|
{
|
||||||
|
return &site;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Building> getAllBuildings(const FactoryState& state)
|
||||||
|
{
|
||||||
|
return state.buildings;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<ConstructionSite> getAllSites(const FactoryState& state)
|
||||||
|
{
|
||||||
|
return std::vector<ConstructionSite>(state.constructionQueue.begin(),
|
||||||
|
state.constructionQueue.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
int getProductionBuildingCount(const FactoryState& state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type)) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getActiveProductionBuildingCount(const FactoryState& state)
|
||||||
|
{
|
||||||
|
int count = 0;
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (isProductionBuildingType(b.type) && b.production.has_value()) { ++count; }
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isTileOccupied(const FactoryState& state, QPoint tile)
|
||||||
|
{
|
||||||
|
return state.grid.isOccupied(tile);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id)
|
||||||
|
{
|
||||||
|
const Building* building = findBuilding(state, id);
|
||||||
|
return building && building->queuedForDeconstruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
|
||||||
|
BuildingType type)
|
||||||
|
{
|
||||||
|
const Building* best = nullptr;
|
||||||
|
float bestDist = std::numeric_limits<float>::max();
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (b.type != type)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
QVector2D center(b.anchor.x() + b.footprint.width() / 2.0f,
|
||||||
|
b.anchor.y() + b.footprint.height() / 2.0f);
|
||||||
|
float dist = (center - worldPos).length();
|
||||||
|
if (dist < bestDist)
|
||||||
|
{
|
||||||
|
bestDist = dist;
|
||||||
|
best = &b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId)
|
||||||
|
{
|
||||||
|
Building* bay = findBuilding(state, bayId);
|
||||||
|
if (!bay || bay->type != BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (bay->queuedForDeconstruction)
|
||||||
|
{
|
||||||
|
return false; // queued for deconstruction: stopped operating (REQ-BLD-DECON-QUEUE)
|
||||||
|
}
|
||||||
|
// Emerging scrap still counts against the bay's holding capacity
|
||||||
|
// (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
if (bay->getOutputItemCount() >= bay->outputBuffer.capacity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
bay->outputBuffer.items.push_back(Item{ItemType{"scrap"}});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id)
|
||||||
|
{
|
||||||
|
if (const Building* building = findBuilding(state, id))
|
||||||
|
{
|
||||||
|
return building->inputPorts;
|
||||||
|
}
|
||||||
|
if (const ConstructionSite* site = findSite(state, id))
|
||||||
|
{
|
||||||
|
// A site stores no ports; derive its output ports from the mask (absolute)
|
||||||
|
// and run the same input-edge scan (REQ-BLD-BELT-DRAG, REQ-MAT-INPUT-PORTS).
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site->type);
|
||||||
|
if (def == nullptr) { return {}; }
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, site->rotation);
|
||||||
|
std::vector<Port> outputPortsAbsolute;
|
||||||
|
outputPortsAbsolute.reserve(mask.outputPorts.size());
|
||||||
|
for (const Port& port : mask.outputPorts)
|
||||||
|
{
|
||||||
|
outputPortsAbsolute.push_back(Port{ site->anchor + port.tile, port.direction });
|
||||||
|
}
|
||||||
|
return computeInputPorts(site->bodyCells, outputPortsAbsolute);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BeltSystem::SplitterInfo>
|
||||||
|
getSiteSplitterInfo(const FactoryState& state, const GameConfig& config, BuildingId id)
|
||||||
|
{
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != id) { continue; }
|
||||||
|
if (site.type != BuildingType::Splitter) { return std::nullopt; }
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(site.type);
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(
|
||||||
|
def ? def->surfaceMask : std::vector<std::string>{}, site.rotation);
|
||||||
|
if (mask.outputPorts.size() < 2) { return std::nullopt; }
|
||||||
|
|
||||||
|
BeltSystem::SplitterInfo info;
|
||||||
|
info.outputA = mask.outputPorts[0].direction;
|
||||||
|
info.outputB = mask.outputPorts[1].direction;
|
||||||
|
info.filterA = site.splitterFilterA;
|
||||||
|
info.filterB = site.splitterFilterB;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
71
src/lib/sim/FactoryQueries.h
Normal file
71
src/lib/sim/FactoryQueries.h
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
#include <QVector2D>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Port.h"
|
||||||
|
|
||||||
|
// Queries and operations over the factory's world data that need nothing but that
|
||||||
|
// data — no config, no belts, no RNG. Free functions rather than BuildingSystem
|
||||||
|
// methods so that callers depend on the data they read instead of on the system
|
||||||
|
// that happens to tick it (see FactoryState.h).
|
||||||
|
//
|
||||||
|
// Most need nothing but the state. The two at the bottom also take the config,
|
||||||
|
// because answering them means reading a building definition — but still no belts,
|
||||||
|
// no RNG and no system.
|
||||||
|
|
||||||
|
// The building with the given id, or nullptr when no building has it. Construction
|
||||||
|
// sites are not buildings yet — use findSite for those.
|
||||||
|
const Building* findBuilding(const FactoryState& state, BuildingId id);
|
||||||
|
Building* findBuilding(FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
// The queued construction site with the given id, or nullptr.
|
||||||
|
const ConstructionSite* findSite(const FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
std::vector<Building> getAllBuildings(const FactoryState& state);
|
||||||
|
std::vector<ConstructionSite> getAllSites(const FactoryState& state);
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Max Factory Production": count of completed
|
||||||
|
// (operational) Miner/Smelter/Assembler/ReprocessingPlant/Shipyard buildings.
|
||||||
|
int getProductionBuildingCount(const FactoryState& state);
|
||||||
|
|
||||||
|
// REQ-UI-DEBUG-OVERLAY "Current Factory Production": subset of the above that
|
||||||
|
// currently has an active production cycle.
|
||||||
|
int getActiveProductionBuildingCount(const FactoryState& state);
|
||||||
|
|
||||||
|
bool isTileOccupied(const FactoryState& state, QPoint tile);
|
||||||
|
|
||||||
|
// True while the building is in the deconstruction queue (REQ-BLD-DECON-QUEUE).
|
||||||
|
bool isQueuedForDeconstruction(const FactoryState& state, BuildingId id);
|
||||||
|
|
||||||
|
// The nearest building of the given type to a world position, or nullptr when
|
||||||
|
// none exists. Distance is measured to the building's footprint centre.
|
||||||
|
const Building* findNearestBuilding(const FactoryState& state, QVector2D worldPos,
|
||||||
|
BuildingType type);
|
||||||
|
|
||||||
|
// Hands one scrap to a Salvage Bay's output buffer (REQ-BLD-SALVAGE-BAY). Fails
|
||||||
|
// if the id is not a Salvage Bay, it is queued for deconstruction
|
||||||
|
// (REQ-BLD-DECON-QUEUE), or its holding capacity is already taken — emerging
|
||||||
|
// scrap counts against that capacity (REQ-MAT-OUTPUT-EMERGE).
|
||||||
|
bool deliverScrapToSalvageBay(FactoryState& state, BuildingId bayId);
|
||||||
|
|
||||||
|
// Every belt-facing edge of the building or site with this id (REQ-MAT-INPUT-PORTS,
|
||||||
|
// REQ-BLD-BELT-DRAG). Empty when the id is unknown. A site has no stored ports, so
|
||||||
|
// they are derived from its surface mask.
|
||||||
|
std::vector<Port> getInputPorts(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
|
|
||||||
|
// The two output directions and stored filters of a queued Splitter site
|
||||||
|
// (REQ-BLD-SITE-CONFIG), or nullopt if the id is not one. Operational splitters are
|
||||||
|
// configured through BeltSystem by tile instead.
|
||||||
|
std::optional<BeltSystem::SplitterInfo> getSiteSplitterInfo(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingId id);
|
||||||
66
src/lib/sim/FactoryState.h
Normal file
66
src/lib/sim/FactoryState.h
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <deque>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "BuildingGrid.h"
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "Tick.h"
|
||||||
|
|
||||||
|
// One pending demolition of a fully-built building (REQ-BLD-DECON-QUEUE).
|
||||||
|
// completesAt == 0 means "queued but its timer has not started yet"
|
||||||
|
// (mirrors ConstructionSite). For a Splitter, the filters it had are captured
|
||||||
|
// here so cancelDeconstruction can restore them on re-registration.
|
||||||
|
struct DeconstructionEntry
|
||||||
|
{
|
||||||
|
BuildingId id = kInvalidBuildingId;
|
||||||
|
Tick completesAt = 0;
|
||||||
|
std::vector<ItemType> splitterFilterA;
|
||||||
|
std::vector<ItemType> splitterFilterB;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The factory's world data: every building, the work queued on them, and the
|
||||||
|
// tile ownership index. This is the buildings-side counterpart to EntityAdmin —
|
||||||
|
// data with no behaviour of its own beyond what BuildingGrid encapsulates.
|
||||||
|
//
|
||||||
|
// Buildings deliberately stay a plain vector rather than becoming EnTT entities
|
||||||
|
// (see docs/architecture.md). Separating this data from the systems that operate
|
||||||
|
// on it is not a step toward putting them in the entity model; it is the same
|
||||||
|
// data/behaviour split the ecs/system/ classes already follow, where world data
|
||||||
|
// arrives as a tick argument instead of being owned by the system.
|
||||||
|
//
|
||||||
|
// Owned by Simulation (and by ArenaSimulation in the balancing tool), not by the
|
||||||
|
// systems that operate on it. BuildingSystem holds a reference. The remaining step
|
||||||
|
// is to pass this into the tick methods instead, so the systems become stateless
|
||||||
|
// over it — that one is gated on the query surface, which today reaches the data
|
||||||
|
// through BuildingSystem's ~180 const call sites.
|
||||||
|
struct FactoryState
|
||||||
|
{
|
||||||
|
std::vector<Building> buildings;
|
||||||
|
std::deque<ConstructionSite> constructionQueue;
|
||||||
|
std::deque<DeconstructionEntry> deconstructionQueue;
|
||||||
|
|
||||||
|
// The authority on which building owns which tile; every placement and removal
|
||||||
|
// path claims and releases its body cells here.
|
||||||
|
BuildingGrid grid;
|
||||||
|
|
||||||
|
// Current buildable asteroid width, the left bound for placement. Grows as the
|
||||||
|
// player buys expansions (REQ-EXP-UNLOCK). Deliberately not checksummed: it is
|
||||||
|
// derived from config and Simulation's expansion count, which is folded already.
|
||||||
|
// Seeded from config by BuildingSystem's constructor.
|
||||||
|
int asteroidWidth_tiles = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// A fresh factory for a new run: nothing built, and the asteroid bound seeded from
|
||||||
|
// config. Every owner of a FactoryState creates it this way — the bound has no
|
||||||
|
// sensible default without the config, so a default-constructed state would refuse
|
||||||
|
// every placement on the asteroid.
|
||||||
|
inline FactoryState makeFactoryState(const GameConfig& config)
|
||||||
|
{
|
||||||
|
FactoryState state;
|
||||||
|
state.asteroidWidth_tiles = config.world.regions.asteroidWidth_tiles;
|
||||||
|
return state;
|
||||||
|
}
|
||||||
121
src/lib/sim/PlacementRules.cpp
Normal file
121
src/lib/sim/PlacementRules.cpp
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "SurfaceMask.h"
|
||||||
|
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,const std::vector<QPoint>& bodyCells,
|
||||||
|
QPoint anchor)
|
||||||
|
{
|
||||||
|
const int heightTiles = config.world.heightTiles;
|
||||||
|
const int leftEdgeX = -state.asteroidWidth_tiles;
|
||||||
|
for (const QPoint& cell : bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
if (worldCell.y() < 0 || worldCell.y() >= heightTiles)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (worldCell.x() < leftEdgeX)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,BuildingType type, QPoint anchor,
|
||||||
|
Rotation rotation)
|
||||||
|
{
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rotation);
|
||||||
|
|
||||||
|
if (!bodyCellsWithinWorldBounds(state, config, mask.bodyCells, anchor))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Terrain: ship-dock (S) cells must sit in space (x >= 0); all other body
|
||||||
|
// (A) cells must sit on the asteroid (x < 0). (REQ-BLD-PLACE-VALID)
|
||||||
|
for (const QPoint& cell : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const QPoint worldCell = anchor + cell;
|
||||||
|
bool isShipDock = false;
|
||||||
|
for (const QPoint& dock : mask.shipDockCells)
|
||||||
|
{
|
||||||
|
if (dock == cell)
|
||||||
|
{
|
||||||
|
isShipDock = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isShipDock)
|
||||||
|
{
|
||||||
|
if (worldCell.x() < 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (worldCell.x() >= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rot)
|
||||||
|
{
|
||||||
|
// Tunnel Entries and Tunnel Exits cannot be rotated in place; re-orienting a
|
||||||
|
// tunnel requires deconstructing and re-placing it (REQ-BLD-ROTATE-IN-PLACE).
|
||||||
|
if (type == BuildingType::TunnelEntry || type == BuildingType::TunnelExit)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BuildingDef* def = config.buildings.findBuildingDef(type);
|
||||||
|
if (!def) { return std::nullopt; }
|
||||||
|
|
||||||
|
const ParsedSurfaceMask mask = parseSurfaceMask(def->surfaceMask, rot);
|
||||||
|
if (mask.bodyCells.empty()) { return std::nullopt; }
|
||||||
|
|
||||||
|
// All body cells must be occupied by the same entity.
|
||||||
|
const QPoint firstAbs = anchor + mask.bodyCells[0];
|
||||||
|
const std::optional<BuildingId> firstOwner = state.grid.findOwner(firstAbs);
|
||||||
|
if (!firstOwner.has_value()) { return std::nullopt; }
|
||||||
|
const BuildingId candidateId = *firstOwner;
|
||||||
|
|
||||||
|
for (const QPoint& rel : mask.bodyCells)
|
||||||
|
{
|
||||||
|
const std::optional<BuildingId> owner = state.grid.findOwner(anchor + rel);
|
||||||
|
if (!owner.has_value() || *owner != candidateId)
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the candidate is the same building type with the same cell count.
|
||||||
|
for (const ConstructionSite& site : state.constructionQueue)
|
||||||
|
{
|
||||||
|
if (site.id != candidateId) { continue; }
|
||||||
|
if (site.type != type) { return std::nullopt; }
|
||||||
|
if (site.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
for (const Building& b : state.buildings)
|
||||||
|
{
|
||||||
|
if (b.id != candidateId) { continue; }
|
||||||
|
if (b.type != type) { return std::nullopt; }
|
||||||
|
if (b.bodyCells.size() != mask.bodyCells.size()) { return std::nullopt; }
|
||||||
|
return candidateId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
38
src/lib/sim/PlacementRules.h
Normal file
38
src/lib/sim/PlacementRules.h
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <QPoint>
|
||||||
|
|
||||||
|
#include "BuildingId.h"
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "Rotation.h"
|
||||||
|
|
||||||
|
// Where a building may be placed, and what is already sitting on those tiles.
|
||||||
|
// Free functions over the factory state and the config — they read no other
|
||||||
|
// system state, so they do not belong to BuildingSystem.
|
||||||
|
|
||||||
|
// True if every body cell lies inside the world: 0 <= y < world.height_tiles and
|
||||||
|
// x >= the current asteroid left edge (REQ-BLD-PLACE-VALID). Terrain type is not
|
||||||
|
// checked — see isPlacementValid for the full rule.
|
||||||
|
bool bodyCellsWithinWorldBounds(const FactoryState& state, const GameConfig& config,
|
||||||
|
const std::vector<QPoint>& bodyCells, QPoint anchor);
|
||||||
|
|
||||||
|
// True if the placement satisfies REQ-BLD-PLACE-VALID terrain and world-bounds
|
||||||
|
// rules: every ship-dock (S) cell sits in space (x >= 0), every other body (A)
|
||||||
|
// cell sits on the asteroid (x < 0 and x >= the left edge), and every cell has
|
||||||
|
// 0 <= y < world.height_tiles. There is no right-side bound — space extends
|
||||||
|
// rightward. Tile occupancy is NOT checked here.
|
||||||
|
bool isPlacementValid(const FactoryState& state, const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor, Rotation rotation);
|
||||||
|
|
||||||
|
// The building or site that a ghost of the given type/anchor/rotation would
|
||||||
|
// rotate in place rather than replace: same type, same body cells, one owner
|
||||||
|
// (REQ-BLD-ROTATE-IN-PLACE). Tunnels never qualify.
|
||||||
|
std::optional<BuildingId> findRotateInPlaceTarget(const FactoryState& state,
|
||||||
|
const GameConfig& config,
|
||||||
|
BuildingType type, QPoint anchor,
|
||||||
|
Rotation rot);
|
||||||
142
src/lib/sim/ProductionRules.cpp
Normal file
142
src/lib/sim/ProductionRules.cpp
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
#include "ProductionRules.h"
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "ItemType.h"
|
||||||
|
#include "ModulesConfig.h"
|
||||||
|
#include "ShipsConfig.h"
|
||||||
|
|
||||||
|
std::vector<const RecipeDef*>
|
||||||
|
gatherCandidateRecipes(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
std::vector<const RecipeDef*> candidates;
|
||||||
|
if (isAutoRecipeBuildingType(b.type))
|
||||||
|
{
|
||||||
|
for (const RecipeDef& r : config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (r.building == b.type && !r.inputs.empty())
|
||||||
|
{
|
||||||
|
candidates.push_back(&r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const RecipeDef* recipe = config.recipes.findRecipeDef(b.recipeId, b.type);
|
||||||
|
if (recipe)
|
||||||
|
{
|
||||||
|
candidates.push_back(recipe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return candidates;
|
||||||
|
}
|
||||||
|
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe)
|
||||||
|
{
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
|
b.inputBuffer.counts.find(ItemType{ing.item});
|
||||||
|
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
|
if (have < ing.amount)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
std::map<std::string, int>
|
||||||
|
computeShipyardRequiredMaterials(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
std::map<std::string, int> requiredMaterials;
|
||||||
|
const ShipDef* shipDef = config.ships.findShipDef(b.recipeId);
|
||||||
|
if (!shipDef)
|
||||||
|
{
|
||||||
|
return requiredMaterials;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : shipDef->schematic.materials)
|
||||||
|
{
|
||||||
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
if (b.shipLayout.has_value())
|
||||||
|
{
|
||||||
|
for (const PlacedModule& pm : b.shipLayout->placedModules)
|
||||||
|
{
|
||||||
|
const ModuleDef* modDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
|
if (!modDef)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : modDef->materials)
|
||||||
|
{
|
||||||
|
requiredMaterials[ing.item] += ing.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return requiredMaterials;
|
||||||
|
}
|
||||||
|
bool hasInputsToStart(const GameConfig& config, const Building& b)
|
||||||
|
{
|
||||||
|
if (b.type == BuildingType::Shipyard)
|
||||||
|
{
|
||||||
|
const std::map<std::string, int> required =
|
||||||
|
computeShipyardRequiredMaterials(config, b);
|
||||||
|
for (const std::pair<const std::string, int>& req : required)
|
||||||
|
{
|
||||||
|
const std::map<ItemType, int>::const_iterator it =
|
||||||
|
b.inputBuffer.counts.find(ItemType{req.first});
|
||||||
|
const int have = (it != b.inputBuffer.counts.end()) ? it->second : 0;
|
||||||
|
if (have < req.second)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipe buildings: startable if any candidate recipe's inputs are satisfied.
|
||||||
|
// A Miner recipe has no inputs, so an idle Miner is always startable and its
|
||||||
|
// only idle reason is a full output buffer.
|
||||||
|
for (const RecipeDef* recipe : gatherCandidateRecipes(config, b))
|
||||||
|
{
|
||||||
|
if (recipeInputsAvailable(b, *recipe))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
std::optional<ProductionStatus>
|
||||||
|
getProductionStatus(const GameConfig& config, const Building& building)
|
||||||
|
{
|
||||||
|
// Salvage Bay has no recipe or production cycle (REQ-BLD-SALVAGE-BAY): it is
|
||||||
|
// "producing" while it holds scrap to push out, and starved when empty.
|
||||||
|
if (building.type == BuildingType::SalvageBay)
|
||||||
|
{
|
||||||
|
return building.getOutputItemCount() >= 1 ? ProductionStatus::Producing
|
||||||
|
: ProductionStatus::Starved;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only the five recipe/cycle production types show a status light besides the
|
||||||
|
// Salvage Bay; belts, splitters, tunnels, HQ, and stations show none.
|
||||||
|
if (!isProductionBuildingType(building.type))
|
||||||
|
{
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grey only applies to player-configured types; auto-recipe buildings
|
||||||
|
// (Smelter, Reprocessing Plant) always run an implicit recipe.
|
||||||
|
if (!isAutoRecipeBuildingType(building.type) && building.recipeId.empty())
|
||||||
|
{
|
||||||
|
return ProductionStatus::Unconfigured;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (building.production.has_value())
|
||||||
|
{
|
||||||
|
return ProductionStatus::Producing;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle: missing inputs (red) take precedence over a full output buffer
|
||||||
|
// (yellow). If inputs are present yet the building is idle, the only remaining
|
||||||
|
// reason it could not start a cycle is a full output buffer (REQ-MAT-CYCLE).
|
||||||
|
return hasInputsToStart(config, building) ? ProductionStatus::Blocked
|
||||||
|
: ProductionStatus::Starved;
|
||||||
|
}
|
||||||
47
src/lib/sim/ProductionRules.h
Normal file
47
src/lib/sim/ProductionRules.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "Building.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "RecipesConfig.h"
|
||||||
|
|
||||||
|
// Production state of a building for the UI status light (REQ-UI-STATUS-LIGHT).
|
||||||
|
// The simulation owns the classification so it stays in sync with the
|
||||||
|
// production-cycle predicates (REQ-MAT-CYCLE); the UI maps each value to a fill
|
||||||
|
// color.
|
||||||
|
enum class ProductionStatus
|
||||||
|
{
|
||||||
|
Unconfigured, // no recipe/schematic selected (grey)
|
||||||
|
Producing, // a production cycle is active (green)
|
||||||
|
Starved, // idle: a required input is missing / Salvage Bay empty (red)
|
||||||
|
Blocked, // idle: output buffer full, inputs otherwise present (yellow)
|
||||||
|
};
|
||||||
|
|
||||||
|
// The rules deciding what a building can produce and whether it can start.
|
||||||
|
// Pure functions of the config and the building itself — they read no factory
|
||||||
|
// state, so they are free functions rather than BuildingSystem members.
|
||||||
|
|
||||||
|
// Recipes this building could run: every recipe of its type for an auto-recipe
|
||||||
|
// building (REQ-BLD-SMELTER, REQ-BLD-REPROCESSING), otherwise just its selected one.
|
||||||
|
std::vector<const RecipeDef*> gatherCandidateRecipes(const GameConfig& config,
|
||||||
|
const Building& b);
|
||||||
|
|
||||||
|
// True when the building's input buffer holds every ingredient the recipe needs.
|
||||||
|
bool recipeInputsAvailable(const Building& b, const RecipeDef& recipe);
|
||||||
|
|
||||||
|
// Total materials a shipyard needs for its schematic plus its placed modules
|
||||||
|
// (REQ-BLD-SHIPYARD), keyed by item id.
|
||||||
|
std::map<std::string, int> computeShipyardRequiredMaterials(const GameConfig& config,
|
||||||
|
const Building& b);
|
||||||
|
|
||||||
|
// True when a production cycle could start right now, ignoring output-buffer space.
|
||||||
|
bool hasInputsToStart(const GameConfig& config, const Building& b);
|
||||||
|
|
||||||
|
// Status light for a building, or nullopt for types that show none — belts,
|
||||||
|
// splitters, tunnels, HQ and defence stations (REQ-UI-STATUS-LIGHT).
|
||||||
|
std::optional<ProductionStatus> getProductionStatus(const GameConfig& config,
|
||||||
|
const Building& building);
|
||||||
@@ -21,22 +21,9 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
{
|
{
|
||||||
ShipStats result{};
|
ShipStats result{};
|
||||||
|
|
||||||
const ShipDef* shipDef = nullptr;
|
const ShipDef* shipDef = config.ships.findShipDef(shipId);
|
||||||
for (const ShipDef& d : config.ships.ships)
|
|
||||||
{
|
|
||||||
if (d.id == shipId) { shipDef = &d; break; }
|
|
||||||
}
|
|
||||||
if (!shipDef) { return result; }
|
if (!shipDef) { return result; }
|
||||||
|
|
||||||
auto findModuleDef = [&](const std::string& id) -> const ModuleDef*
|
|
||||||
{
|
|
||||||
for (const ModuleDef& d : config.modules.modules)
|
|
||||||
{
|
|
||||||
if (d.id == id) { return &d; }
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
const double tileSize = config.world.tileSize_m;
|
const double tileSize = config.world.tileSize_m;
|
||||||
|
|
||||||
// --- Base hull stats (convert from SI to display units) ------------------
|
// --- Base hull stats (convert from SI to display units) ------------------
|
||||||
@@ -67,7 +54,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
|
|
||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = config.modules.findModuleDef(pm.moduleId);
|
||||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
if (def->weaponCapability)
|
if (def->weaponCapability)
|
||||||
@@ -107,7 +94,7 @@ ShipStats calculateShipStats(const GameConfig& config,
|
|||||||
|
|
||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* def = findModuleDef(pm.moduleId);
|
const ModuleDef* def = config.modules.findModuleDef(pm.moduleId);
|
||||||
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
if (!def) { throw std::runtime_error("unknown module id '" + pm.moduleId + "'"); }
|
||||||
|
|
||||||
for (const ModuleStatModifier& sm : def->statModifiers)
|
for (const ModuleStatModifier& sm : def->statModifiers)
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
|
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
#include "ConstructionSystem.h"
|
||||||
|
#include "DeconstructionSystem.h"
|
||||||
|
#include "PlacementRules.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
#include "AiSystem.h"
|
#include "AiSystem.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "DisplayName.h"
|
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "DynamicBodyComponent.h"
|
#include "DynamicBodyComponent.h"
|
||||||
@@ -43,39 +47,16 @@ Simulation::Simulation(GameConfig config, unsigned int seed)
|
|||||||
, m_hqProxyEntity(entt::null)
|
, m_hqProxyEntity(entt::null)
|
||||||
, m_playerStation1Entity(entt::null)
|
, m_playerStation1Entity(entt::null)
|
||||||
, m_playerStation2Entity(entt::null)
|
, m_playerStation2Entity(entt::null)
|
||||||
|
, m_unlockState(m_config)
|
||||||
, m_beltSystem(m_config.world.beltSpeed_tps)
|
, m_beltSystem(m_config.world.beltSpeed_tps)
|
||||||
{
|
{
|
||||||
m_currentEnemyStationEntities[0] = entt::null;
|
m_currentEnemyStationEntities[0] = entt::null;
|
||||||
m_currentEnemyStationEntities[1] = entt::null;
|
m_currentEnemyStationEntities[1] = entt::null;
|
||||||
|
m_factoryState = makeFactoryState(m_config);
|
||||||
|
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
initializeSubsystems();
|
||||||
m_config,
|
|
||||||
m_beltSystem,
|
|
||||||
[this]() { return allocateBuildingId(); },
|
|
||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
|
||||||
[this](const std::string& id, QVector2D pos,
|
|
||||||
const std::optional<ShipLayoutConfig>& layout) {
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_schematicLevels.find(id);
|
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
m_shipSystem->spawn(id, pos, /*isEnemy=*/false, layout);
|
|
||||||
},
|
|
||||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
|
||||||
m_rng);
|
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
|
||||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
|
||||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
|
||||||
m_dynamicBodySystem = std::make_unique<DynamicBodySystem>();
|
|
||||||
m_debrisSystem = std::make_unique<DebrisSystem>(m_admin);
|
|
||||||
m_salvagerSystem = std::make_unique<SalvagerSystem>(m_admin);
|
|
||||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
|
||||||
|
|
||||||
initializeUnlockState();
|
m_unlockState.initializeUnlockState();
|
||||||
placeInitialStructures();
|
placeInitialStructures();
|
||||||
registerForEvents();
|
registerForEvents();
|
||||||
}
|
}
|
||||||
@@ -119,7 +100,16 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_pendingSchematicChoices.clear();
|
m_pendingSchematicChoices.clear();
|
||||||
|
|
||||||
m_admin.clear();
|
m_admin.clear();
|
||||||
|
m_factoryState = makeFactoryState(m_config);
|
||||||
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
m_beltSystem = BeltSystem(m_config.world.beltSpeed_tps);
|
||||||
|
initializeSubsystems();
|
||||||
|
|
||||||
|
m_unlockState.initializeUnlockState();
|
||||||
|
placeInitialStructures();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Simulation::initializeSubsystems()
|
||||||
|
{
|
||||||
m_buildingSystem = std::make_unique<BuildingSystem>(
|
m_buildingSystem = std::make_unique<BuildingSystem>(
|
||||||
m_config,
|
m_config,
|
||||||
m_beltSystem,
|
m_beltSystem,
|
||||||
@@ -127,9 +117,7 @@ void Simulation::reset(unsigned int seed)
|
|||||||
[this](int amount) { m_buildingBlocksStock += amount; },
|
[this](int amount) { m_buildingBlocksStock += amount; },
|
||||||
[this](const std::string& id, QVector2D pos,
|
[this](const std::string& id, QVector2D pos,
|
||||||
const std::optional<ShipLayoutConfig>& layout) {
|
const std::optional<ShipLayoutConfig>& layout) {
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
if (!isSchematicUnlocked(id))
|
||||||
m_schematicLevels.find(id);
|
|
||||||
if (it == m_schematicLevels.end() || !it->second.unlocked)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -137,6 +125,9 @@ void Simulation::reset(unsigned int seed)
|
|||||||
},
|
},
|
||||||
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
[this](const std::string& itemId) -> bool { return isItemUnlocked(itemId); },
|
||||||
m_rng);
|
m_rng);
|
||||||
|
m_constructionSystem = std::make_unique<ConstructionSystem>(m_config);
|
||||||
|
m_deconstructionSystem = std::make_unique<DeconstructionSystem>(
|
||||||
|
m_config, [this](int amount) { m_buildingBlocksStock += amount; });
|
||||||
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
m_shipSystem = std::make_unique<ShipSystem>(m_config, m_admin);
|
||||||
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
m_aiSystem = std::make_unique<AiSystem>(m_config);
|
||||||
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
m_movementIntentSystem = std::make_unique<MovementIntentSystem>();
|
||||||
@@ -146,58 +137,6 @@ void Simulation::reset(unsigned int seed)
|
|||||||
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
m_repairSystem = std::make_unique<RepairSystem>(m_admin);
|
||||||
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
m_waveSystem = std::make_unique<WaveSystem>(m_config, m_rng);
|
||||||
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
m_combatSystem = std::make_unique<CombatSystem>(m_config);
|
||||||
|
|
||||||
initializeUnlockState();
|
|
||||||
placeInitialStructures();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::initializeUnlockState()
|
|
||||||
{
|
|
||||||
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
|
||||||
// starts locked iff it is granted by a group.
|
|
||||||
m_grantedShipIds.clear();
|
|
||||||
m_grantedModuleIds.clear();
|
|
||||||
m_grantedBuildingIds.clear();
|
|
||||||
m_grantedRecipeIds.clear();
|
|
||||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
|
||||||
{
|
|
||||||
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
|
||||||
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
|
||||||
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
|
||||||
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
|
||||||
}
|
|
||||||
|
|
||||||
m_awardedUnlockGroupIds.clear();
|
|
||||||
|
|
||||||
m_schematicLevels.clear();
|
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
|
||||||
m_schematicLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_moduleSchematicLevels.clear();
|
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
|
||||||
m_moduleSchematicLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_buildingLevels.clear();
|
|
||||||
for (const BuildingDef& def : m_config.buildings.buildings)
|
|
||||||
{
|
|
||||||
SchematicState state;
|
|
||||||
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
|
||||||
m_buildingLevels[def.id] = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
|
||||||
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
|
||||||
m_unlockedRecipeSchematicIds.clear();
|
|
||||||
|
|
||||||
recomputeUnlocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -219,15 +158,15 @@ void Simulation::apply(const Command& command)
|
|||||||
const BuildingId id = *placed;
|
const BuildingId id = *placed;
|
||||||
if (c.recipeId.has_value())
|
if (c.recipeId.has_value())
|
||||||
{
|
{
|
||||||
m_buildingSystem->setRecipe(id, *c.recipeId);
|
m_buildingSystem->setRecipe(m_factoryState, id, *c.recipeId);
|
||||||
}
|
}
|
||||||
if (c.shipLayout.has_value())
|
if (c.shipLayout.has_value())
|
||||||
{
|
{
|
||||||
m_buildingSystem->setShipLayout(id, *c.shipLayout);
|
m_buildingSystem->setShipLayout(m_factoryState, id, *c.shipLayout);
|
||||||
}
|
}
|
||||||
if (c.hasSplitterFilters)
|
if (c.hasSplitterFilters)
|
||||||
{
|
{
|
||||||
m_buildingSystem->setSiteSplitterFilters(id, c.splitterFilterA, c.splitterFilterB);
|
m_buildingSystem->setSiteSplitterFilters(m_factoryState, id, c.splitterFilterA, c.splitterFilterB);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -240,26 +179,26 @@ void Simulation::apply(const Command& command)
|
|||||||
case CommandKind::RotateInPlace:
|
case CommandKind::RotateInPlace:
|
||||||
{
|
{
|
||||||
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
const RotateInPlaceCommand& c = static_cast<const RotateInPlaceCommand&>(command);
|
||||||
m_buildingSystem->rotateInPlace(*c.id, c.newRotation);
|
m_buildingSystem->rotateInPlace(m_factoryState, *c.id, c.newRotation);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetRecipe:
|
case CommandKind::SetRecipe:
|
||||||
{
|
{
|
||||||
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
const SetRecipeCommand& c = static_cast<const SetRecipeCommand&>(command);
|
||||||
m_buildingSystem->setRecipe(*c.id, c.recipeId);
|
m_buildingSystem->setRecipe(m_factoryState, *c.id, c.recipeId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetShipLayout:
|
case CommandKind::SetShipLayout:
|
||||||
{
|
{
|
||||||
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
const SetShipLayoutCommand& c = static_cast<const SetShipLayoutCommand&>(command);
|
||||||
m_buildingSystem->setShipLayout(*c.id, c.layout);
|
m_buildingSystem->setShipLayout(m_factoryState, *c.id, c.layout);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetSiteSplitterFilters:
|
case CommandKind::SetSiteSplitterFilters:
|
||||||
{
|
{
|
||||||
const SetSiteSplitterFiltersCommand& c =
|
const SetSiteSplitterFiltersCommand& c =
|
||||||
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
static_cast<const SetSiteSplitterFiltersCommand&>(command);
|
||||||
m_buildingSystem->setSiteSplitterFilters(*c.id, c.filterA, c.filterB);
|
m_buildingSystem->setSiteSplitterFilters(m_factoryState, *c.id, c.filterA, c.filterB);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case CommandKind::SetSplitterFilters:
|
case CommandKind::SetSplitterFilters:
|
||||||
@@ -309,12 +248,12 @@ void Simulation::tick()
|
|||||||
m_waveSystem->tickThreatAccumulation();
|
m_waveSystem->tickThreatAccumulation();
|
||||||
|
|
||||||
// Construction + production pipeline
|
// Construction + production pipeline
|
||||||
m_buildingSystem->tickConstruction(m_currentTick);
|
m_constructionSystem->tick(m_factoryState, m_beltSystem, m_currentTick);
|
||||||
m_buildingSystem->tickDeconstruction(m_currentTick); // parallel to construction
|
m_deconstructionSystem->tick(m_factoryState, m_currentTick); // parallel to construction
|
||||||
m_buildingSystem->tickBeltPull(); // step 3
|
m_buildingSystem->tickBeltPull(m_factoryState); // step 3
|
||||||
m_buildingSystem->tickProduction(m_currentTick); // step 4
|
m_buildingSystem->tickProduction(m_factoryState, m_currentTick); // step 4
|
||||||
m_buildingSystem->tickShipyardProduction(m_currentTick); // step 4b
|
m_buildingSystem->tickShipyardProduction(m_factoryState, m_currentTick); // step 4b
|
||||||
m_buildingSystem->tickOutputBelts(); // step 5
|
m_buildingSystem->tickOutputBelts(m_factoryState); // step 5
|
||||||
m_beltSystem.tick(); // step 6
|
m_beltSystem.tick(); // step 6
|
||||||
|
|
||||||
// Step 7: ship behavior systems (movement arbitration via intent priority)
|
// Step 7: ship behavior systems (movement arbitration via intent priority)
|
||||||
@@ -329,15 +268,14 @@ void Simulation::tick()
|
|||||||
m_shipSystem->clearMovementIntents();
|
m_shipSystem->clearMovementIntents();
|
||||||
// Score-based behavior selection: evaluate, select winner, execute (sets
|
// Score-based behavior selection: evaluate, select winner, execute (sets
|
||||||
// movement intent + preferred module targets only — no world mutation).
|
// movement intent + preferred module targets only — no world mutation).
|
||||||
m_aiSystem->tick(m_admin, *m_buildingSystem, *m_debrisSystem);
|
m_aiSystem->tick(m_admin, m_factoryState);
|
||||||
// Module systems perform the world mutation (collection/delivery, healing).
|
// Module systems perform the world mutation (collection/delivery, healing).
|
||||||
// Each emits its tool beams and applies its own delayed (mid-beam) effects.
|
// Each emits its tool beams and applies its own delayed (mid-beam) effects.
|
||||||
m_salvagerSystem->tick(m_currentTick, *m_debrisSystem, *m_buildingSystem, m_beamFiredEvents);
|
m_salvagerSystem->tick(m_currentTick, m_factoryState, m_beamFiredEvents);
|
||||||
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
m_repairSystem->tick(m_currentTick, m_beamFiredEvents);
|
||||||
|
|
||||||
// Step 8: combat resolution
|
// Step 8: combat resolution
|
||||||
m_combatSystem->tick(m_currentTick, m_admin,
|
m_combatSystem->tick(m_currentTick, m_admin, m_beamFiredEvents);
|
||||||
*m_buildingSystem, m_beamFiredEvents);
|
|
||||||
|
|
||||||
// Step 8b: deferred damage whose impact tick has arrived
|
// Step 8b: deferred damage whose impact tick has arrived
|
||||||
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
m_combatSystem->applyPendingDamage(m_currentTick, m_admin);
|
||||||
@@ -373,8 +311,7 @@ void Simulation::placeInitialStructures()
|
|||||||
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
|
(m_config.world.heightTiles - hqParsed.footprint.height()) / 2;
|
||||||
const float hqHp =
|
const float hqHp =
|
||||||
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
|
static_cast<float>(m_config.stations.hq.hpFormula.evaluate(0.0));
|
||||||
m_hqBuildingId = m_buildingSystem->placeImmediate(
|
m_hqBuildingId = m_buildingSystem->placeImmediate(m_factoryState, BuildingType::Hq,
|
||||||
BuildingType::Hq,
|
|
||||||
m_config.stations.hq.surfaceMask,
|
m_config.stations.hq.surfaceMask,
|
||||||
QPoint(hqAnchorX, hqAnchorY),
|
QPoint(hqAnchorX, hqAnchorY),
|
||||||
Rotation::East);
|
Rotation::East);
|
||||||
@@ -423,7 +360,7 @@ void Simulation::placeInitialStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_playerStation1Entity});
|
ModuleOwnerComponent{m_playerStation1Entity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const QPoint anchor(psAnchorX, ps2Y);
|
const QPoint anchor(psAnchorX, ps2Y);
|
||||||
@@ -440,7 +377,7 @@ void Simulation::placeInitialStructures()
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_playerStation2Entity});
|
ModuleOwnerComponent{m_playerStation2Entity});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
// Rally point: center of the player defence stations' X column, world vertical midpoint.
|
||||||
@@ -495,7 +432,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
ModuleOwnerComponent{m_currentEnemyStationEntities[0]});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
const QPoint anchor(anchorX, y2);
|
const QPoint anchor(anchorX, y2);
|
||||||
@@ -512,7 +449,7 @@ void Simulation::placeEnemyStationSet(int generation)
|
|||||||
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
m_admin.addComponent<ModuleOwnerComponent>(wChild,
|
||||||
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
ModuleOwnerComponent{m_currentEnemyStationEntities[1]});
|
||||||
}
|
}
|
||||||
m_buildingSystem->registerTileOccupancy(absCells, allocateBuildingId());
|
m_buildingSystem->registerTileOccupancy(m_factoryState, absCells, allocateBuildingId());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -586,7 +523,7 @@ void Simulation::tickDeathsAndLoot()
|
|||||||
{
|
{
|
||||||
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
m_debrisSystem->spawn(pos.value, scrap, despawnAt);
|
||||||
}
|
}
|
||||||
m_buildingSystem->unregisterTileOccupancy(sb.bodyCells);
|
m_buildingSystem->unregisterTileOccupancy(m_factoryState, sb.bodyCells);
|
||||||
{
|
{
|
||||||
std::vector<entt::entity> stationChildren;
|
std::vector<entt::entity> stationChildren;
|
||||||
m_admin.forEach<ModuleOwnerComponent>(
|
m_admin.forEach<ModuleOwnerComponent>(
|
||||||
@@ -633,9 +570,9 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
std::vector<const UnlockGroupDef*> pool;
|
std::vector<const UnlockGroupDef*> pool;
|
||||||
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||||
{
|
{
|
||||||
if (m_awardedUnlockGroupIds.count(group.id) > 0) { continue; }
|
if (m_unlockState.isUnlockGroupAwarded(group.id)) { continue; }
|
||||||
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
if (group.stationLevel < 0 || group.stationLevel > destroyedStationLevel) { continue; }
|
||||||
if (!prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
if (!m_unlockState.prerequisitesSatisfied(group.requiredGroupIds)) { continue; }
|
||||||
pool.push_back(&group);
|
pool.push_back(&group);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -659,7 +596,7 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
const std::size_t endIdx = pool.size() - 1 - static_cast<std::size_t>(i);
|
||||||
std::swap(pool[rollIdx], pool[endIdx]);
|
std::swap(pool[rollIdx], pool[endIdx]);
|
||||||
|
|
||||||
m_pendingSchematicChoices.push_back(makeUnlockOption(*pool[endIdx]));
|
m_pendingSchematicChoices.push_back(m_unlockState.makeUnlockOption(*pool[endIdx]));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (artifactRolled)
|
if (artifactRolled)
|
||||||
@@ -671,48 +608,6 @@ void Simulation::generateSchematicChoices(int destroyedStationLevel)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SchematicChoiceOption Simulation::makeUnlockOption(const UnlockGroupDef& group) const
|
|
||||||
{
|
|
||||||
SchematicChoiceOption option;
|
|
||||||
option.isArtifact = false;
|
|
||||||
option.unlockGroupId = group.id;
|
|
||||||
option.displayName = toDisplayName(group.id);
|
|
||||||
|
|
||||||
for (const std::string& id : group.ships)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.modules)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.buildings)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
for (const std::string& id : group.recipes)
|
|
||||||
{
|
|
||||||
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
|
||||||
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
|
||||||
// every grant (ship + module materials via step 1a, recipe outputs via step
|
|
||||||
// 1b), then diff against the current implicit set.
|
|
||||||
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
|
||||||
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
|
||||||
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
|
||||||
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
|
||||||
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
|
||||||
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
|
||||||
|
|
||||||
const UnlockedSets hypothetical = computeUnlockedSets(
|
|
||||||
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
|
||||||
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
|
||||||
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::applySchematicChoice(int choiceIndex)
|
void Simulation::applySchematicChoice(int choiceIndex)
|
||||||
{
|
{
|
||||||
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
assert(choiceIndex >= 0 && choiceIndex < static_cast<int>(m_pendingSchematicChoices.size()));
|
||||||
@@ -729,204 +624,24 @@ void Simulation::applySchematicChoice(int choiceIndex)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
m_unlockState.awardUnlockGroup(chosen);
|
||||||
// ship, module, building, and assembler recipe at once.
|
|
||||||
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
|
||||||
for (const GrantedSchematic& grant : chosen.grantedItems)
|
|
||||||
{
|
|
||||||
switch (grant.type)
|
|
||||||
{
|
|
||||||
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
|
||||||
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
recomputeUnlocked();
|
|
||||||
m_pendingSchematicChoices.clear();
|
m_pendingSchematicChoices.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
void Simulation::recomputeUnlocked()
|
|
||||||
{
|
|
||||||
const UnlockedSets result = computeUnlockedSets(
|
|
||||||
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
|
||||||
m_unlockedItemIds = result.itemIds;
|
|
||||||
m_unlockedRecipeIds = result.recipeIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> Simulation::getUnlockedShipSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_schematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::set<std::string> Simulation::getUnlockedModuleSchematicIds() const
|
|
||||||
{
|
|
||||||
std::set<std::string> ids;
|
|
||||||
for (const auto& [id, state] : m_moduleSchematicLevels)
|
|
||||||
{
|
|
||||||
if (state.unlocked) { ids.insert(id); }
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
|
||||||
{
|
|
||||||
// A prerequisite is satisfied only once the named unlock group has been
|
|
||||||
// awarded (REQ-LOCK-PREREQ).
|
|
||||||
for (const std::string& groupId : requiredGroupIds)
|
|
||||||
{
|
|
||||||
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Simulation::UnlockedSets Simulation::computeUnlockedSets(
|
|
||||||
const std::set<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
|
||||||
{
|
|
||||||
UnlockedSets result;
|
|
||||||
|
|
||||||
for (const ShipDef& def : m_config.ships.ships)
|
|
||||||
{
|
|
||||||
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
|
||||||
for (const RecipeIngredient& mat : def.schematic.materials)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(mat.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const ModuleDef& def : m_config.modules.modules)
|
|
||||||
{
|
|
||||||
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
|
||||||
for (const RecipeIngredient& mat : def.materials)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(mat.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const RecipeDef& def : m_config.recipes.recipes)
|
|
||||||
{
|
|
||||||
// An assembler recipe seeds the base set when it is explicitly available:
|
|
||||||
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
|
||||||
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
|
||||||
if (def.building == BuildingType::Assembler
|
|
||||||
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
|
||||||
{
|
|
||||||
for (const RecipeOutput& out : def.outputs)
|
|
||||||
{
|
|
||||||
result.itemIds.insert(out.item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool changed = true;
|
|
||||||
while (changed)
|
|
||||||
{
|
|
||||||
changed = false;
|
|
||||||
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
|
||||||
{
|
|
||||||
if (recipe.building != BuildingType::Miner
|
|
||||||
&& recipe.building != BuildingType::Smelter
|
|
||||||
&& recipe.building != BuildingType::Assembler)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Skip a gated assembler recipe (granted by an unlock group) whose
|
|
||||||
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
|
||||||
if (recipe.building == BuildingType::Assembler
|
|
||||||
&& m_grantedRecipeIds.count(recipe.id) > 0
|
|
||||||
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
bool producesUnlocked = false;
|
|
||||||
for (const RecipeOutput& out : recipe.outputs)
|
|
||||||
{
|
|
||||||
if (result.itemIds.count(out.item) > 0)
|
|
||||||
{
|
|
||||||
producesUnlocked = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!producesUnlocked) { continue; }
|
|
||||||
|
|
||||||
if (recipe.building == BuildingType::Miner
|
|
||||||
|| recipe.building == BuildingType::Assembler)
|
|
||||||
{
|
|
||||||
result.recipeIds.insert(recipe.id);
|
|
||||||
}
|
|
||||||
for (const RecipeIngredient& ing : recipe.inputs)
|
|
||||||
{
|
|
||||||
if (result.itemIds.insert(ing.item).second)
|
|
||||||
{
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> Simulation::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
|
||||||
{
|
|
||||||
std::vector<std::string> recipeIds;
|
|
||||||
for (const std::string& recipeId : hypothetical.recipeIds)
|
|
||||||
{
|
|
||||||
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
|
||||||
recipeIds.push_back(recipeId);
|
|
||||||
}
|
|
||||||
std::sort(recipeIds.begin(), recipeIds.end(),
|
|
||||||
[](const std::string& lhs, const std::string& rhs)
|
|
||||||
{
|
|
||||||
return toDisplayName(lhs) < toDisplayName(rhs);
|
|
||||||
});
|
|
||||||
return recipeIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
bool Simulation::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
{
|
{
|
||||||
return m_unlockedRecipeIds.count(recipeId) > 0;
|
return m_unlockState.isRecipeUnlocked(recipeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
bool Simulation::isItemUnlocked(const std::string& itemId) const
|
||||||
{
|
{
|
||||||
return m_unlockedItemIds.count(itemId) > 0;
|
return m_unlockState.isItemUnlocked(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Determinism (see docs/replay_design.md)
|
// Determinism (see docs/replay_design.md)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
void Simulation::appendSchematicMap(Hasher& hasher,
|
|
||||||
const std::map<std::string, SchematicState>& levels)
|
|
||||||
{
|
|
||||||
hasher.append(levels.size());
|
|
||||||
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
|
||||||
{
|
|
||||||
hasher.append(entry.first);
|
|
||||||
hasher.append(entry.second.unlocked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void Simulation::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
|
||||||
{
|
|
||||||
hasher.append(ids.size());
|
|
||||||
for (const std::string& id : ids)
|
|
||||||
{
|
|
||||||
hasher.append(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
unsigned long long Simulation::getRngFingerprint() const
|
unsigned long long Simulation::getRngFingerprint() const
|
||||||
{
|
{
|
||||||
return fingerprintRng(m_rng);
|
return fingerprintRng(m_rng);
|
||||||
@@ -957,16 +672,10 @@ unsigned long long Simulation::computeStateChecksum() const
|
|||||||
hasher.append(getNormalGapRemainingTicks());
|
hasher.append(getNormalGapRemainingTicks());
|
||||||
|
|
||||||
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
// Schematic / unlock state (std::map and std::set iterate in sorted order).
|
||||||
appendSchematicMap(hasher, m_schematicLevels);
|
m_unlockState.appendChecksum(hasher);
|
||||||
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
|
||||||
appendSchematicMap(hasher, m_buildingLevels);
|
|
||||||
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
|
||||||
appendStringSet(hasher, m_unlockedRecipeIds);
|
|
||||||
appendStringSet(hasher, m_unlockedItemIds);
|
|
||||||
|
|
||||||
// Subsystems contribute their own state.
|
// Subsystems contribute their own state.
|
||||||
m_buildingSystem->appendChecksum(hasher);
|
m_buildingSystem->appendChecksum(m_factoryState, hasher);
|
||||||
m_beltSystem.appendChecksum(hasher);
|
m_beltSystem.appendChecksum(hasher);
|
||||||
|
|
||||||
// ECS component state. View iteration order is a pure function of the
|
// ECS component state. View iteration order is a pure function of the
|
||||||
@@ -1079,7 +788,7 @@ void Simulation::tryExpandAsteroid()
|
|||||||
}
|
}
|
||||||
m_buildingBlocksStock -= cost;
|
m_buildingBlocksStock -= cost;
|
||||||
++m_expansionsPurchased;
|
++m_expansionsPurchased;
|
||||||
m_buildingSystem->setAsteroidWidth_tiles(getCurrentAsteroidWidth_tiles());
|
m_buildingSystem->setAsteroidWidth_tiles(m_factoryState, getCurrentAsteroidWidth_tiles());
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isGameOver() const
|
bool Simulation::isGameOver() const
|
||||||
@@ -1109,12 +818,12 @@ double Simulation::getThreatAccumulationRate() const
|
|||||||
|
|
||||||
double Simulation::getMaxFactoryProductionThreatRate() const
|
double Simulation::getMaxFactoryProductionThreatRate() const
|
||||||
{
|
{
|
||||||
return static_cast<double>(m_buildingSystem->getProductionBuildingCount());
|
return static_cast<double>(getProductionBuildingCount(m_factoryState));
|
||||||
}
|
}
|
||||||
|
|
||||||
double Simulation::getCurrentFactoryProductionThreatRate() const
|
double Simulation::getCurrentFactoryProductionThreatRate() const
|
||||||
{
|
{
|
||||||
return static_cast<double>(m_buildingSystem->getActiveProductionBuildingCount());
|
return static_cast<double>(getActiveProductionBuildingCount(m_factoryState));
|
||||||
}
|
}
|
||||||
|
|
||||||
int Simulation::getBossWaveCounter() const
|
int Simulation::getBossWaveCounter() const
|
||||||
@@ -1134,37 +843,17 @@ Tick Simulation::getNormalGapRemainingTicks() const
|
|||||||
|
|
||||||
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
bool Simulation::isSchematicUnlocked(const std::string& shipId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isSchematicUnlocked(shipId);
|
||||||
m_schematicLevels.find(shipId);
|
|
||||||
if (it == m_schematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
bool Simulation::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
{
|
{
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
return m_unlockState.isModuleSchematicUnlocked(moduleId);
|
||||||
m_moduleSchematicLevels.find(moduleId);
|
|
||||||
if (it == m_moduleSchematicLevels.end())
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
bool Simulation::isBuildingUnlocked(BuildingType type) const
|
||||||
{
|
{
|
||||||
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
return m_unlockState.isBuildingUnlocked(type);
|
||||||
if (def == nullptr)
|
|
||||||
{
|
|
||||||
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const std::map<std::string, SchematicState>::const_iterator it =
|
|
||||||
m_buildingLevels.find(def->id);
|
|
||||||
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint anchor, Rotation rotation)
|
||||||
@@ -1176,7 +865,7 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!m_buildingSystem->isPlacementValid(type, anchor, rotation))
|
if (!isPlacementValid(m_factoryState, m_config, type, anchor, rotation))
|
||||||
{
|
{
|
||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
@@ -1195,17 +884,17 @@ std::optional<BuildingId> Simulation::tryPlaceBuilding(BuildingType type, QPoint
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
m_buildingBlocksStock -= cost;
|
m_buildingBlocksStock -= cost;
|
||||||
return m_buildingSystem->place(type, anchor, rotation, m_currentTick);
|
return m_buildingSystem->place(m_factoryState, type, anchor, rotation, m_currentTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::deconstruct(BuildingId id)
|
void Simulation::deconstruct(BuildingId id)
|
||||||
{
|
{
|
||||||
m_buildingBlocksStock += m_buildingSystem->deconstruct(id, m_currentTick);
|
m_buildingBlocksStock += m_buildingSystem->deconstruct(m_factoryState, id, m_currentTick);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Simulation::cancelDeconstruction(BuildingId id)
|
void Simulation::cancelDeconstruction(BuildingId id)
|
||||||
{
|
{
|
||||||
m_buildingSystem->cancelDeconstruction(id);
|
m_buildingSystem->cancelDeconstruction(m_factoryState, id);
|
||||||
}
|
}
|
||||||
|
|
||||||
BuildingSystem& Simulation::getBuildingsMutable()
|
BuildingSystem& Simulation::getBuildingsMutable()
|
||||||
@@ -1213,6 +902,11 @@ BuildingSystem& Simulation::getBuildingsMutable()
|
|||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const FactoryState& Simulation::getFactoryState() const
|
||||||
|
{
|
||||||
|
return m_factoryState;
|
||||||
|
}
|
||||||
|
|
||||||
const BuildingSystem& Simulation::getBuildings() const
|
const BuildingSystem& Simulation::getBuildings() const
|
||||||
{
|
{
|
||||||
return *m_buildingSystem;
|
return *m_buildingSystem;
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <set>
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include <QPoint>
|
#include <QPoint>
|
||||||
|
|
||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "EntityAdmin.h"
|
#include "EntityAdmin.h"
|
||||||
#include "entt/entity/entity.hpp"
|
#include "entt/entity/entity.hpp"
|
||||||
#include "SchematicChoiceOption.h"
|
#include "SchematicChoiceOption.h"
|
||||||
@@ -22,9 +21,12 @@
|
|||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "TracePrintRequestedEvent.h"
|
#include "TracePrintRequestedEvent.h"
|
||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
class AiSystem;
|
class AiSystem;
|
||||||
class BuildingSystem;
|
class BuildingSystem;
|
||||||
|
class ConstructionSystem;
|
||||||
|
class DeconstructionSystem;
|
||||||
struct Command;
|
struct Command;
|
||||||
class Hasher;
|
class Hasher;
|
||||||
class CombatSystem;
|
class CombatSystem;
|
||||||
@@ -119,6 +121,9 @@ public:
|
|||||||
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
|
// chokepoint) or, in tests, SimulationTestAccess — so production code cannot
|
||||||
// mutate the factory outside the recorded command path (docs/replay_design.md).
|
// mutate the factory outside the recorded command path (docs/replay_design.md).
|
||||||
const BuildingSystem& getBuildings() const;
|
const BuildingSystem& getBuildings() const;
|
||||||
|
|
||||||
|
// The factory's world data, for the free queries in FactoryQueries.h.
|
||||||
|
const FactoryState& getFactoryState() const;
|
||||||
const BeltSystem& getBelts() const;
|
const BeltSystem& getBelts() const;
|
||||||
ShipSystem& getShips();
|
ShipSystem& getShips();
|
||||||
const ShipSystem& getShips() const;
|
const ShipSystem& getShips() const;
|
||||||
@@ -165,6 +170,11 @@ private:
|
|||||||
|
|
||||||
BuildingId allocateBuildingId(); // Strictly increasing; never returns kInvalidBuildingId.
|
BuildingId allocateBuildingId(); // Strictly increasing; never returns kInvalidBuildingId.
|
||||||
|
|
||||||
|
// (Re-)create every owned subsystem. Shared by the constructor and reset();
|
||||||
|
// the construction order is load-bearing for determinism, so both paths must
|
||||||
|
// go through here. Only called before the first tick of a run.
|
||||||
|
void initializeSubsystems();
|
||||||
|
|
||||||
// Populate HQ, player defence stations, and the first enemy station set.
|
// Populate HQ, player defence stations, and the first enemy station set.
|
||||||
void placeInitialStructures();
|
void placeInitialStructures();
|
||||||
|
|
||||||
@@ -198,73 +208,20 @@ private:
|
|||||||
entt::entity m_playerStation2Entity;
|
entt::entity m_playerStation2Entity;
|
||||||
entt::entity m_currentEnemyStationEntities[2];
|
entt::entity m_currentEnemyStationEntities[2];
|
||||||
|
|
||||||
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
|
// Schematic/unlock bookkeeping (REQ-DEF-SCHEMATIC-DROP, REQ-LOCK-EXPLICIT,
|
||||||
struct SchematicState
|
// REQ-LOCK-IMPLICIT, REQ-LOCK-BUILDING, REQ-LOCK-PREREQ). Constructed before
|
||||||
{
|
// initializeSubsystems() runs since BuildingSystem's spawn-gating lambda
|
||||||
bool unlocked;
|
// calls into it (see initializeSubsystems()).
|
||||||
};
|
UnlockState m_unlockState;
|
||||||
std::map<std::string, SchematicState> m_schematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
|
||||||
std::map<std::string, SchematicState> m_buildingLevels;
|
|
||||||
|
|
||||||
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
|
|
||||||
std::set<std::string> m_awardedUnlockGroupIds;
|
|
||||||
|
|
||||||
// Ids granted by some unlock group, per kind — cached from config at init.
|
|
||||||
// An item starts locked iff it appears in the corresponding set.
|
|
||||||
std::set<std::string> m_grantedShipIds;
|
|
||||||
std::set<std::string> m_grantedModuleIds;
|
|
||||||
std::set<std::string> m_grantedBuildingIds;
|
|
||||||
std::set<std::string> m_grantedRecipeIds;
|
|
||||||
|
|
||||||
// Builds the granted-id sets and initializes all per-item unlock maps from
|
|
||||||
// them (shared by the constructor and reset). Ends with recomputeUnlocked().
|
|
||||||
void initializeUnlockState();
|
|
||||||
|
|
||||||
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
|
|
||||||
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
|
|
||||||
|
|
||||||
// Determinism helpers — fold sub-state into the hasher in deterministic order.
|
|
||||||
static void appendSchematicMap(Hasher& hasher,
|
|
||||||
const std::map<std::string, SchematicState>& levels);
|
|
||||||
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
|
|
||||||
|
|
||||||
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeSchematicIds;
|
|
||||||
|
|
||||||
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
|
||||||
std::set<std::string> m_unlockedRecipeIds;
|
|
||||||
std::set<std::string> m_unlockedItemIds;
|
|
||||||
|
|
||||||
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
|
||||||
void recomputeUnlocked();
|
|
||||||
|
|
||||||
// Result of the REQ-LOCK-IMPLICIT traversal.
|
|
||||||
struct UnlockedSets
|
|
||||||
{
|
|
||||||
std::set<std::string> itemIds;
|
|
||||||
std::set<std::string> recipeIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
|
|
||||||
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedModuleSchematicIds,
|
|
||||||
const std::set<std::string>& unlockedRecipeSchematicIds) const;
|
|
||||||
|
|
||||||
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
|
|
||||||
std::set<std::string> getUnlockedShipSchematicIds() const;
|
|
||||||
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
|
||||||
|
|
||||||
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
|
||||||
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
|
||||||
|
|
||||||
// Ids (sorted alphabetically by display name) of the recipes in
|
|
||||||
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
|
||||||
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
|
|
||||||
|
|
||||||
EntityAdmin m_admin;
|
EntityAdmin m_admin;
|
||||||
|
// The factory's world data. Owned here, not by BuildingSystem, so the systems
|
||||||
|
// that operate on it can be handed the same state (see FactoryState.h).
|
||||||
|
FactoryState m_factoryState;
|
||||||
BeltSystem m_beltSystem;
|
BeltSystem m_beltSystem;
|
||||||
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
std::unique_ptr<BuildingSystem> m_buildingSystem;
|
||||||
|
std::unique_ptr<ConstructionSystem> m_constructionSystem;
|
||||||
|
std::unique_ptr<DeconstructionSystem> m_deconstructionSystem;
|
||||||
std::unique_ptr<ShipSystem> m_shipSystem;
|
std::unique_ptr<ShipSystem> m_shipSystem;
|
||||||
std::unique_ptr<AiSystem> m_aiSystem;
|
std::unique_ptr<AiSystem> m_aiSystem;
|
||||||
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
std::unique_ptr<MovementIntentSystem> m_movementIntentSystem;
|
||||||
|
|||||||
@@ -335,15 +335,7 @@ double calculateShipThreatCost(const ThreatCostTable& table,
|
|||||||
const std::string& shipId,
|
const std::string& shipId,
|
||||||
const std::vector<PlacedModule>& modules)
|
const std::vector<PlacedModule>& modules)
|
||||||
{
|
{
|
||||||
const ShipDef* shipDef = nullptr;
|
const ShipDef* shipDef = config.ships.findShipDef(shipId);
|
||||||
for (const ShipDef& d : config.ships.ships)
|
|
||||||
{
|
|
||||||
if (d.id == shipId)
|
|
||||||
{
|
|
||||||
shipDef = &d;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (shipDef == nullptr)
|
if (shipDef == nullptr)
|
||||||
{
|
{
|
||||||
return 0.0;
|
return 0.0;
|
||||||
@@ -357,15 +349,7 @@ double calculateShipThreatCost(const ThreatCostTable& table,
|
|||||||
// Add module production times and material threats.
|
// Add module production times and material threats.
|
||||||
for (const PlacedModule& pm : modules)
|
for (const PlacedModule& pm : modules)
|
||||||
{
|
{
|
||||||
const ModuleDef* moduleDef = nullptr;
|
const ModuleDef* moduleDef = config.modules.findModuleDef(pm.moduleId);
|
||||||
for (const ModuleDef& d : config.modules.modules)
|
|
||||||
{
|
|
||||||
if (d.id == pm.moduleId)
|
|
||||||
{
|
|
||||||
moduleDef = &d;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (moduleDef == nullptr)
|
if (moduleDef == nullptr)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
352
src/lib/sim/UnlockState.cpp
Normal file
352
src/lib/sim/UnlockState.cpp
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
#include "UnlockState.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
#include "DisplayName.h"
|
||||||
|
#include "StateChecksum.h"
|
||||||
|
|
||||||
|
UnlockState::UnlockState(const GameConfig& config)
|
||||||
|
: m_config(config)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::initializeUnlockState()
|
||||||
|
{
|
||||||
|
// Cache the ids granted by some unlock group (REQ-LOCK-EXPLICIT); an item
|
||||||
|
// starts locked iff it is granted by a group.
|
||||||
|
m_grantedShipIds.clear();
|
||||||
|
m_grantedModuleIds.clear();
|
||||||
|
m_grantedBuildingIds.clear();
|
||||||
|
m_grantedRecipeIds.clear();
|
||||||
|
for (const UnlockGroupDef& group : m_config.unlocks.groups)
|
||||||
|
{
|
||||||
|
m_grantedShipIds.insert(group.ships.begin(), group.ships.end());
|
||||||
|
m_grantedModuleIds.insert(group.modules.begin(), group.modules.end());
|
||||||
|
m_grantedBuildingIds.insert(group.buildings.begin(), group.buildings.end());
|
||||||
|
m_grantedRecipeIds.insert(group.recipes.begin(), group.recipes.end());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_awardedUnlockGroupIds.clear();
|
||||||
|
|
||||||
|
m_schematicLevels.clear();
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedShipIds.count(def.id) == 0);
|
||||||
|
m_schematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_moduleSchematicLevels.clear();
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedModuleIds.count(def.id) == 0);
|
||||||
|
m_moduleSchematicLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_buildingLevels.clear();
|
||||||
|
for (const BuildingDef& def : m_config.buildings.buildings)
|
||||||
|
{
|
||||||
|
SchematicState state;
|
||||||
|
state.unlocked = (m_grantedBuildingIds.count(def.id) == 0);
|
||||||
|
m_buildingLevels[def.id] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gated assembler recipes start locked; unlocked_at_start recipes are handled
|
||||||
|
// in the REQ-LOCK-IMPLICIT traversal, not tracked here.
|
||||||
|
m_unlockedRecipeSchematicIds.clear();
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isSchematicUnlocked(const std::string& shipId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_schematicLevels.find(shipId);
|
||||||
|
if (it == m_schematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isModuleSchematicUnlocked(const std::string& moduleId) const
|
||||||
|
{
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_moduleSchematicLevels.find(moduleId);
|
||||||
|
if (it == m_moduleSchematicLevels.end())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isRecipeUnlocked(const std::string& recipeId) const
|
||||||
|
{
|
||||||
|
return m_unlockedRecipeIds.count(recipeId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isItemUnlocked(const std::string& itemId) const
|
||||||
|
{
|
||||||
|
return m_unlockedItemIds.count(itemId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isBuildingUnlocked(BuildingType type) const
|
||||||
|
{
|
||||||
|
const BuildingDef* def = m_config.buildings.findBuildingDef(type);
|
||||||
|
if (def == nullptr)
|
||||||
|
{
|
||||||
|
// Types without a config entry (e.g. HQ, defence stations) are unrestricted.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const std::map<std::string, SchematicState>::const_iterator it =
|
||||||
|
m_buildingLevels.find(def->id);
|
||||||
|
return it == m_buildingLevels.end() ? true : it->second.unlocked;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::isUnlockGroupAwarded(const std::string& groupId) const
|
||||||
|
{
|
||||||
|
return m_awardedUnlockGroupIds.count(groupId) > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool UnlockState::prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const
|
||||||
|
{
|
||||||
|
// A prerequisite is satisfied only once the named unlock group has been
|
||||||
|
// awarded (REQ-LOCK-PREREQ).
|
||||||
|
for (const std::string& groupId : requiredGroupIds)
|
||||||
|
{
|
||||||
|
if (m_awardedUnlockGroupIds.count(groupId) == 0) { return false; }
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SchematicChoiceOption UnlockState::makeUnlockOption(const UnlockGroupDef& group) const
|
||||||
|
{
|
||||||
|
SchematicChoiceOption option;
|
||||||
|
option.isArtifact = false;
|
||||||
|
option.unlockGroupId = group.id;
|
||||||
|
option.displayName = toDisplayName(group.id);
|
||||||
|
|
||||||
|
for (const std::string& id : group.ships)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Ship, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.modules)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Module, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.buildings)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Building, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
for (const std::string& id : group.recipes)
|
||||||
|
{
|
||||||
|
option.grantedItems.push_back({SchematicType::Recipe, id, toDisplayName(id)});
|
||||||
|
}
|
||||||
|
|
||||||
|
// REQ-DEF-SCHEMATIC-DROP: preview recipes newly implicitly unlocked by
|
||||||
|
// awarding this whole group. Seed the hypothetical explicit-unlock sets with
|
||||||
|
// every grant (ship + module materials via step 1a, recipe outputs via step
|
||||||
|
// 1b), then diff against the current implicit set.
|
||||||
|
std::set<std::string> hypotheticalShipIds = getUnlockedShipSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalModuleIds = getUnlockedModuleSchematicIds();
|
||||||
|
std::set<std::string> hypotheticalRecipeSchematicIds = m_unlockedRecipeSchematicIds;
|
||||||
|
for (const std::string& id : group.ships) { hypotheticalShipIds.insert(id); }
|
||||||
|
for (const std::string& id : group.modules) { hypotheticalModuleIds.insert(id); }
|
||||||
|
for (const std::string& id : group.recipes) { hypotheticalRecipeSchematicIds.insert(id); }
|
||||||
|
|
||||||
|
const UnlockedSets hypothetical = computeUnlockedSets(
|
||||||
|
hypotheticalShipIds, hypotheticalModuleIds, hypotheticalRecipeSchematicIds);
|
||||||
|
option.newlyUnlockedRecipeIds = computeNewlyUnlockedRecipeIds(hypothetical);
|
||||||
|
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::awardUnlockGroup(const SchematicChoiceOption& chosen)
|
||||||
|
{
|
||||||
|
// Award the whole unlock group (REQ-DEF-SCHEMATIC-DROP): unlock every granted
|
||||||
|
// ship, module, building, and assembler recipe at once.
|
||||||
|
m_awardedUnlockGroupIds.insert(chosen.unlockGroupId);
|
||||||
|
for (const GrantedSchematic& grant : chosen.grantedItems)
|
||||||
|
{
|
||||||
|
switch (grant.type)
|
||||||
|
{
|
||||||
|
case SchematicType::Ship: m_schematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Module: m_moduleSchematicLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Building: m_buildingLevels.at(grant.id).unlocked = true; break;
|
||||||
|
case SchematicType::Recipe: m_unlockedRecipeSchematicIds.insert(grant.id); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
recomputeUnlocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Implicit unlock computation (REQ-LOCK-IMPLICIT)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::recomputeUnlocked()
|
||||||
|
{
|
||||||
|
const UnlockedSets result = computeUnlockedSets(
|
||||||
|
getUnlockedShipSchematicIds(), getUnlockedModuleSchematicIds(), m_unlockedRecipeSchematicIds);
|
||||||
|
m_unlockedItemIds = result.itemIds;
|
||||||
|
m_unlockedRecipeIds = result.recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedShipSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_schematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string> UnlockState::getUnlockedModuleSchematicIds() const
|
||||||
|
{
|
||||||
|
std::set<std::string> ids;
|
||||||
|
for (const auto& [id, state] : m_moduleSchematicLevels)
|
||||||
|
{
|
||||||
|
if (state.unlocked) { ids.insert(id); }
|
||||||
|
}
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnlockState::UnlockedSets UnlockState::computeUnlockedSets(
|
||||||
|
const std::set<std::string>& unlockedShipSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedRecipeSchematicIds) const
|
||||||
|
{
|
||||||
|
UnlockedSets result;
|
||||||
|
|
||||||
|
for (const ShipDef& def : m_config.ships.ships)
|
||||||
|
{
|
||||||
|
if (unlockedShipSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.schematic.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const ModuleDef& def : m_config.modules.modules)
|
||||||
|
{
|
||||||
|
if (unlockedModuleSchematicIds.count(def.id) == 0) { continue; }
|
||||||
|
for (const RecipeIngredient& mat : def.materials)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(mat.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const RecipeDef& def : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
// An assembler recipe seeds the base set when it is explicitly available:
|
||||||
|
// flagged unlocked_at_start (base recipes the graph can't reach), or a
|
||||||
|
// gated recipe whose unlock group has been awarded (REQ-LOCK-EXPLICIT).
|
||||||
|
if (def.building == BuildingType::Assembler
|
||||||
|
&& (def.unlockedAtStart || unlockedRecipeSchematicIds.count(def.id) > 0))
|
||||||
|
{
|
||||||
|
for (const RecipeOutput& out : def.outputs)
|
||||||
|
{
|
||||||
|
result.itemIds.insert(out.item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool changed = true;
|
||||||
|
while (changed)
|
||||||
|
{
|
||||||
|
changed = false;
|
||||||
|
for (const RecipeDef& recipe : m_config.recipes.recipes)
|
||||||
|
{
|
||||||
|
if (recipe.building != BuildingType::Miner
|
||||||
|
&& recipe.building != BuildingType::Smelter
|
||||||
|
&& recipe.building != BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Skip a gated assembler recipe (granted by an unlock group) whose
|
||||||
|
// group has not yet been awarded (REQ-LOCK-IMPLICIT step 2).
|
||||||
|
if (recipe.building == BuildingType::Assembler
|
||||||
|
&& m_grantedRecipeIds.count(recipe.id) > 0
|
||||||
|
&& unlockedRecipeSchematicIds.count(recipe.id) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
bool producesUnlocked = false;
|
||||||
|
for (const RecipeOutput& out : recipe.outputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.count(out.item) > 0)
|
||||||
|
{
|
||||||
|
producesUnlocked = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!producesUnlocked) { continue; }
|
||||||
|
|
||||||
|
if (recipe.building == BuildingType::Miner
|
||||||
|
|| recipe.building == BuildingType::Assembler)
|
||||||
|
{
|
||||||
|
result.recipeIds.insert(recipe.id);
|
||||||
|
}
|
||||||
|
for (const RecipeIngredient& ing : recipe.inputs)
|
||||||
|
{
|
||||||
|
if (result.itemIds.insert(ing.item).second)
|
||||||
|
{
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> UnlockState::computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const
|
||||||
|
{
|
||||||
|
std::vector<std::string> recipeIds;
|
||||||
|
for (const std::string& recipeId : hypothetical.recipeIds)
|
||||||
|
{
|
||||||
|
if (m_unlockedRecipeIds.count(recipeId) > 0) { continue; }
|
||||||
|
recipeIds.push_back(recipeId);
|
||||||
|
}
|
||||||
|
std::sort(recipeIds.begin(), recipeIds.end(),
|
||||||
|
[](const std::string& lhs, const std::string& rhs)
|
||||||
|
{
|
||||||
|
return toDisplayName(lhs) < toDisplayName(rhs);
|
||||||
|
});
|
||||||
|
return recipeIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Determinism (see docs/replay_design.md)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
void UnlockState::appendSchematicMap(Hasher& hasher,
|
||||||
|
const std::map<std::string, SchematicState>& levels)
|
||||||
|
{
|
||||||
|
hasher.append(levels.size());
|
||||||
|
for (const std::pair<const std::string, SchematicState>& entry : levels)
|
||||||
|
{
|
||||||
|
hasher.append(entry.first);
|
||||||
|
hasher.append(entry.second.unlocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendStringSet(Hasher& hasher, const std::set<std::string>& ids)
|
||||||
|
{
|
||||||
|
hasher.append(ids.size());
|
||||||
|
for (const std::string& id : ids)
|
||||||
|
{
|
||||||
|
hasher.append(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void UnlockState::appendChecksum(Hasher& hasher) const
|
||||||
|
{
|
||||||
|
appendSchematicMap(hasher, m_schematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_moduleSchematicLevels);
|
||||||
|
appendSchematicMap(hasher, m_buildingLevels);
|
||||||
|
appendStringSet(hasher, m_awardedUnlockGroupIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeSchematicIds);
|
||||||
|
appendStringSet(hasher, m_unlockedRecipeIds);
|
||||||
|
appendStringSet(hasher, m_unlockedItemIds);
|
||||||
|
}
|
||||||
129
src/lib/sim/UnlockState.h
Normal file
129
src/lib/sim/UnlockState.h
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <set>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "BuildingType.h"
|
||||||
|
#include "GameConfig.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
|
|
||||||
|
class Hasher;
|
||||||
|
|
||||||
|
// Owns schematic/unlock bookkeeping for one run: which ship, module, and
|
||||||
|
// building schematics are unlocked (REQ-LOCK-EXPLICIT), which assembler recipe
|
||||||
|
// schematics have been explicitly granted, and the implicit recipe/item unlock
|
||||||
|
// sets derived from that state (REQ-LOCK-IMPLICIT). Reads config the same way
|
||||||
|
// BuildingSystem does (a bound const reference to Simulation::m_config, which is
|
||||||
|
// safe across restart because that member's storage address never changes —
|
||||||
|
// reset() move-assigns into it rather than replacing it).
|
||||||
|
//
|
||||||
|
// Simulation forwards its isXUnlocked-style public queries here and drives
|
||||||
|
// state changes (awarding an unlock group) here; the RNG-touching schematic
|
||||||
|
// choice generation itself stays in Simulation (call ordering of m_rng is the
|
||||||
|
// determinism backbone and must not move).
|
||||||
|
class UnlockState
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit UnlockState(const GameConfig& config);
|
||||||
|
|
||||||
|
// Builds the granted-id sets and initializes all per-item unlock maps from
|
||||||
|
// them (shared by the constructor and Simulation::reset). Ends with
|
||||||
|
// recomputeUnlocked().
|
||||||
|
void initializeUnlockState();
|
||||||
|
|
||||||
|
// Ship schematic state query.
|
||||||
|
bool isSchematicUnlocked(const std::string& shipId) const;
|
||||||
|
|
||||||
|
// Module schematic state query.
|
||||||
|
bool isModuleSchematicUnlocked(const std::string& moduleId) const;
|
||||||
|
|
||||||
|
// Implicit recipe/item unlock queries (REQ-LOCK-IMPLICIT).
|
||||||
|
bool isRecipeUnlocked(const std::string& recipeId) const;
|
||||||
|
bool isItemUnlocked(const std::string& itemId) const;
|
||||||
|
|
||||||
|
// Building unlock query (REQ-LOCK-BUILDING). True if the building type is not
|
||||||
|
// gated by any unlock group, or its granting group has been awarded.
|
||||||
|
bool isBuildingUnlocked(BuildingType type) const;
|
||||||
|
|
||||||
|
// True if the unlock group has already been awarded to the player.
|
||||||
|
bool isUnlockGroupAwarded(const std::string& groupId) const;
|
||||||
|
|
||||||
|
// True if every prerequisite unlock group has been awarded (REQ-LOCK-PREREQ).
|
||||||
|
bool prerequisitesSatisfied(const std::vector<std::string>& requiredGroupIds) const;
|
||||||
|
|
||||||
|
// Builds a schematic choice option for one unlock group (REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
SchematicChoiceOption makeUnlockOption(const UnlockGroupDef& group) const;
|
||||||
|
|
||||||
|
// Awards the unlock group backing `chosen` (REQ-DEF-SCHEMATIC-DROP): marks
|
||||||
|
// every granted ship/module/building schematic unlocked, records granted
|
||||||
|
// recipe schematics, marks the group as awarded, and recomputes the implicit
|
||||||
|
// unlock sets. Mirrors the non-artifact branch of the original
|
||||||
|
// Simulation::applySchematicChoice exactly; callers still special-case
|
||||||
|
// chosen.isArtifact themselves before calling this.
|
||||||
|
void awardUnlockGroup(const SchematicChoiceOption& chosen);
|
||||||
|
|
||||||
|
// Determinism helper (see Simulation::computeStateChecksum): folds unlock
|
||||||
|
// state into the hasher via the same seven calls, in the same order, that
|
||||||
|
// used to live at the Simulation::computeStateChecksum call site.
|
||||||
|
void appendChecksum(Hasher& hasher) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Schematic unlock state (REQ-DEF-SCHEMATIC-DROP).
|
||||||
|
struct SchematicState
|
||||||
|
{
|
||||||
|
bool unlocked;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recomputes m_unlockedRecipeIds and m_unlockedItemIds from current schematic state.
|
||||||
|
void recomputeUnlocked();
|
||||||
|
|
||||||
|
// Result of the REQ-LOCK-IMPLICIT traversal.
|
||||||
|
struct UnlockedSets
|
||||||
|
{
|
||||||
|
std::set<std::string> itemIds;
|
||||||
|
std::set<std::string> recipeIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pure REQ-LOCK-IMPLICIT traversal given hypothetical explicit-unlock sets.
|
||||||
|
UnlockedSets computeUnlockedSets(const std::set<std::string>& unlockedShipSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedModuleSchematicIds,
|
||||||
|
const std::set<std::string>& unlockedRecipeSchematicIds) const;
|
||||||
|
|
||||||
|
// Current explicit-unlock id sets, derived from m_schematicLevels / m_moduleSchematicLevels.
|
||||||
|
std::set<std::string> getUnlockedShipSchematicIds() const;
|
||||||
|
std::set<std::string> getUnlockedModuleSchematicIds() const;
|
||||||
|
|
||||||
|
// Ids (sorted alphabetically by display name) of the recipes in
|
||||||
|
// hypothetical.recipeIds that are not yet in m_unlockedRecipeIds.
|
||||||
|
std::vector<std::string> computeNewlyUnlockedRecipeIds(const UnlockedSets& hypothetical) const;
|
||||||
|
|
||||||
|
// Determinism helpers — fold sub-state into the hasher in deterministic order.
|
||||||
|
static void appendSchematicMap(Hasher& hasher,
|
||||||
|
const std::map<std::string, SchematicState>& levels);
|
||||||
|
static void appendStringSet(Hasher& hasher, const std::set<std::string>& ids);
|
||||||
|
|
||||||
|
const GameConfig& m_config;
|
||||||
|
|
||||||
|
std::map<std::string, SchematicState> m_schematicLevels;
|
||||||
|
std::map<std::string, SchematicState> m_moduleSchematicLevels;
|
||||||
|
std::map<std::string, SchematicState> m_buildingLevels;
|
||||||
|
|
||||||
|
// Unlock groups awarded so far (REQ-LOCK-EXPLICIT). Group ids.
|
||||||
|
std::set<std::string> m_awardedUnlockGroupIds;
|
||||||
|
|
||||||
|
// Ids granted by some unlock group, per kind — cached from config at init.
|
||||||
|
// An item starts locked iff it appears in the corresponding set.
|
||||||
|
std::set<std::string> m_grantedShipIds;
|
||||||
|
std::set<std::string> m_grantedModuleIds;
|
||||||
|
std::set<std::string> m_grantedBuildingIds;
|
||||||
|
std::set<std::string> m_grantedRecipeIds;
|
||||||
|
|
||||||
|
// Explicitly unlocked assembler recipe schematics (REQ-LOCK-EXPLICIT).
|
||||||
|
std::set<std::string> m_unlockedRecipeSchematicIds;
|
||||||
|
|
||||||
|
// Implicit unlock sets derived from schematic state (REQ-LOCK-IMPLICIT).
|
||||||
|
std::set<std::string> m_unlockedRecipeIds;
|
||||||
|
std::set<std::string> m_unlockedItemIds;
|
||||||
|
};
|
||||||
@@ -11,11 +11,7 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
static GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void killEnemyStations(Simulation& sim)
|
static void killEnemyStations(Simulation& sim)
|
||||||
{
|
{
|
||||||
@@ -51,7 +47,7 @@ static int findArtifactChoiceIndex(const Simulation& sim)
|
|||||||
TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded",
|
TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count are loaded",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
CHECK(cfg.world.artifacts.artifactWinCount == 3);
|
CHECK(cfg.world.artifacts.artifactWinCount == 3);
|
||||||
// 0.05 * x at x=2 should be 0.1
|
// 0.05 * x at x=2 should be 0.1
|
||||||
CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1));
|
CHECK(cfg.world.artifacts.artifactChanceFormula.evaluate(2.0) == Approx(0.1));
|
||||||
@@ -64,7 +60,7 @@ TEST_CASE("ArtifactWinCondition: artifact_chance_formula and artifact_win_count
|
|||||||
TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start",
|
TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game start",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
const Simulation sim(loadConfig());
|
const Simulation sim(loadTestConfig());
|
||||||
CHECK(sim.getArtifactCount() == 0);
|
CHECK(sim.getArtifactCount() == 0);
|
||||||
CHECK_FALSE(sim.isWon());
|
CHECK_FALSE(sim.isWon());
|
||||||
}
|
}
|
||||||
@@ -76,7 +72,7 @@ TEST_CASE("ArtifactWinCondition: artifact count is 0 and isWon is false at game
|
|||||||
TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1",
|
TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula returns 1",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|
||||||
@@ -95,7 +91,7 @@ TEST_CASE("ArtifactWinCondition: artifact option appears when chance formula ret
|
|||||||
TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact",
|
TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artifact",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|
||||||
@@ -112,7 +108,7 @@ TEST_CASE("ArtifactWinCondition: at most 2 schematic options accompany the artif
|
|||||||
TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0",
|
TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns 0",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("0");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("0");
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|
||||||
@@ -133,7 +129,7 @@ TEST_CASE("ArtifactWinCondition: no artifact option when chance formula returns
|
|||||||
TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|
||||||
@@ -151,7 +147,7 @@ TEST_CASE("ArtifactWinCondition: selecting artifact increments artifact count",
|
|||||||
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
|
TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increment artifact count",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|
||||||
@@ -176,7 +172,7 @@ TEST_CASE("ArtifactWinCondition: selecting a non-artifact option does not increm
|
|||||||
TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count",
|
TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches win count",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
cfg.world.artifacts.artifactWinCount = 1;
|
cfg.world.artifacts.artifactWinCount = 1;
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
@@ -196,7 +192,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true when artifact count reaches
|
|||||||
TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count",
|
TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below win count",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
cfg.world.artifacts.artifactWinCount = 2;
|
cfg.world.artifacts.artifactWinCount = 2;
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
@@ -212,7 +208,7 @@ TEST_CASE("ArtifactWinCondition: isWon stays false when artifact count is below
|
|||||||
TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts",
|
TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required number of artifacts",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
cfg.world.artifacts.artifactWinCount = 2;
|
cfg.world.artifacts.artifactWinCount = 2;
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
@@ -237,7 +233,7 @@ TEST_CASE("ArtifactWinCondition: isWon becomes true after collecting required nu
|
|||||||
TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
|
TEST_CASE("ArtifactWinCondition: reset clears artifact count and win state",
|
||||||
"[artifact_win]")
|
"[artifact_win]")
|
||||||
{
|
{
|
||||||
GameConfig cfg = loadConfig();
|
GameConfig cfg = loadTestConfig();
|
||||||
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
cfg.world.artifacts.artifactChanceFormula = Formula::compile("1");
|
||||||
cfg.world.artifacts.artifactWinCount = 1;
|
cfg.world.artifacts.artifactWinCount = 1;
|
||||||
Simulation sim(std::move(cfg));
|
Simulation sim(std::move(cfg));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <random>
|
#include <random>
|
||||||
@@ -14,6 +15,8 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "ConstructionSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "DeliverScrapBehavior.h"
|
#include "DeliverScrapBehavior.h"
|
||||||
@@ -45,25 +48,23 @@
|
|||||||
#include "ShipLayout.h"
|
#include "ShipLayout.h"
|
||||||
#include "ShipSystem.h"
|
#include "ShipSystem.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Fixture
|
// Fixture
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
static GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Fixture
|
struct Fixture
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
|
FactoryState state = makeFactoryState(cfg);
|
||||||
BeltSystem belts;
|
BeltSystem belts;
|
||||||
BuildingId nextBuildingId;
|
BuildingId nextBuildingId;
|
||||||
int stock;
|
int stock;
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
BuildingSystem buildings;
|
BuildingSystem buildings;
|
||||||
|
ConstructionSystem construction;
|
||||||
ShipSystem ships;
|
ShipSystem ships;
|
||||||
AiSystem ai;
|
AiSystem ai;
|
||||||
SalvagerSystem salvager;
|
SalvagerSystem salvager;
|
||||||
@@ -75,7 +76,7 @@ struct Fixture
|
|||||||
std::vector<BeamFiredEvent> beamEvents;
|
std::vector<BeamFiredEvent> beamEvents;
|
||||||
|
|
||||||
explicit Fixture()
|
explicit Fixture()
|
||||||
: cfg(loadConfig())
|
: cfg(loadTestConfig())
|
||||||
, belts(cfg.world.beltSpeed_tps)
|
, belts(cfg.world.beltSpeed_tps)
|
||||||
, nextBuildingId(1)
|
, nextBuildingId(1)
|
||||||
, stock(0)
|
, stock(0)
|
||||||
@@ -86,6 +87,7 @@ struct Fixture
|
|||||||
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
[](const std::string&, QVector2D, const std::optional<ShipLayoutConfig>&) {},
|
||||||
[](const std::string&) -> bool { return true; },
|
[](const std::string&) -> bool { return true; },
|
||||||
rng)
|
rng)
|
||||||
|
, construction(cfg)
|
||||||
, ships(cfg, admin)
|
, ships(cfg, admin)
|
||||||
, ai(cfg)
|
, ai(cfg)
|
||||||
, salvager(admin)
|
, salvager(admin)
|
||||||
@@ -99,14 +101,14 @@ struct Fixture
|
|||||||
void decide()
|
void decide()
|
||||||
{
|
{
|
||||||
ships.clearMovementIntents();
|
ships.clearMovementIntents();
|
||||||
ai.tick(admin, buildings, scraps);
|
ai.tick(admin, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
// World mutation: collection/delivery and healing.
|
// World mutation: collection/delivery and healing.
|
||||||
void runModules()
|
void runModules()
|
||||||
{
|
{
|
||||||
beamEvents.clear();
|
beamEvents.clear();
|
||||||
salvager.tick(tick, scraps, buildings, beamEvents);
|
salvager.tick(tick, state, beamEvents);
|
||||||
repair.tick(tick, beamEvents);
|
repair.tick(tick, beamEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +141,7 @@ struct Fixture
|
|||||||
void salvageTick()
|
void salvageTick()
|
||||||
{
|
{
|
||||||
beamEvents.clear();
|
beamEvents.clear();
|
||||||
salvager.tick(tick, scraps, buildings, beamEvents);
|
salvager.tick(tick, state, beamEvents);
|
||||||
++tick;
|
++tick;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -953,18 +955,18 @@ TEST_CASE("BehaviorSystem: full-cargo salvage ship moves toward SalvageBay", "[b
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
|
|
||||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||||
QPoint(-4, 0), Rotation::East, 0).value();
|
QPoint(-4, 0), Rotation::East, 0).value();
|
||||||
Tick t = 0;
|
Tick t = 0;
|
||||||
for (int i = 0; i < 500; ++i)
|
for (int i = 0; i < 500; ++i)
|
||||||
{
|
{
|
||||||
f.buildings.tickConstruction(t++);
|
f.construction.tick(f.state, f.belts, t++);
|
||||||
if (f.buildings.findBuilding(bayId) != nullptr)
|
if (findBuilding(f.state, bayId) != nullptr)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
REQUIRE(f.buildings.findBuilding(bayId) != nullptr);
|
REQUIRE(findBuilding(f.state, bayId) != nullptr);
|
||||||
|
|
||||||
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
const ShipLayoutConfig salvageLayout = makeSingleModuleLayout("salvager");
|
||||||
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
const entt::entity ship = f.ships.spawn("salvage_ship", QVector2D(5.0f, 0.0f),
|
||||||
@@ -988,15 +990,15 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
|
|||||||
{
|
{
|
||||||
Fixture f;
|
Fixture f;
|
||||||
|
|
||||||
const BuildingId bayId = f.buildings.place(BuildingType::SalvageBay,
|
const BuildingId bayId = f.buildings.place(f.state, BuildingType::SalvageBay,
|
||||||
QPoint(-4, 0), Rotation::East, 0).value();
|
QPoint(-4, 0), Rotation::East, 0).value();
|
||||||
Tick t = 0;
|
Tick t = 0;
|
||||||
for (int i = 0; i < 500; ++i)
|
for (int i = 0; i < 500; ++i)
|
||||||
{
|
{
|
||||||
f.buildings.tickConstruction(t++);
|
f.construction.tick(f.state, f.belts, t++);
|
||||||
if (f.buildings.findBuilding(bayId) != nullptr) { break; }
|
if (findBuilding(f.state, bayId) != nullptr) { break; }
|
||||||
}
|
}
|
||||||
const Building* bay = f.buildings.findBuilding(bayId);
|
const Building* bay = findBuilding(f.state, bayId);
|
||||||
REQUIRE(bay != nullptr);
|
REQUIRE(bay != nullptr);
|
||||||
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
|
// Config-driven output-buffer capacity is applied on placement (REQ-BLD-SALVAGE-BAY).
|
||||||
REQUIRE(bay->outputBuffer.capacity == 20);
|
REQUIRE(bay->outputBuffer.capacity == 20);
|
||||||
@@ -1017,7 +1019,7 @@ TEST_CASE("SalvagerSystem: full-cargo ship at its SalvageBay hands over cargo",
|
|||||||
|
|
||||||
// One unit handed over from cargo into the bay's output buffer.
|
// One unit handed over from cargo into the bay's output buffer.
|
||||||
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
|
REQUIRE(f.admin.get<CargoComponent>(ship).current == before - 1);
|
||||||
const Building* bayAfter = f.buildings.findBuilding(bayId);
|
const Building* bayAfter = findBuilding(f.state, bayId);
|
||||||
REQUIRE(bayAfter != nullptr);
|
REQUIRE(bayAfter != nullptr);
|
||||||
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
|
REQUIRE(bayAfter->outputBuffer.items.size() == 1);
|
||||||
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
|
REQUIRE(bayAfter->outputBuffer.items.front().type.id == "scrap");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <climits>
|
#include <climits>
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "SurfaceMask.h"
|
#include "SurfaceMask.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Helpers that mirror the production implementations under test.
|
// Helpers that mirror the production implementations under test.
|
||||||
@@ -147,11 +149,6 @@ static void applyRotationCCW(Blueprint& bp, const GameConfig& cfg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
|
// Mirrors BlueprintPanel::createBlueprintFromSelection's player-placeable filter:
|
||||||
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
// building types absent from buildings.toml (HQ, stations) or with playerPlaceable=false
|
||||||
// are silently excluded before the bounding-box center and offsets are computed.
|
// are silently excluded before the bounding-box center and offsets are computed.
|
||||||
@@ -312,7 +309,7 @@ TEST_CASE("Blueprint: non-axis-aligned offset rotates correctly", "[blueprint]")
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
|
TEST_CASE("Blueprint: CW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
// Building one tile to the right, facing East.
|
// Building one tile to the right, facing East.
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
@@ -332,7 +329,7 @@ TEST_CASE("Blueprint: CW constellation rotation updates offset and building rota
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
|
TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
BlueprintBuilding bb;
|
BlueprintBuilding bb;
|
||||||
@@ -351,7 +348,7 @@ TEST_CASE("Blueprint: CCW constellation rotation updates offset and building rot
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
|
TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
BlueprintBuilding bb;
|
BlueprintBuilding bb;
|
||||||
@@ -371,7 +368,7 @@ TEST_CASE("Blueprint: four CW rotations restore offset and building rotation", "
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
|
TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
// Two buildings left and right of center; after CW they should be above and below.
|
// Two buildings left and right of center; after CW they should be above and below.
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
@@ -398,7 +395,7 @@ TEST_CASE("Blueprint: multi-building constellation rotates symmetrically CW", "[
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
// East miner: anchor (0,0), body cells (0,0),(1,0),(0,1).
|
// East miner: anchor (0,0), body cells (0,0),(1,0),(0,1).
|
||||||
// Output port indicator '>' at (1,1) → port tile (1,1), direction East.
|
// Output port indicator '>' at (1,1) → port tile (1,1), direction East.
|
||||||
@@ -436,7 +433,7 @@ TEST_CASE("Blueprint: CW rotation keeps belt adjacent to miner output port", "[b
|
|||||||
|
|
||||||
TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
Blueprint bp;
|
Blueprint bp;
|
||||||
bp.name = "test";
|
bp.name = "test";
|
||||||
@@ -473,7 +470,7 @@ TEST_CASE("Blueprint: CCW rotation keeps belt adjacent to miner output port", "[
|
|||||||
TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint",
|
TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty blueprint",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
// Hq has no entry in buildings.toml, so it is treated as non-player-placeable.
|
// Hq has no entry in buildings.toml, so it is treated as non-player-placeable.
|
||||||
const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East };
|
const BuildingSpec hq{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Hq, Rotation::East };
|
||||||
@@ -485,7 +482,7 @@ TEST_CASE("Blueprint creation: non-player-placeable building alone yields empty
|
|||||||
TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings",
|
TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable buildings",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
|
const BuildingSpec belt{ QPoint(-5, 0), {QPoint(-5, 0)}, BuildingType::Belt, Rotation::East };
|
||||||
const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East };
|
const BuildingSpec hq { QPoint(-3, 0), {QPoint(-3, 0)}, BuildingType::Hq, Rotation::East };
|
||||||
@@ -498,7 +495,7 @@ TEST_CASE("Blueprint creation: mixed selection keeps only player-placeable build
|
|||||||
TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings",
|
TEST_CASE("Blueprint creation: bounding box ignores non-player-placeable buildings",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
|
|
||||||
// Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint.
|
// Belt at (-5, 0). HQ at (-3, 0) — excluded from the blueprint.
|
||||||
// If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1.
|
// If HQ were included: bboxX = [-5, -3], center.x = -4, belt offset = -1.
|
||||||
@@ -520,7 +517,7 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
|
|||||||
// Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
|
// Simulate placing a two-belt blueprint with offsets (-1, 0) and (+1, 0)
|
||||||
// at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0).
|
// at cursor tile (-5, 0). Expected anchors: (-6, 0) and (-4, 0).
|
||||||
// (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
|
// (Belt surface_mask ["A>"] — body at relative (0,0), port at (1,0).)
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
const QPoint cursor(-5, 0);
|
const QPoint cursor(-5, 0);
|
||||||
const QPoint offsetA(-1, 0);
|
const QPoint offsetA(-1, 0);
|
||||||
@@ -533,14 +530,14 @@ TEST_CASE("Blueprint placement: buildings land at anchor + offset from cursor",
|
|||||||
|
|
||||||
REQUIRE(idA != kInvalidBuildingId);
|
REQUIRE(idA != kInvalidBuildingId);
|
||||||
REQUIRE(idB != kInvalidBuildingId);
|
REQUIRE(idB != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetA)); // (-6, 0)
|
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetA)); // (-6, 0)
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(cursor + offsetB)); // (-4, 0)
|
REQUIRE(isTileOccupied(sim.getFactoryState(), cursor + offsetB)); // (-4, 0)
|
||||||
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(cursor)); // center not occupied
|
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), cursor)); // center not occupied
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
TEST_CASE("Blueprint placement: cost is deducted for each building in sequence", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Find belt cost from config (belt cost = 2 in test config).
|
// Find belt cost from config (belt cost = 2 in test config).
|
||||||
int beltCost = 0;
|
int beltCost = 0;
|
||||||
@@ -563,7 +560,7 @@ TEST_CASE("Blueprint placement: cost is deducted for each building in sequence",
|
|||||||
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
|
TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId and deducts nothing",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
|
// Find miner cost (15 in test config) — expensive enough to exhaust a small stock.
|
||||||
int minerCost = 0;
|
int minerCost = 0;
|
||||||
@@ -594,7 +591,7 @@ TEST_CASE("Blueprint placement: insufficient blocks returns kInvalidBuildingId a
|
|||||||
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
|
TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and charges nothing",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
const int startBlocks = sim.getBuildingBlocksStock();
|
const int startBlocks = sim.getBuildingBlocksStock();
|
||||||
|
|
||||||
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
|
// A miner is all-asteroid; placing it in space (x >= 0) violates the terrain
|
||||||
@@ -604,13 +601,13 @@ TEST_CASE("Simulation: tryPlaceBuilding rejects terrain-invalid placement and ch
|
|||||||
|
|
||||||
REQUIRE_FALSE(id.has_value());
|
REQUIRE_FALSE(id.has_value());
|
||||||
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
|
REQUIRE(sim.getBuildingBlocksStock() == startBlocks);
|
||||||
REQUIRE(sim.getBuildings().getAllSites().empty());
|
REQUIRE(getAllSites(sim.getFactoryState()).empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
|
TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies tiles, charges cost",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
int minerCost = 0;
|
int minerCost = 0;
|
||||||
for (const BuildingDef& def : sim.getConfig().buildings.buildings)
|
for (const BuildingDef& def : sim.getConfig().buildings.buildings)
|
||||||
@@ -627,11 +624,11 @@ TEST_CASE("Simulation: tryPlaceBuilding accepts a valid asteroid spot, occupies
|
|||||||
|
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
|
REQUIRE(sim.getBuildingBlocksStock() == startBlocks - minerCost);
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 0)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 0)));
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-2, 0)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 0)));
|
||||||
REQUIRE(sim.getBuildings().isTileOccupied(QPoint(-3, 1)));
|
REQUIRE(isTileOccupied(sim.getFactoryState(), QPoint(-3, 1)));
|
||||||
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
|
// The output-port tile (1,1)+anchor = (-2,1) is not a body cell.
|
||||||
REQUIRE_FALSE(sim.getBuildings().isTileOccupied(QPoint(-2, 1)));
|
REQUIRE_FALSE(isTileOccupied(sim.getFactoryState(), QPoint(-2, 1)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -663,15 +660,15 @@ TEST_CASE("Blueprint: building with no recipe has empty recipeId", "[blueprint]"
|
|||||||
|
|
||||||
TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]")
|
TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
|
// Miner body cells: (0,0),(1,0),(0,1) — all at x < 0, valid for asteroid.
|
||||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const ConstructionSite* site = sim.getBuildings().findSite(id);
|
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||||
REQUIRE(site != nullptr);
|
REQUIRE(site != nullptr);
|
||||||
REQUIRE(site->recipeId == "mine_iron_ore");
|
REQUIRE(site->recipeId == "mine_iron_ore");
|
||||||
}
|
}
|
||||||
@@ -679,11 +676,11 @@ TEST_CASE("Blueprint placement: setRecipe on construction site stores recipe", "
|
|||||||
TEST_CASE("Blueprint placement: recipe transfers to building after construction completes",
|
TEST_CASE("Blueprint placement: recipe transfers to building after construction completes",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_copper_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_copper_ore");
|
||||||
|
|
||||||
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
|
// Miner construction_time_seconds = 10 → completesAt = secondsToTicks(10) = 300.
|
||||||
// Run 301 ticks (0..300) to process the completion tick.
|
// Run 301 ticks (0..300) to process the completion tick.
|
||||||
@@ -692,7 +689,7 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
|||||||
sim.tick();
|
sim.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Building* b = sim.getBuildings().findBuilding(id);
|
const Building* b = findBuilding(sim.getFactoryState(), id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
REQUIRE(b->recipeId == "mine_copper_ore");
|
REQUIRE(b->recipeId == "mine_copper_ore");
|
||||||
}
|
}
|
||||||
@@ -705,15 +702,15 @@ TEST_CASE("Blueprint placement: recipe transfers to building after construction
|
|||||||
|
|
||||||
TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
|
// Freshly placed → a ConstructionSite (not ticked to completion). A 1x1 belt keeps
|
||||||
// the body-cell bounding-box centered on the anchor, so a single site → zero offset.
|
// the body-cell bounding-box centered on the anchor, so a single site → zero offset.
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Belt, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
@@ -724,12 +721,12 @@ TEST_CASE("Blueprint creation: a construction site is captured", "[blueprint]")
|
|||||||
|
|
||||||
TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
|
TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { id });
|
||||||
|
|
||||||
@@ -740,22 +737,22 @@ TEST_CASE("Blueprint creation: a construction site's recipe is captured", "[blue
|
|||||||
TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
|
TEST_CASE("Blueprint creation: mixed operational building and construction site are both captured",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Building A: place, configure, and tick to completion so it is operational.
|
// Building A: place, configure, and tick to completion so it is operational.
|
||||||
const BuildingId idA =
|
const BuildingId idA =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(idA != kInvalidBuildingId);
|
REQUIRE(idA != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(idA, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idA, "mine_iron_ore");
|
||||||
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
for (int i = 0; i <= static_cast<int>(secondsToTicks(10.0)); ++i) { sim.tick(); }
|
||||||
REQUIRE(sim.getBuildings().findBuilding(idA) != nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), idA) != nullptr);
|
||||||
|
|
||||||
// Building B: place and configure, but leave as a construction site.
|
// Building B: place and configure, but leave as a construction site.
|
||||||
const BuildingId idB =
|
const BuildingId idB =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-6, 0), Rotation::East).value();
|
||||||
REQUIRE(idB != kInvalidBuildingId);
|
REQUIRE(idB != kInvalidBuildingId);
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(idB, "mine_copper_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), idB, "mine_copper_ore");
|
||||||
REQUIRE(sim.getBuildings().findSite(idB) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), idB) != nullptr);
|
||||||
|
|
||||||
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
const Blueprint bp = captureBlueprintFromSelection(sim, { idA, idB });
|
||||||
|
|
||||||
@@ -772,14 +769,14 @@ TEST_CASE("Blueprint creation: mixed operational building and construction site
|
|||||||
|
|
||||||
TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
|
TEST_CASE("Blueprint creation: selectionHasPlaceableBuilding sees a construction site", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
|
REQUIRE_FALSE(selectionHasPlaceableBuilding(sim, {}));
|
||||||
|
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
|
REQUIRE(selectionHasPlaceableBuilding(sim, { id }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,7 +784,7 @@ TEST_CASE("Blueprint placement: interceptor schematic is unlocked at game start"
|
|||||||
{
|
{
|
||||||
// "interceptor" has unlock_at_station_level = -1 in the test config.
|
// "interceptor" has unlock_at_station_level = -1 in the test config.
|
||||||
// This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
|
// This confirms the guard in placeBlueprintAtTile passes for start-unlocked schematics.
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
REQUIRE(sim.isSchematicUnlocked("interceptor"));
|
REQUIRE(sim.isSchematicUnlocked("interceptor"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -796,7 +793,7 @@ TEST_CASE("Blueprint placement: repair_ship schematic is locked at game start",
|
|||||||
// "repair_ship" has unlock_at_station_level = 0 in the test config.
|
// "repair_ship" has unlock_at_station_level = 0 in the test config.
|
||||||
// This confirms the guard in placeBlueprintAtTile blocks locked schematics,
|
// This confirms the guard in placeBlueprintAtTile blocks locked schematics,
|
||||||
// leaving the shipyard's schematic unset.
|
// leaving the shipyard's schematic unset.
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
REQUIRE_FALSE(sim.isSchematicUnlocked("repair_ship"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,7 +839,7 @@ TEST_CASE("Blueprint: building without layout has nullopt shipLayout", "[bluepri
|
|||||||
|
|
||||||
TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
|
TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout", "[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
|
// Shipyard surface_mask ["AAAS>","AAAS "] with Rotation::East:
|
||||||
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
|
// A-tiles at (-3,0),(-2,0),(-1,0),(-3,1),(-2,1),(-1,1) — all x < 0, valid asteroid tiles.
|
||||||
@@ -857,9 +854,9 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
|
|||||||
pm.rotation = Rotation::East;
|
pm.rotation = Rotation::East;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||||
|
|
||||||
const ConstructionSite* site = sim.getBuildings().findSite(id);
|
const ConstructionSite* site = findSite(sim.getFactoryState(), id);
|
||||||
REQUIRE(site != nullptr);
|
REQUIRE(site != nullptr);
|
||||||
REQUIRE(site->shipLayout.has_value());
|
REQUIRE(site->shipLayout.has_value());
|
||||||
REQUIRE(site->shipLayout->placedModules.size() == 1);
|
REQUIRE(site->shipLayout->placedModules.size() == 1);
|
||||||
@@ -869,7 +866,7 @@ TEST_CASE("Blueprint placement: setShipLayout on construction site stores layout
|
|||||||
TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
|
TEST_CASE("Blueprint placement: ship layout transfers to building after construction completes",
|
||||||
"[blueprint]")
|
"[blueprint]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig());
|
Simulation sim(loadTestConfig());
|
||||||
|
|
||||||
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
|
const BuildingId id = SimulationTestAccess::place(sim,BuildingType::Shipyard, QPoint(-3, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
@@ -881,7 +878,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
|
|||||||
pm.rotation = Rotation::North;
|
pm.rotation = Rotation::North;
|
||||||
layout.placedModules.push_back(pm);
|
layout.placedModules.push_back(pm);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, layout);
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, layout);
|
||||||
|
|
||||||
// Shipyard construction_time_seconds = 30 in the test config.
|
// Shipyard construction_time_seconds = 30 in the test config.
|
||||||
double constructionTime = 0.0;
|
double constructionTime = 0.0;
|
||||||
@@ -896,7 +893,7 @@ TEST_CASE("Blueprint placement: ship layout transfers to building after construc
|
|||||||
sim.tick();
|
sim.tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
const Building* b = sim.getBuildings().findBuilding(id);
|
const Building* b = findBuilding(sim.getFactoryState(), id);
|
||||||
REQUIRE(b != nullptr);
|
REQUIRE(b != nullptr);
|
||||||
REQUIRE(b->shipLayout.has_value());
|
REQUIRE(b->shipLayout.has_value());
|
||||||
REQUIRE(b->shipLayout->placedModules.size() == 1);
|
REQUIRE(b->shipLayout->placedModules.size() == 1);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "catch.hpp"
|
#include "catch.hpp"
|
||||||
|
#include "FactoryQueries.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@
|
|||||||
#include "ShipsConfig.h"
|
#include "ShipsConfig.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
|
|
||||||
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
|
// readBuildingConfig underpins the copy-settings gesture (REQ-BLD-COPY-CONFIG):
|
||||||
// it extracts a building's recipe / schematic / layout / splitter filters so they
|
// it extracts a building's recipe / schematic / layout / splitter filters so they
|
||||||
@@ -21,11 +23,6 @@
|
|||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
|
const BuildingDef* findDef(const GameConfig& cfg, BuildingType type)
|
||||||
{
|
{
|
||||||
for (const BuildingDef& def : cfg.buildings.buildings)
|
for (const BuildingDef& def : cfg.buildings.buildings)
|
||||||
@@ -40,8 +37,7 @@ BuildingId placeOperational(Simulation& sim, const GameConfig& cfg,
|
|||||||
{
|
{
|
||||||
const BuildingDef* def = findDef(cfg, type);
|
const BuildingDef* def = findDef(cfg, type);
|
||||||
REQUIRE(def != nullptr);
|
REQUIRE(def != nullptr);
|
||||||
return SimulationTestAccess::buildings(sim).placeImmediate(
|
return SimulationTestAccess::buildings(sim).placeImmediate(SimulationTestAccess::state(sim), type, def->surfaceMask, anchor, Rotation::East);
|
||||||
type, def->surfaceMask, anchor, Rotation::East);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
||||||
@@ -66,11 +62,11 @@ const ShipDef* findAvailableSchematic(const GameConfig& cfg)
|
|||||||
|
|
||||||
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
|
TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Simulation sim(loadConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
|
|
||||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Miner, QPoint(0, 0));
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
@@ -84,8 +80,8 @@ TEST_CASE("readBuildingConfig returns a miner's selected recipe", "[copyconfig]"
|
|||||||
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
||||||
"[copyconfig]")
|
"[copyconfig]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Simulation sim(loadConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
|
|
||||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Assembler, QPoint(0, 0));
|
||||||
|
|
||||||
@@ -99,15 +95,15 @@ TEST_CASE("readBuildingConfig leaves recipe unset when nothing is selected",
|
|||||||
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
||||||
"[copyconfig]")
|
"[copyconfig]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Simulation sim(loadConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
|
|
||||||
const ShipDef* schematic = findAvailableSchematic(cfg);
|
const ShipDef* schematic = findAvailableSchematic(cfg);
|
||||||
REQUIRE(schematic != nullptr);
|
REQUIRE(schematic != nullptr);
|
||||||
|
|
||||||
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
const BuildingId id = placeOperational(sim, cfg, BuildingType::Shipyard, QPoint(0, 0));
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, schematic->id);
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, schematic->id);
|
||||||
SimulationTestAccess::buildings(sim).setShipLayout(id, ShipLayoutConfig{});
|
SimulationTestAccess::buildings(sim).setShipLayout(SimulationTestAccess::state(sim), id, ShipLayoutConfig{});
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
@@ -119,17 +115,17 @@ TEST_CASE("readBuildingConfig returns a shipyard's schematic and layout",
|
|||||||
|
|
||||||
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
Simulation sim(loadConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
|
|
||||||
// A placed miner enters the construction queue as a site (not yet operational).
|
// A placed miner enters the construction queue as a site (not yet operational).
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
REQUIRE(id != kInvalidBuildingId);
|
REQUIRE(id != kInvalidBuildingId);
|
||||||
REQUIRE(sim.getBuildings().findBuilding(id) == nullptr);
|
REQUIRE(findBuilding(sim.getFactoryState(), id) == nullptr);
|
||||||
REQUIRE(sim.getBuildings().findSite(id) != nullptr);
|
REQUIRE(findSite(sim.getFactoryState(), id) != nullptr);
|
||||||
|
|
||||||
SimulationTestAccess::buildings(sim).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(sim).setRecipe(SimulationTestAccess::state(sim), id, "mine_iron_ore");
|
||||||
|
|
||||||
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
const std::optional<BuildingConfig> config = readBuildingConfig(sim, id);
|
||||||
REQUIRE(config.has_value());
|
REQUIRE(config.has_value());
|
||||||
@@ -140,6 +136,6 @@ TEST_CASE("readBuildingConfig reads a queued construction site", "[copyconfig]")
|
|||||||
|
|
||||||
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
|
TEST_CASE("readBuildingConfig returns nullopt for an unknown id", "[copyconfig]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 7);
|
Simulation sim(loadTestConfig(), 7);
|
||||||
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
CHECK_FALSE(readBuildingConfig(sim, kInvalidBuildingId).has_value());
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@
|
|||||||
add_files(
|
add_files(
|
||||||
TEST_FILES
|
TEST_FILES
|
||||||
|
|
||||||
|
TestConfig.h
|
||||||
|
|
||||||
test.cpp
|
test.cpp
|
||||||
FormulaTest.cpp
|
FormulaTest.cpp
|
||||||
ConfigLoaderTest.cpp
|
ConfigLoaderTest.cpp
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
#include "BeltSystem.h"
|
#include "BeltSystem.h"
|
||||||
#include "Building.h"
|
#include "Building.h"
|
||||||
#include "BuildingSystem.h"
|
#include "BuildingSystem.h"
|
||||||
|
#include "FactoryState.h"
|
||||||
#include "BuildingType.h"
|
#include "BuildingType.h"
|
||||||
#include "CombatSystem.h"
|
#include "CombatSystem.h"
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
@@ -22,11 +23,7 @@
|
|||||||
#include "StationBodyComponent.h"
|
#include "StationBodyComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
#include "WeaponComponent.h"
|
#include "WeaponComponent.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
static GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const ShipDef* findCombatShip(const GameConfig& cfg)
|
static const ShipDef* findCombatShip(const GameConfig& cfg)
|
||||||
{
|
{
|
||||||
@@ -55,6 +52,7 @@ static entt::entity findWeaponChild(EntityAdmin& admin, entt::entity ship)
|
|||||||
struct CombatFixture
|
struct CombatFixture
|
||||||
{
|
{
|
||||||
GameConfig cfg;
|
GameConfig cfg;
|
||||||
|
FactoryState state = makeFactoryState(cfg);
|
||||||
std::mt19937 rng;
|
std::mt19937 rng;
|
||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
BuildingId nextBuildingId;
|
BuildingId nextBuildingId;
|
||||||
@@ -64,7 +62,7 @@ struct CombatFixture
|
|||||||
CombatSystem combat;
|
CombatSystem combat;
|
||||||
|
|
||||||
explicit CombatFixture()
|
explicit CombatFixture()
|
||||||
: cfg(loadConfig())
|
: cfg(loadTestConfig())
|
||||||
, rng(42)
|
, rng(42)
|
||||||
, nextBuildingId(1)
|
, nextBuildingId(1)
|
||||||
, belts(cfg.world.beltSpeed_tps)
|
, belts(cfg.world.beltSpeed_tps)
|
||||||
@@ -114,7 +112,7 @@ TEST_CASE("CombatSystem: ship fires when cooldown=0 and target in range", "[comb
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
||||||
@@ -147,13 +145,13 @@ TEST_CASE("CombatSystem: cooldown prevents firing before it expires", "[combat]"
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
REQUIRE_FALSE(enemyFiredIn(events));
|
REQUIRE_FALSE(enemyFiredIn(events));
|
||||||
|
|
||||||
f.combat.tick(1, f.admin, f.buildings, events);
|
f.combat.tick(1, f.admin, events);
|
||||||
REQUIRE_FALSE(enemyFiredIn(events));
|
REQUIRE_FALSE(enemyFiredIn(events));
|
||||||
|
|
||||||
f.combat.tick(2, f.admin, f.buildings, events);
|
f.combat.tick(2, f.admin, events);
|
||||||
REQUIRE(enemyFiredIn(events));
|
REQUIRE(enemyFiredIn(events));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +166,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
REQUIRE(events.empty());
|
REQUIRE(events.empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +176,7 @@ TEST_CASE("CombatSystem: no fire when target is out of range", "[combat]")
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
// Find the player station entity via ECS.
|
// Find the player station entity via ECS.
|
||||||
entt::entity stationEntity = entt::null;
|
entt::entity stationEntity = entt::null;
|
||||||
@@ -217,7 +215,7 @@ TEST_CASE("CombatSystem: player station fires at enemy ship in range", "[combat]
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
entt::entity stationEntity = entt::null;
|
entt::entity stationEntity = entt::null;
|
||||||
QVector2D stationCenter;
|
QVector2D stationCenter;
|
||||||
@@ -255,7 +253,7 @@ TEST_CASE("CombatSystem: enemy station fires at player ship in range", "[combat]
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]")
|
TEST_CASE("CombatSystem: player ship fires at enemy station in range", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
entt::entity stationEntity = entt::null;
|
entt::entity stationEntity = entt::null;
|
||||||
QVector2D stationCenter;
|
QVector2D stationCenter;
|
||||||
@@ -311,7 +309,7 @@ TEST_CASE("CombatSystem: damage not applied before impact tick", "[combat]")
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
for (Tick t = 1; t < 5; ++t)
|
for (Tick t = 1; t < 5; ++t)
|
||||||
{
|
{
|
||||||
@@ -333,7 +331,7 @@ TEST_CASE("CombatSystem: damage applied exactly at impact tick", "[combat]")
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
f.combat.applyPendingDamage(5, f.admin);
|
f.combat.applyPendingDamage(5, f.admin);
|
||||||
|
|
||||||
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
REQUIRE(f.admin.get<HealthComponent>(player).hp < hpBefore);
|
||||||
@@ -350,7 +348,7 @@ TEST_CASE("CombatSystem: damage silently dropped if target already dead", "[comb
|
|||||||
f.wireEnemyTarget(enemy, player);
|
f.wireEnemyTarget(enemy, player);
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
f.ships.despawn(player);
|
f.ships.despawn(player);
|
||||||
|
|
||||||
@@ -373,7 +371,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
|||||||
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
const float hpBefore = f.admin.get<HealthComponent>(player).hp;
|
||||||
|
|
||||||
std::vector<BeamFiredEvent> events;
|
std::vector<BeamFiredEvent> events;
|
||||||
f.combat.tick(0, f.admin, f.buildings, events);
|
f.combat.tick(0, f.admin, events);
|
||||||
|
|
||||||
f.ships.despawn(enemy);
|
f.ships.despawn(enemy);
|
||||||
|
|
||||||
@@ -388,7 +386,7 @@ TEST_CASE("CombatSystem: damage still applied if shooter already dead", "[combat
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
const ShipDef* combatDef = findCombatShip(sim.getConfig());
|
const ShipDef* combatDef = findCombatShip(sim.getConfig());
|
||||||
REQUIRE(combatDef != nullptr);
|
REQUIRE(combatDef != nullptr);
|
||||||
@@ -405,7 +403,7 @@ TEST_CASE("CombatSystem: dead ship is removed after tick step 9", "[combat]")
|
|||||||
|
|
||||||
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
// Scrap dropped on death is derived from the ship's as-built threat cost
|
// Scrap dropped on death is derived from the ship's as-built threat cost
|
||||||
// (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
|
// (REQ-RES-DEBRIS-DROP): round(threat * scrap_per_threat). The interceptor's
|
||||||
@@ -417,14 +415,14 @@ TEST_CASE("CombatSystem: scrap is spawned on ship death", "[combat]")
|
|||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
|
|
||||||
const std::vector<DebrisInfo> scraps = sim.getDebrisSystem().getAllDebrisInfo();
|
const std::vector<DebrisInfo> scraps = getAllDebrisInfo(sim.getAdmin());
|
||||||
REQUIRE(scraps.size() == 1);
|
REQUIRE(scraps.size() == 1);
|
||||||
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
|
CHECK(sim.getAdmin().get<DebrisComponent>(scraps[0].entity).amount == 59);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
TEST_CASE("CombatSystem: HQ death sets game over", "[combat]")
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), 42);
|
Simulation sim(loadTestConfig(), 42);
|
||||||
|
|
||||||
// Damage the HQ proxy entity (has HqProxy + Health).
|
// Damage the HQ proxy entity (has HqProxy + Health).
|
||||||
sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
|
sim.getAdmin().forEach<HqProxyComponent, HealthComponent>(
|
||||||
|
|||||||
@@ -11,14 +11,7 @@
|
|||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
namespace
|
|
||||||
{
|
|
||||||
GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
// The command chokepoint (Simulation::apply) must produce exactly the same state
|
// The command chokepoint (Simulation::apply) must produce exactly the same state
|
||||||
// as driving the underlying mutators directly — that equivalence is what lets a
|
// as driving the underlying mutators directly — that equivalence is what lets a
|
||||||
@@ -26,8 +19,8 @@ GameConfig loadConfig()
|
|||||||
|
|
||||||
TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
|
TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaCommand(loadConfig(), 99);
|
Simulation viaCommand(loadTestConfig(), 99);
|
||||||
Simulation viaDirect(loadConfig(), 99);
|
Simulation viaDirect(loadTestConfig(), 99);
|
||||||
|
|
||||||
PlaceBuildingCommand command;
|
PlaceBuildingCommand command;
|
||||||
command.type = BuildingType::Miner;
|
command.type = BuildingType::Miner;
|
||||||
@@ -42,8 +35,8 @@ TEST_CASE("apply(PlaceBuildingCommand) matches direct placement", "[command]")
|
|||||||
|
|
||||||
TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]")
|
TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaCommand(loadConfig(), 99);
|
Simulation viaCommand(loadTestConfig(), 99);
|
||||||
Simulation viaDirect(loadConfig(), 99);
|
Simulation viaDirect(loadTestConfig(), 99);
|
||||||
|
|
||||||
PlaceBuildingCommand command;
|
PlaceBuildingCommand command;
|
||||||
command.type = BuildingType::Miner;
|
command.type = BuildingType::Miner;
|
||||||
@@ -54,15 +47,15 @@ TEST_CASE("apply(PlaceBuildingCommand) with recipe matches place-then-setRecipe"
|
|||||||
|
|
||||||
const BuildingId id =
|
const BuildingId id =
|
||||||
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
SimulationTestAccess::place(viaDirect, BuildingType::Miner, QPoint(-2, 0), Rotation::East).value();
|
||||||
SimulationTestAccess::buildings(viaDirect).setRecipe(id, "mine_iron_ore");
|
SimulationTestAccess::buildings(viaDirect).setRecipe(SimulationTestAccess::state(viaDirect), id, "mine_iron_ore");
|
||||||
|
|
||||||
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
REQUIRE(viaCommand.computeStateChecksum() == viaDirect.computeStateChecksum());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
|
TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaCommand(loadConfig(), 99);
|
Simulation viaCommand(loadTestConfig(), 99);
|
||||||
Simulation viaDirect(loadConfig(), 99);
|
Simulation viaDirect(loadTestConfig(), 99);
|
||||||
|
|
||||||
const BuildingId idA =
|
const BuildingId idA =
|
||||||
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||||
@@ -81,8 +74,8 @@ TEST_CASE("apply(DeconstructCommand) matches direct deconstruct", "[command]")
|
|||||||
|
|
||||||
TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
|
TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstruction", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaCommand(loadConfig(), 99);
|
Simulation viaCommand(loadTestConfig(), 99);
|
||||||
Simulation viaDirect(loadConfig(), 99);
|
Simulation viaDirect(loadTestConfig(), 99);
|
||||||
|
|
||||||
const BuildingId idA =
|
const BuildingId idA =
|
||||||
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
SimulationTestAccess::place(viaCommand, BuildingType::Miner, QPoint(-3, 0), Rotation::East).value();
|
||||||
@@ -110,8 +103,8 @@ TEST_CASE("apply(CancelDeconstructionCommand) matches direct cancelDeconstructio
|
|||||||
|
|
||||||
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
|
TEST_CASE("CommandManager drains queued commands in FIFO order through apply", "[command]")
|
||||||
{
|
{
|
||||||
Simulation viaManager(loadConfig(), 99);
|
Simulation viaManager(loadTestConfig(), 99);
|
||||||
Simulation viaDirect(loadConfig(), 99);
|
Simulation viaDirect(loadTestConfig(), 99);
|
||||||
|
|
||||||
CommandManager manager(viaManager);
|
CommandManager manager(viaManager);
|
||||||
|
|
||||||
|
|||||||
@@ -116,16 +116,16 @@ TEST_CASE("DebrisSystem: collectOne depletes one scrap and keeps the debris unti
|
|||||||
|
|
||||||
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
|
const entt::entity e = ss.spawn(QVector2D(0.0f, 0.0f), 3, 100);
|
||||||
|
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
|
REQUIRE(admin.get<DebrisComponent>(e).amount == 2);
|
||||||
|
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE(admin.isValid(e));
|
REQUIRE(admin.isValid(e));
|
||||||
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
|
REQUIRE(admin.get<DebrisComponent>(e).amount == 1);
|
||||||
|
|
||||||
// Final unit collected: the debris is removed once depleted.
|
// Final unit collected: the debris is removed once depleted.
|
||||||
REQUIRE(ss.collectOne(e));
|
REQUIRE(collectOne(admin, e));
|
||||||
REQUIRE_FALSE(admin.isValid(e));
|
REQUIRE_FALSE(admin.isValid(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ TEST_CASE("DebrisSystem: collectOne returns false for an invalid entity", "[debr
|
|||||||
EntityAdmin admin;
|
EntityAdmin admin;
|
||||||
DebrisSystem ss(admin);
|
DebrisSystem ss(admin);
|
||||||
|
|
||||||
REQUIRE_FALSE(ss.collectOne(entt::null));
|
REQUIRE_FALSE(collectOne(admin, entt::null));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -149,7 +149,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo returns all spawned debris", "[debris]
|
|||||||
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||||
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||||
REQUIRE(info.size() == 2);
|
REQUIRE(info.size() == 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,7 +161,7 @@ TEST_CASE("DebrisSystem: getAllDebrisInfo reports each debris entry.s remaining
|
|||||||
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
const entt::entity a = ss.spawn(QVector2D(1.0f, 2.0f), 3, 100);
|
||||||
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
const entt::entity b = ss.spawn(QVector2D(4.0f, 5.0f), 6, 200);
|
||||||
|
|
||||||
const std::vector<DebrisInfo> info = ss.getAllDebrisInfo();
|
const std::vector<DebrisInfo> info = getAllDebrisInfo(admin);
|
||||||
REQUIRE(info.size() == 2);
|
REQUIRE(info.size() == 2);
|
||||||
for (const DebrisInfo& i : info)
|
for (const DebrisInfo& i : info)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -5,28 +5,52 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
|
#include "FactionComponent.h"
|
||||||
#include "GameConfig.h"
|
#include "GameConfig.h"
|
||||||
|
#include "HealthComponent.h"
|
||||||
#include "Rotation.h"
|
#include "Rotation.h"
|
||||||
|
#include "SchematicChoiceOption.h"
|
||||||
#include "Simulation.h"
|
#include "Simulation.h"
|
||||||
#include "SimulationTestAccess.h"
|
#include "SimulationTestAccess.h"
|
||||||
#include "StateChecksum.h"
|
#include "StateChecksum.h"
|
||||||
|
#include "StationBodyComponent.h"
|
||||||
#include "Tick.h"
|
#include "Tick.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
constexpr int kScriptTicks = 2000;
|
constexpr int kScriptTicks = 2000;
|
||||||
|
|
||||||
|
// Ticks at which the scripted session destroys the enemy stations, and the ticks
|
||||||
|
// on which the resulting schematic choice is taken. A station dying triggers the
|
||||||
|
// choice generation (REQ-DEF-SCHEMATIC-DROP), which lands during that same tick,
|
||||||
|
// so the choice is applied on the tick after.
|
||||||
|
constexpr int kFirstStationKillTick = 800;
|
||||||
|
constexpr int kFirstChoiceTick = kFirstStationKillTick + 1;
|
||||||
|
constexpr int kSecondStationKillTick = 1400;
|
||||||
|
constexpr int kSecondChoiceTick = kSecondStationKillTick + 1;
|
||||||
|
|
||||||
|
// Zeroes the HP of every enemy station, so the next tick processes their death.
|
||||||
|
void killEnemyStations(Simulation& sim)
|
||||||
|
{
|
||||||
|
sim.getAdmin().forEach<StationBodyComponent, FactionComponent, HealthComponent>(
|
||||||
|
[](entt::entity, StationBodyComponent&, FactionComponent& faction,
|
||||||
|
HealthComponent& health)
|
||||||
|
{
|
||||||
|
if (faction.isEnemy) { health.hp = 0.0f; }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Runs a fixed scripted session and returns the full-state checksum after every
|
// Runs a fixed scripted session and returns the full-state checksum after every
|
||||||
// tick. The script places a small factory, deconstructs part of it mid-run, and
|
// tick. The script places a small factory, deconstructs part of it mid-run, and
|
||||||
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
|
// otherwise lets waves/combat run so the RNG stream and ECS state are exercised.
|
||||||
|
// It also destroys the enemy stations twice and takes the offered schematic
|
||||||
|
// choice, so that unlock state (awarded groups, per-schematic levels, and the
|
||||||
|
// implicit recipe/item sets derived from them) is exercised as well and reaches
|
||||||
|
// the checksum via UnlockState::appendChecksum.
|
||||||
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
||||||
{
|
{
|
||||||
Simulation sim(loadConfig(), seed);
|
Simulation sim(loadTestConfig(), seed);
|
||||||
|
|
||||||
// Tick 0: a miner feeding a short belt line on the asteroid.
|
// Tick 0: a miner feeding a short belt line on the asteroid.
|
||||||
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-3, 0), Rotation::East);
|
SimulationTestAccess::place(sim, BuildingType::Miner, QPoint(-3, 0), Rotation::East);
|
||||||
@@ -44,6 +68,26 @@ std::vector<std::uint64_t> runScriptedSession(unsigned int seed)
|
|||||||
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
|
SimulationTestAccess::place(sim, BuildingType::Smelter, QPoint(-3, 3), Rotation::East);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (t == kFirstStationKillTick || t == kSecondStationKillTick)
|
||||||
|
{
|
||||||
|
killEnemyStations(sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (t == kFirstChoiceTick)
|
||||||
|
{
|
||||||
|
// Guarded rather than assumed: if the test config ever stops offering
|
||||||
|
// a group here, this script would silently stop covering unlock state.
|
||||||
|
REQUIRE(sim.hasSchematicChoicesPending());
|
||||||
|
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The second award is opportunistic — whether a group is still eligible
|
||||||
|
// depends on what the first one granted and on the prerequisite gating.
|
||||||
|
if (t == kSecondChoiceTick && sim.hasSchematicChoicesPending())
|
||||||
|
{
|
||||||
|
SimulationTestAccess::applySchematicChoice(sim, 0);
|
||||||
|
}
|
||||||
|
|
||||||
sim.tick();
|
sim.tick();
|
||||||
checksums.push_back(sim.computeStateChecksum());
|
checksums.push_back(sim.computeStateChecksum());
|
||||||
}
|
}
|
||||||
@@ -121,8 +165,8 @@ TEST_CASE("fingerprintRng: equal states match, advanced states differ", "[determ
|
|||||||
|
|
||||||
TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]")
|
TEST_CASE("Simulation::rngFingerprint is stable for equal seeds", "[determinism]")
|
||||||
{
|
{
|
||||||
const Simulation a(loadConfig(), 777);
|
const Simulation a(loadTestConfig(), 777);
|
||||||
const Simulation b(loadConfig(), 777);
|
const Simulation b(loadTestConfig(), 777);
|
||||||
|
|
||||||
REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
|
REQUIRE(a.getRngFingerprint() == b.getRngFingerprint());
|
||||||
}
|
}
|
||||||
@@ -156,3 +200,44 @@ TEST_CASE("Simulation: different seeds diverge in state checksum", "[determinism
|
|||||||
// the RNG-driven divergence; a constant checksum would be a broken hash).
|
// the RNG-driven divergence; a constant checksum would be a broken hash).
|
||||||
REQUIRE(a != b);
|
REQUIRE(a != b);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Unlock state coverage
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
TEST_CASE("Simulation: unlock state contributes to the state checksum",
|
||||||
|
"[determinism][unlock]")
|
||||||
|
{
|
||||||
|
// Two sessions with identical history up to the schematic choice; only one
|
||||||
|
// takes the choice. This pins down that awarding an unlock group actually
|
||||||
|
// reaches the checksum, which the scripted-session tests above rely on but
|
||||||
|
// cannot show on their own: they would still pass if UnlockState were left
|
||||||
|
// out of the fold entirely.
|
||||||
|
Simulation taken(loadTestConfig(), 12345u);
|
||||||
|
Simulation skipped(loadTestConfig(), 12345u);
|
||||||
|
|
||||||
|
for (int t = 0; t < kFirstStationKillTick; ++t)
|
||||||
|
{
|
||||||
|
taken.tick();
|
||||||
|
skipped.tick();
|
||||||
|
}
|
||||||
|
killEnemyStations(taken);
|
||||||
|
killEnemyStations(skipped);
|
||||||
|
taken.tick();
|
||||||
|
skipped.tick();
|
||||||
|
|
||||||
|
// In lockstep before the choice, so the divergence below has one cause.
|
||||||
|
REQUIRE(taken.computeStateChecksum() == skipped.computeStateChecksum());
|
||||||
|
|
||||||
|
REQUIRE(taken.hasSchematicChoicesPending());
|
||||||
|
|
||||||
|
// An artifact choice bumps m_artifactCount, which is folded separately; the
|
||||||
|
// divergence would then not be attributable to unlock state.
|
||||||
|
REQUIRE_FALSE(taken.getPendingSchematicChoices()[0].isArtifact);
|
||||||
|
|
||||||
|
SimulationTestAccess::applySchematicChoice(taken, 0);
|
||||||
|
|
||||||
|
// The pending-choice list is not itself folded into the checksum, so the
|
||||||
|
// only state that changed is the unlock bookkeeping.
|
||||||
|
REQUIRE(taken.computeStateChecksum() != skipped.computeStateChecksum());
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,11 +2,7 @@
|
|||||||
|
|
||||||
#include "ConfigLoader.h"
|
#include "ConfigLoader.h"
|
||||||
#include "ModulesConfig.h"
|
#include "ModulesConfig.h"
|
||||||
|
#include "TestConfig.h"
|
||||||
static GameConfig loadConfig()
|
|
||||||
{
|
|
||||||
return ConfigLoader::loadFromDirectory(CONFIG_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
|
static const ModuleDef* findModule(const GameConfig& cfg, const std::string& id)
|
||||||
{
|
{
|
||||||
@@ -28,7 +24,7 @@ static const ModuleStatModifier* findModifier(const ModuleDef& def, const std::s
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
REQUIRE(cfg.modules.modules.size() >= 2);
|
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||||
|
|
||||||
const ModuleDef& armor = cfg.modules.modules[0];
|
const ModuleDef& armor = cfg.modules.modules[0];
|
||||||
@@ -49,7 +45,7 @@ TEST_CASE("ConfigLoader: loadModules parses modules.toml", "[config][modules]")
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
REQUIRE(cfg.modules.modules.size() >= 2);
|
REQUIRE(cfg.modules.modules.size() >= 2);
|
||||||
|
|
||||||
const ModuleDef& sensor = cfg.modules.modules[1];
|
const ModuleDef& sensor = cfg.modules.modules[1];
|
||||||
@@ -62,7 +58,7 @@ TEST_CASE("ConfigLoader: loadModules parses additive modifiers", "[config][modul
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (weapon_primer)", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
const ModuleDef* primer = findModule(cfg, "weapon_primer");
|
const ModuleDef* primer = findModule(cfg, "weapon_primer");
|
||||||
REQUIRE(primer != nullptr);
|
REQUIRE(primer != nullptr);
|
||||||
REQUIRE(primer->statModifiers.size() == 1);
|
REQUIRE(primer->statModifiers.size() == 1);
|
||||||
@@ -74,7 +70,7 @@ TEST_CASE("ConfigLoader: multiplicative modifier with unit suffix is parsed (wea
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modifiers", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
|
const ModuleDef* stab = findModule(cfg, "weapon_stabilizer");
|
||||||
REQUIRE(stab != nullptr);
|
REQUIRE(stab != nullptr);
|
||||||
REQUIRE(stab->statModifiers.size() == 2);
|
REQUIRE(stab->statModifiers.size() == 2);
|
||||||
@@ -92,7 +88,7 @@ TEST_CASE("ConfigLoader: weapon_stabilizer parses two multiplicative weapon modi
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive main_acceleration", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
const ModuleDef* ab = findModule(cfg, "afterburner");
|
const ModuleDef* ab = findModule(cfg, "afterburner");
|
||||||
REQUIRE(ab != nullptr);
|
REQUIRE(ab != nullptr);
|
||||||
REQUIRE(ab->statModifiers.size() == 2);
|
REQUIRE(ab->statModifiers.size() == 2);
|
||||||
@@ -110,7 +106,7 @@ TEST_CASE("ConfigLoader: afterburner parses multiplicative speed and additive ma
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and additive maneuvering_acceleration", "[config][modules]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
|
const ModuleDef* mt = findModule(cfg, "maneuvering_thrusters");
|
||||||
REQUIRE(mt != nullptr);
|
REQUIRE(mt != nullptr);
|
||||||
REQUIRE(mt->statModifiers.size() == 2);
|
REQUIRE(mt->statModifiers.size() == 2);
|
||||||
@@ -128,7 +124,7 @@ TEST_CASE("ConfigLoader: maneuvering_thrusters parses multiplicative speed and a
|
|||||||
|
|
||||||
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
TEST_CASE("ConfigLoader: loadShips parses layout field", "[config][ships]")
|
||||||
{
|
{
|
||||||
const GameConfig cfg = loadConfig();
|
const GameConfig cfg = loadTestConfig();
|
||||||
REQUIRE(!cfg.ships.ships.empty());
|
REQUIRE(!cfg.ships.ships.empty());
|
||||||
|
|
||||||
const ShipDef& ship = cfg.ships.ships[0];
|
const ShipDef& ship = cfg.ships.ships[0];
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user