Add a Main Menu + save browser, replacing auto-generate and the one-slot save
The app used to always generate a world straight from planet.cfg on launch and F5 always overwrote a single fixed planet.save. Now it opens on a full-screen Main Menu (Home / Settings / Load World) so every PlanetConfig field, the seed, starting a new world, and browsing saves are all reachable without hand-editing planet.cfg, and saves are timestamped so nothing is silently overwritten. A toolbar button reopens the menu later without disturbing a running world. The settings editor is generic (no per-field UI code): configFieldTable() reuses the existing CONFIG_FIELDS X-macro to build a runtime field table, grouped into 25 categories via an explicit name->category lookup (a "first field of each section" boundary scan was tried first and is wrong, since CONFIG_FIELDS emits all doubles, then all ints, then the seed -- not struct declaration order, so most categories aren't contiguous in that order). Save v25 adds the viewer's colour-mode/overlay state, kept out of Planet::writeState/readState's signature so it touches none of the existing headless tests. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TMfyZv91tonDnPJqbaTVJE
This commit is contained in:
parent
78e58171e5
commit
e24326b735
2
.gitignore
vendored
2
.gitignore
vendored
@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Runtime-generated, machine-specific
|
# Runtime-generated, machine-specific
|
||||||
planet.cfg
|
planet.cfg
|
||||||
planet.save
|
*.save
|
||||||
screenshot*.png
|
screenshot*.png
|
||||||
|
|
||||||
# Claude Code local (machine-specific) settings
|
# Claude Code local (machine-specific) settings
|
||||||
|
|||||||
45
BUILD.md
45
BUILD.md
@ -72,7 +72,7 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
F fast-forward Phase-1 forming to settled (instant)
|
F fast-forward Phase-1 forming to settled (instant)
|
||||||
R reseed planet (restart forming)
|
R reseed planet (restart forming)
|
||||||
+ / - subdivision level (detail), 1..7
|
+ / - subdivision level (detail), 1..7
|
||||||
F5 / F9 save / load full state (planet.save)
|
F5 / F9 save / load full state (writes/reads a timestamped world_<time>.save)
|
||||||
F12 screenshot to screenshot.png
|
F12 screenshot to screenshot.png
|
||||||
F11 export the 2D map as a high-resolution (~4K) PNG (also a toolbar button)
|
F11 export the 2D map as a high-resolution (~4K) PNG (also a toolbar button)
|
||||||
Shift+F11 export a labeled atlas (~8K, every place/settlement name, no overlap; toolbar button)
|
Shift+F11 export a labeled atlas (~8K, every place/settlement name, no overlap; toolbar button)
|
||||||
@ -80,28 +80,39 @@ the full ~2.8x speedup; the default uses all cores for no extra gain:
|
|||||||
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
|
F1 toggle the clickable toolbar menu (top-right of the 3D globe; a button/
|
||||||
checkbox/dropdown for every key above, built with raygui)
|
checkbox/dropdown for every key above, built with raygui)
|
||||||
|
|
||||||
CLI flags (applied before the first load/generate):
|
The app opens on a full-screen **Main Menu** (not a live world): a Home tab (New
|
||||||
|
World / Back to World / seed + Randomize), a Settings tab editing every one of the
|
||||||
|
~240 PlanetConfig fields (grouped into 25 categories), and a Load World tab listing
|
||||||
|
every save. A toolbar "Main Menu" button (World/Time section) reopens it later
|
||||||
|
without disturbing a running world.
|
||||||
|
|
||||||
|
CLI flags (applied before the Main Menu is shown):
|
||||||
|
|
||||||
--seed N override the config seed
|
--seed N override the config seed
|
||||||
--config PATH use an alternate config file instead of planet.cfg
|
--config PATH use an alternate config file instead of planet.cfg
|
||||||
|
|
||||||
## Files (written in the working directory)
|
## Files (written in the working directory)
|
||||||
|
|
||||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
planet.cfg human-editable key=value config of every PlanetConfig
|
||||||
auto-created on first run, reload live with F2. Range-checked on
|
parameter; auto-created on first run, reload live with F2
|
||||||
load; an invalid file reverts to safe defaults (not overwritten).
|
or edit any field in the Main Menu's Settings tab.
|
||||||
planet.save binary snapshot (versioned, currently v22: +diplomacy; v21
|
Range-checked on load/New World; invalid settings revert
|
||||||
+conflict/wars; v20 +settlements; v19 +ecoregions; v18
|
to safe defaults (planet.cfg itself is not overwritten).
|
||||||
+geography salt; v17 +geography/atlas; v16 +event log; v15
|
world_<time>.save one binary snapshot per save (versioned, currently v25:
|
||||||
+stateful volcanoes; v13 +Live World clock rate; v12
|
+viewer colour-mode/overlay state; v24 +edit-mode locks;
|
||||||
+step-back history (~40 frames, so a load can rewind storms);
|
v23 +cultural evolution; v22 +diplomacy; v21
|
||||||
v11 +weather systems/storms; v10 +weather fields; v9 +moons;
|
+conflict/wars; v20 +settlements; v19 +ecoregions; v18
|
||||||
v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
|
+geography salt; v17 +geography/atlas; v16 +event log; v15
|
||||||
writes it, F9 reloads and resumes deterministically. As of v6
|
+stateful volcanoes; v13 +Live World clock rate; v12
|
||||||
the config is stored as a self-describing key=value block (like
|
+step-back history (~40 frames, so a load can rewind storms);
|
||||||
planet.cfg), so adding/removing config fields no longer breaks saves
|
v11 +weather systems/storms; v10 +weather fields; v9 +moons;
|
||||||
(unknown keys ignored, missing keys default). v6 cannot load pre-v6
|
v8 +Live World clock; v7 +biota): seed + config + full planet
|
||||||
saves (one-time break) -- regenerate them.
|
state. F5/"Save" always writes a fresh timestamped file;
|
||||||
|
F9/"Load" (or the Main Menu's Load World tab) picks/lists
|
||||||
|
them. As of v6 the config is stored as a self-describing
|
||||||
|
key=value block (like planet.cfg), so adding/removing config
|
||||||
|
fields no longer breaks saves (unknown keys ignored, missing
|
||||||
|
keys default). v6 cannot load pre-v6 saves (one-time break).
|
||||||
|
|
||||||
## Config (planet.cfg, or PlanetConfig defaults in code)
|
## Config (planet.cfg, or PlanetConfig defaults in code)
|
||||||
|
|
||||||
|
|||||||
88
CLAUDE.md
88
CLAUDE.md
@ -501,6 +501,57 @@ on the Live World clock). **Steps 1–7 of the roadmap are done (plus a derived
|
|||||||
Verified live: one continent now shows six distinct peoples instead of a single dominant culture,
|
Verified live: one continent now shows six distinct peoples instead of a single dominant culture,
|
||||||
with realms still correctly mono-cultural. `src/sim` + `src/render`; cultures are derived/not saved,
|
with realms still correctly mono-cultural. `src/sim` + `src/render`; cultures are derived/not saved,
|
||||||
so no save-format version bump.
|
so no save-format version bump.
|
||||||
|
- **Main Menu + a proper save browser (was: always auto-generate, one fixed save slot)** *(done — see
|
||||||
|
`src/render/MainMenu.{hpp,cpp}`, `Viewer::startNewWorld()`, save v25)* — the app used to always
|
||||||
|
generate a world straight from `planet.cfg` on launch, and `F5` always overwrote the same
|
||||||
|
`planet.save`. Now it opens on a full-screen **Main Menu** (`AppScreen::MainMenu`, a complete early
|
||||||
|
return in `renderFrame()`/`handleInput()` — not a dim overlay like the Phase-3 modal, since the menu
|
||||||
|
must render before `planet.generate()` has ever run): a **Home** tab (New World / Back to World —
|
||||||
|
the latter only once a world exists — a prominent Seed field + Randomize, and status text), a
|
||||||
|
**Settings** tab editing **every one of the ~240 `PlanetConfig` fields**, and a **Load World** tab
|
||||||
|
listing every `*.save` file. Reachable again later via a toolbar "Main Menu" button (World/Time
|
||||||
|
section) without disturbing a running world — "Back to World" is a pure screen-flag flip. **Saves
|
||||||
|
are now timestamped**: `F5`/"Save" always writes a fresh `world_<timestamp>.save`
|
||||||
|
(`makeTimestampedSavePath()`, the same `strftime` idiom as the map/atlas export); `F9`/"Load" picks
|
||||||
|
the newest via `listSaveFiles()` (`LoadDirectoryFilesEx(".", ".save", false)`); the fixed
|
||||||
|
`SAVE_PATH` member is gone. **A generic settings editor drives all 240 fields with no per-field
|
||||||
|
code**: `Planet::configFieldTable()` (`src/sim/PlanetIO.cpp`) reuses the existing
|
||||||
|
`CONFIG_FIELDS(D, I, U)` X-macro a third time to build a runtime `vector<ConfigFieldDesc>` (name +
|
||||||
|
category + type-erased get/set lambdas) — the same "add a field, everything downstream keeps
|
||||||
|
working" property `writeConfigFields()`/`parseConfigStream()` already give `planet.cfg`/saves.
|
||||||
|
Fields are grouped into **25 categories** via an explicit `name → category` lookup table
|
||||||
|
(`kConfigFieldCategory`, hand-verified against `PlanetConfig`'s actual declaration order in
|
||||||
|
`PlanetTypes.hpp`) — **not** a "first field of each section" boundary scan over
|
||||||
|
`configFieldTable()`'s own iteration order, which was tried first and is a real trap: `CONFIG_FIELDS`
|
||||||
|
expands to all `D()` fields, then all `I()` fields, then `U(seed)`, so a category whose fields mix
|
||||||
|
types (nearly all of them — e.g. "Plate lifecycle" has 9 doubles early and 5 ints in the separate
|
||||||
|
int run) is **not contiguous** in that order; a boundary scan silently merged everything after the
|
||||||
|
first non-contiguous category into one bucket. `configCategoryList()` returns the 25 labels in
|
||||||
|
display order, derived from `kConfigFieldCategory`'s own (genuinely contiguous, hand-written) block
|
||||||
|
order. The Settings tab is a left category rail + a `GuiScrollPanel` field list (the exact
|
||||||
|
scroll/scissor pattern from `Toolbar.cpp`); each row is a generic `drawConfigFieldRow()` modeled on
|
||||||
|
Edit Mode's `editRow()` (`Panels.cpp`) minus the lock column, and `handleMainMenuInput()`'s
|
||||||
|
click-to-focus/type/Enter-commits/Escape-cancels loop mirrors Edit Mode's typed-numeric-entry
|
||||||
|
handling (`ViewerInput.cpp`) exactly — click a field, type a new value, Enter calls
|
||||||
|
`Viewer::commitMenuField()` (a `std::stod` in a `try/catch`, silently discarding on a bad number,
|
||||||
|
same as `commitEditField()`). No live bounds-checking while typing: `validateConfig()` only runs at
|
||||||
|
"New World" time (same as the existing `F2` behaviour), reverting to defaults on failure. **Save v25**
|
||||||
|
appends the viewer's **colour mode + all overlay toggles** (`showBorders`/`showDrift`/.../
|
||||||
|
`showRealms`, 13 bits) so a loaded world shows what it looked like when saved, not just its
|
||||||
|
simulation state — deliberately kept **out of** `Planet::writeState`/`readState`'s 15-`bool hasX`
|
||||||
|
signature (same idiom already used there for `settled`/`phase3`/`liveWorld`/`liveRate`) so this
|
||||||
|
purely-viewer-side addition touches zero of the ~18 `test_*.cpp` files that call `readState`
|
||||||
|
directly; instead it's written/read directly in `Viewer::saveGame()`/`loadGame()`, positioned after
|
||||||
|
the existing event-journal block (a live bug caught the position mismatch: the read was first
|
||||||
|
wired in right after the header fields while the write happens at the very end of the file, which
|
||||||
|
silently desynced every read after it and broke the *unrelated* `test_events` suite too — moving
|
||||||
|
the read to match the write's actual position fixed both). Camera angle, map zoom/pan, selected
|
||||||
|
cell and the Live-info tab stay session-local, matching the user's explicit scope call. New headless
|
||||||
|
`test_viewerstate.cpp` (modeled on `test_events.cpp`): the v25 round-trip, plus a version-patched
|
||||||
|
negative test confirming a pre-v25 save loads without crashing and leaves mode/overlays untouched.
|
||||||
|
`src/sim` (`Planet.hpp`, `PlanetIO.cpp`) + `src/render` (`Viewer.{hpp,cpp}`, `ViewerInput.cpp`,
|
||||||
|
`ViewerRender.cpp`, `Toolbar.cpp`, new `MainMenu.{hpp,cpp}`); `.gitignore`'s `planet.save` line
|
||||||
|
became `*.save`.
|
||||||
|
|
||||||
## Current state
|
## Current state
|
||||||
|
|
||||||
@ -923,6 +974,8 @@ src/
|
|||||||
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
||||||
Panels.* right-column UI: detail panel, hover info, world stats, edit-mode panel
|
Panels.* right-column UI: detail panel, hover info, world stats, edit-mode panel
|
||||||
Toolbar.* clickable raygui sidebar menu (mouse-friendly counterpart to the key list)
|
Toolbar.* clickable raygui sidebar menu (mouse-friendly counterpart to the key list)
|
||||||
|
MainMenu.* full-screen Main Menu: generic settings editor (all PlanetConfig fields), seed,
|
||||||
|
New World / Load World / Back to World (AppScreen::MainMenu vs. World)
|
||||||
RayGuiImpl.cpp the one RAYGUI_IMPLEMENTATION translation unit (raygui.h is declaration-only elsewhere)
|
RayGuiImpl.cpp the one RAYGUI_IMPLEMENTATION translation unit (raygui.h is declaration-only elsewhere)
|
||||||
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
|
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
|
||||||
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
|
ViewerInput.cpp handleInput(): camera, hover picking, click, keys
|
||||||
@ -1046,7 +1099,10 @@ on a settled world; re-press regenerates) · `W` enter/leave **Live World** (set
|
|||||||
settlement — with per-field lock against automatic recompute; see below) ·
|
settlement — with per-field lock against automatic recompute; see below) ·
|
||||||
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
|
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
|
||||||
button/checkbox/dropdown there — see below) ·
|
button/checkbox/dropdown there — see below) ·
|
||||||
`+`/`-` subdivision level (1..7) · `F5` save (`planet.save`) · `F9` load ·
|
`+`/`-` subdivision level (1..7) · `F5` save (writes a fresh `world_<timestamp>.save`) · `F9` load
|
||||||
|
(the newest save) · a toolbar "Main Menu" button (World/Time section) opens the full-screen **Main
|
||||||
|
Menu** — edit any of the ~240 `planet.cfg` fields, set a seed, start a New World or browse/Load
|
||||||
|
World — without disturbing a running world (see below) ·
|
||||||
`F12` screenshot (`screenshot.png`) · `F11` export the 2D map as a high-resolution (~4K)
|
`F12` screenshot (`screenshot.png`) · `F11` export the 2D map as a high-resolution (~4K)
|
||||||
`map_export_<timestamp>.png` (also a toolbar button) · `Shift+F11` export a labeled reference atlas
|
`map_export_<timestamp>.png` (also a toolbar button) · `Shift+F11` export a labeled reference atlas
|
||||||
(~8K, every geography feature + settlement name, no overlapping text — also a toolbar button) ·
|
(~8K, every geography feature + settlement name, no overlapping text — also a toolbar button) ·
|
||||||
@ -1101,15 +1157,20 @@ Mode. Locked out (`GuiDisable`d) while the Phase-3 "start hydrology?" prompt is
|
|||||||
everything else.
|
everything else.
|
||||||
|
|
||||||
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
CLI: `--seed N` overrides `cfg.seed`; `--config PATH` uses an alternate config
|
||||||
file (both applied before the initial load/generate).
|
file (both applied before the initial load/generate). The app opens on the **Main
|
||||||
|
Menu** rather than auto-generating a world; `planet.generate()` runs only once you
|
||||||
|
pick New World or Load World there (see the Main Menu entry above).
|
||||||
|
|
||||||
Two files in the working dir: `planet.cfg` (human-editable key=value of every
|
`planet.cfg` (human-editable key=value of every PlanetConfig param, auto-created
|
||||||
PlanetConfig param, auto-created on first run, reload with `F2`) and
|
on first run, reload with `F2` or edit any field live in the Main Menu's Settings
|
||||||
`planet.save` (binary: seed + config + full planet state, written/read by
|
tab) plus **one `world_<timestamp>.save` file per save** in the working dir
|
||||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
(binary: seed + config + full planet state, written/read by
|
||||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
`Planet::writeState`/`readState`, resumes deterministically) — `F5`/the toolbar
|
||||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
"Save" button always write a fresh timestamped file, `F9`/"Load" (or the Main
|
||||||
save header is versioned (currently **24**; v2 adds the `[`/`]` drift rate, v3 a
|
Menu's Load World tab) picks/lists them via `listSaveFiles()`. Config is
|
||||||
|
range-checked by `validateConfig()` on load/`F2`/New World; an invalid file/settings
|
||||||
|
reverts to safe defaults (without overwriting your `planet.cfg`) and shows a status
|
||||||
|
message. The save header is versioned (currently **25**; v2 adds the `[`/`]` drift rate, v3 a
|
||||||
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
`phase3` flag, v4 a per-cell biome byte, v6 stores config as a **self-describing
|
||||||
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
key=value text block** instead of a raw POD dump, v7 appends the **biota population**
|
||||||
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
block — three Organism lists per cell, gated by a flag byte, v8 appends the **Live World**
|
||||||
@ -1130,7 +1191,10 @@ counter (cultural evolution), likewise per-frame (per-settlement vector + the li
|
|||||||
truncates, a replay re-creates), and v24 the **edit mode** block — a per-cell `editLock` bitmask
|
truncates, a replay re-creates), and v24 the **edit mode** block — a per-cell `editLock` bitmask
|
||||||
(manual-edit locks, `F3`) as a trailing vector plus sparse locked-value maps for the climate/biota-
|
(manual-edit locks, `F3`) as a trailing vector plus sparse locked-value maps for the climate/biota-
|
||||||
density/habitability fields (which have no other persistent storage — the map only holds an entry
|
density/habitability fields (which have no other persistent storage — the map only holds an entry
|
||||||
while its lock bit is set);
|
while its lock bit is set), and v25 the viewer's **colour mode + overlay toggles**
|
||||||
|
(kept out of `Planet::writeState`/`readState`'s signature — written/read directly in
|
||||||
|
`Viewer::saveGame()`/`loadGame()`, so it doesn't touch the ~18 `test_*.cpp` files that
|
||||||
|
call `readState` directly);
|
||||||
newer-than-supported is
|
newer-than-supported is
|
||||||
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
rejected. Older saves (no biota block) load fine with an empty population (press `L`);
|
||||||
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
pre-v8 saves load with Live World off; pre-v9 saves synthesize moons from the seed; pre-v10
|
||||||
@ -1143,7 +1207,9 @@ demand via `M`); pre-v19 saves load with no ecoregions (regenerated via `E`); pr
|
|||||||
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
no settlements (re-seeded via `U`); pre-v21 saves load with no wars (everyone independent; wars begin
|
||||||
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs);
|
again as the clock runs); pre-v22 saves load with no diplomacy (relations re-form as the clock runs);
|
||||||
pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same
|
pre-v23 saves load with no culture state (re-seeded one-per-continent on the next refresh — the same
|
||||||
peoples as before; evolution starts from there); pre-v24 saves load with no manual edits/locks.
|
peoples as before; evolution starts from there); pre-v24 saves load with no manual edits/locks;
|
||||||
|
pre-v25 saves load with the colour mode/overlays left at whatever the Viewer already held (same
|
||||||
|
"session state survives a load" behaviour saves have always had for anything not in the file).
|
||||||
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
A load drops any **stale** pre-load `wxUndo` history and reloads the
|
||||||
saved one.
|
saved one.
|
||||||
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
**As of v6, adding/removing PlanetConfig fields no longer breaks saves** — the saved
|
||||||
|
|||||||
@ -63,6 +63,7 @@ set(RENDER_SOURCES
|
|||||||
src/render/Picking.cpp
|
src/render/Picking.cpp
|
||||||
src/render/Panels.cpp
|
src/render/Panels.cpp
|
||||||
src/render/Toolbar.cpp
|
src/render/Toolbar.cpp
|
||||||
|
src/render/MainMenu.cpp
|
||||||
src/render/RayGuiImpl.cpp
|
src/render/RayGuiImpl.cpp
|
||||||
src/render/Viewer.cpp
|
src/render/Viewer.cpp
|
||||||
src/render/ViewerInput.cpp
|
src/render/ViewerInput.cpp
|
||||||
@ -106,3 +107,11 @@ if(UNIX AND NOT APPLE)
|
|||||||
target_link_libraries(test_events PRIVATE m pthread dl)
|
target_link_libraries(test_events PRIVATE m pthread dl)
|
||||||
endif()
|
endif()
|
||||||
add_test(NAME events COMMAND test_events)
|
add_test(NAME events COMMAND test_events)
|
||||||
|
|
||||||
|
add_executable(test_viewerstate test_viewerstate.cpp ${RENDER_SOURCES})
|
||||||
|
target_include_directories(test_viewerstate PRIVATE src/sim src/render ${raygui_SOURCE_DIR}/src)
|
||||||
|
target_link_libraries(test_viewerstate PRIVATE planetsim_sim raylib)
|
||||||
|
if(UNIX AND NOT APPLE)
|
||||||
|
target_link_libraries(test_viewerstate PRIVATE m pthread dl)
|
||||||
|
endif()
|
||||||
|
add_test(NAME viewerstate COMMAND test_viewerstate)
|
||||||
|
|||||||
275
src/render/MainMenu.cpp
Normal file
275
src/render/MainMenu.cpp
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
#include "MainMenu.hpp"
|
||||||
|
#include "Viewer.hpp"
|
||||||
|
#include "raygui.h"
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
// The full-screen Main Menu (see MainMenu.hpp). Layout is a pure function of screenW/screenH (no
|
||||||
|
// world needs to exist), so geometry helpers here are shared verbatim between drawMainMenu() (which
|
||||||
|
// rebuilds the click-target rect lists in MainMenuState every frame -- same idiom as
|
||||||
|
// Panels.cpp's edit panel) and handleMainMenuInput() (which reads them back). Field editing mirrors
|
||||||
|
// Edit Mode's click-to-type interaction (Panels.cpp::editRow / ViewerInput.cpp's typing loop /
|
||||||
|
// Viewer::commitEditField) but is driven generically by configFieldTable() instead of a hand-written
|
||||||
|
// row per field.
|
||||||
|
namespace {
|
||||||
|
const float HEADER_H = 64.0f;
|
||||||
|
const float TAB_W = 160.0f, TAB_H = 32.0f, TAB_GAP = 8.0f, TAB_X0 = 260.0f;
|
||||||
|
|
||||||
|
struct TabDef { MenuTab tab; const char* label; };
|
||||||
|
const TabDef kTabs[] = {
|
||||||
|
{ MenuTab::Home, "Home" },
|
||||||
|
{ MenuTab::Settings, "Settings" },
|
||||||
|
{ MenuTab::LoadWorld, "Load World" },
|
||||||
|
};
|
||||||
|
|
||||||
|
Rectangle tabButtonRect(int i) {
|
||||||
|
return Rectangle{ TAB_X0 + i * (TAB_W + TAB_GAP), 16.0f, TAB_W, TAB_H };
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle mainMenuBody(const Viewer& v) {
|
||||||
|
return Rectangle{ 0.0f, HEADER_H, (float)v.screenW, (float)v.screenH - HEADER_H };
|
||||||
|
}
|
||||||
|
|
||||||
|
// configFieldTable() index of the "seed" field (U(seed), the CONFIG_FIELDS token name),
|
||||||
|
// resolved once and shown prominently on the Home tab.
|
||||||
|
int seedFieldIndex() {
|
||||||
|
static const int idx = [] {
|
||||||
|
const auto& t = configFieldTable();
|
||||||
|
for (size_t i = 0; i < t.size(); ++i)
|
||||||
|
if (std::strcmp(t[i].name, "seed") == 0) return (int)i;
|
||||||
|
return -1;
|
||||||
|
}();
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw one editable config-field row: a label + a value box (click to type, like Edit Mode's
|
||||||
|
// editRow() in Panels.cpp, minus the lock column -- config fields have no lock concept).
|
||||||
|
// Records the row's hit rect + table index into m.fieldRects/fieldIndices for the click handler.
|
||||||
|
int drawConfigFieldRow(MainMenuState& m, int fieldIdx, const ConfigFieldDesc& d, double value,
|
||||||
|
int x, int y, int w) {
|
||||||
|
Rectangle r{ (float)x, (float)y, (float)w, 22.0f };
|
||||||
|
bool active = (m.editingConfigField == fieldIdx);
|
||||||
|
DrawRectangleRec(r, active ? Color{40, 46, 64, 255} : Color{22, 24, 34, 255});
|
||||||
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
||||||
|
DrawText(d.name, (int)r.x + 4, (int)r.y + 3, 14, Color{190, 195, 210, 255});
|
||||||
|
std::string val = active ? (m.editBuffer + "_")
|
||||||
|
: (d.isInteger ? TextFormat("%.0f", value) : TextFormat("%.4g", value));
|
||||||
|
int vw = MeasureText(val.c_str(), 14);
|
||||||
|
DrawText(val.c_str(), (int)(r.x + r.width) - vw - 6, (int)r.y + 3, 14,
|
||||||
|
active ? Color{255, 230, 140, 255} : RAYWHITE);
|
||||||
|
m.fieldRects.push_back(r);
|
||||||
|
m.fieldIndices.push_back(fieldIdx);
|
||||||
|
return y + 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fixed geometry for the Home tab's non-field buttons (New World / Back to World / Randomize)
|
||||||
|
// -- shared verbatim by drawHomeTab() and its click handler so they can never drift apart.
|
||||||
|
struct HomeLayout { Rectangle newBtn, backBtn, seedRow, randBtn; };
|
||||||
|
HomeLayout computeHomeLayout(Rectangle body) {
|
||||||
|
HomeLayout L;
|
||||||
|
float x = body.x + 40.0f, y = body.y + 90.0f;
|
||||||
|
L.newBtn = Rectangle{ x, y, 220.0f, 44.0f };
|
||||||
|
L.backBtn = Rectangle{ x + 220.0f + 16.0f, y, 220.0f, 44.0f };
|
||||||
|
y += 44.0f + 66.0f;
|
||||||
|
L.seedRow = Rectangle{ x, y, 280.0f, 30.0f };
|
||||||
|
L.randBtn = Rectangle{ x + 280.0f + 12.0f, y, 140.0f, 30.0f };
|
||||||
|
return L;
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawHomeTab(Viewer& v, Rectangle body) {
|
||||||
|
MainMenuState& m = v.menuState;
|
||||||
|
HomeLayout L = computeHomeLayout(body);
|
||||||
|
int x = (int)body.x + 40;
|
||||||
|
|
||||||
|
DrawText("World Creation", x, (int)body.y + 40, 26, RAYWHITE);
|
||||||
|
|
||||||
|
DrawRectangleRec(L.newBtn, Color{70, 110, 70, 255});
|
||||||
|
DrawRectangleLinesEx(L.newBtn, 1, Color{110, 160, 110, 255});
|
||||||
|
DrawText("New World", (int)L.newBtn.x + 20, (int)L.newBtn.y + 12, 18, RAYWHITE);
|
||||||
|
|
||||||
|
bool canBack = m.worldEverGenerated;
|
||||||
|
DrawRectangleRec(L.backBtn, canBack ? Color{60, 70, 100, 255} : Color{34, 36, 44, 255});
|
||||||
|
DrawRectangleLinesEx(L.backBtn, 1, canBack ? Color{100, 120, 160, 255} : Color{60, 62, 70, 255});
|
||||||
|
DrawText("Back to World", (int)L.backBtn.x + 16, (int)L.backBtn.y + 12, 18,
|
||||||
|
canBack ? RAYWHITE : Color{110, 112, 120, 255});
|
||||||
|
|
||||||
|
DrawText("Seed", x, (int)L.seedRow.y - 22, 18, Color{190, 195, 210, 255});
|
||||||
|
int si = seedFieldIndex();
|
||||||
|
if (si >= 0) {
|
||||||
|
const auto& t = configFieldTable();
|
||||||
|
double val = t[si].get(v.cfg);
|
||||||
|
bool active = (m.editingConfigField == si);
|
||||||
|
DrawRectangleRec(L.seedRow, active ? Color{40, 46, 64, 255} : Color{22, 24, 34, 255});
|
||||||
|
DrawRectangleLinesEx(L.seedRow, 1, Color{70, 74, 90, 255});
|
||||||
|
std::string vs = active ? (m.editBuffer + "_") : TextFormat("%.0f", val);
|
||||||
|
DrawText(vs.c_str(), (int)L.seedRow.x + 8, (int)L.seedRow.y + 6, 18,
|
||||||
|
active ? Color{255, 230, 140, 255} : RAYWHITE);
|
||||||
|
m.fieldRects.push_back(L.seedRow);
|
||||||
|
m.fieldIndices.push_back(si);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrawRectangleRec(L.randBtn, Color{50, 50, 70, 255});
|
||||||
|
DrawRectangleLinesEx(L.randBtn, 1, Color{90, 90, 120, 255});
|
||||||
|
DrawText("Randomize", (int)L.randBtn.x + 14, (int)L.randBtn.y + 6, 16, RAYWHITE);
|
||||||
|
|
||||||
|
int hy = (int)L.seedRow.y + 60;
|
||||||
|
DrawText("Tune every setting on the Settings tab, then start a new world here.",
|
||||||
|
x, hy, 16, Color{160, 164, 180, 255});
|
||||||
|
hy += 26;
|
||||||
|
DrawText("New World applies every change made here; unsaved edits to a running world",
|
||||||
|
x, hy, 16, Color{160, 164, 180, 255});
|
||||||
|
hy += 20;
|
||||||
|
DrawText("are kept if you just go Back to World instead.", x, hy, 16, Color{160, 164, 180, 255});
|
||||||
|
if (!v.statusMsg.empty() && GetTime() < v.statusUntil) {
|
||||||
|
hy += 30;
|
||||||
|
DrawText(v.statusMsg.c_str(), x, hy, 16, Color{120, 230, 140, 255});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawSettingsTab(Viewer& v, Rectangle body) {
|
||||||
|
MainMenuState& m = v.menuState;
|
||||||
|
const auto& cats = configCategoryList();
|
||||||
|
const auto& table = configFieldTable();
|
||||||
|
|
||||||
|
const float railW = 280.0f;
|
||||||
|
Rectangle rail{ body.x + 24.0f, body.y + 16.0f, railW, body.height - 32.0f };
|
||||||
|
DrawRectangleRec(rail, Color{16, 18, 26, 255});
|
||||||
|
DrawRectangleLinesEx(rail, 1, Color{70, 74, 90, 255});
|
||||||
|
float ry = rail.y + 4.0f;
|
||||||
|
for (size_t i = 0; i < cats.size(); ++i) {
|
||||||
|
Rectangle r{ rail.x + 4.0f, ry, rail.width - 8.0f, 26.0f };
|
||||||
|
bool active = (m.settingsCategory == (int)i);
|
||||||
|
DrawRectangleRec(r, active ? Color{50, 58, 84, 255} : Color{24, 26, 36, 255});
|
||||||
|
DrawText(cats[i], (int)r.x + 8, (int)r.y + 5, 14,
|
||||||
|
active ? Color{255, 230, 140, 255} : Color{190, 195, 210, 255});
|
||||||
|
m.categoryRects.push_back(r);
|
||||||
|
ry += 28.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
float px = rail.x + rail.width + 24.0f;
|
||||||
|
Rectangle scrollBounds{ px, body.y + 16.0f, (body.x + body.width) - px - 24.0f, body.height - 32.0f };
|
||||||
|
const char* cat = (m.settingsCategory >= 0 && m.settingsCategory < (int)cats.size())
|
||||||
|
? cats[m.settingsCategory] : "";
|
||||||
|
int count = 0;
|
||||||
|
for (const auto& d : table) if (std::strcmp(d.category, cat) == 0) ++count;
|
||||||
|
Rectangle content{ 0, 0, scrollBounds.width - 16.0f, (float)count * 26.0f + 16.0f };
|
||||||
|
Rectangle view{};
|
||||||
|
GuiScrollPanel(scrollBounds, nullptr, content, &m.settingsScroll, &view);
|
||||||
|
BeginScissorMode((int)view.x, (int)view.y, (int)view.width, (int)view.height);
|
||||||
|
float fx = scrollBounds.x + 8.0f + m.settingsScroll.x;
|
||||||
|
float fy = scrollBounds.y + 8.0f + m.settingsScroll.y;
|
||||||
|
float fw = content.width - 16.0f;
|
||||||
|
for (size_t i = 0; i < table.size(); ++i) {
|
||||||
|
if (std::strcmp(table[i].category, cat) != 0) continue;
|
||||||
|
double val = table[i].get(v.cfg);
|
||||||
|
fy = (float)drawConfigFieldRow(m, (int)i, table[i], val, (int)fx, (int)fy, (int)fw);
|
||||||
|
}
|
||||||
|
EndScissorMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawLoadWorldTab(Viewer& v, Rectangle body) {
|
||||||
|
MainMenuState& m = v.menuState;
|
||||||
|
int x = (int)body.x + 40, y = (int)body.y + 30;
|
||||||
|
DrawText("Load World", x, y, 22, RAYWHITE);
|
||||||
|
y += 40;
|
||||||
|
if (m.saveFiles.empty()) {
|
||||||
|
DrawText("No saves found.", x, y, 16, Color{160, 164, 180, 255});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const auto& f : m.saveFiles) {
|
||||||
|
Rectangle r{ (float)x, (float)y, 560.0f, 28.0f };
|
||||||
|
DrawRectangleRec(r, Color{22, 24, 34, 255});
|
||||||
|
DrawRectangleLinesEx(r, 1, Color{70, 74, 90, 255});
|
||||||
|
DrawText(f.c_str(), (int)r.x + 8, (int)r.y + 5, 16, RAYWHITE);
|
||||||
|
m.saveFileRects.push_back(r);
|
||||||
|
y += 32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void drawMainMenu(Viewer& v) {
|
||||||
|
MainMenuState& m = v.menuState;
|
||||||
|
m.categoryRects.clear();
|
||||||
|
m.fieldRects.clear(); m.fieldIndices.clear();
|
||||||
|
m.saveFileRects.clear();
|
||||||
|
|
||||||
|
DrawText("Planet Sim", 16, 14, 30, RAYWHITE);
|
||||||
|
for (size_t i = 0; i < std::size(kTabs); ++i) {
|
||||||
|
Rectangle r = tabButtonRect((int)i);
|
||||||
|
bool active = (m.tab == kTabs[i].tab);
|
||||||
|
DrawRectangleRec(r, active ? Color{60, 70, 100, 255} : Color{28, 30, 42, 255});
|
||||||
|
DrawRectangleLinesEx(r, 1, Color{90, 90, 110, 255});
|
||||||
|
int tw = MeasureText(kTabs[i].label, 18);
|
||||||
|
DrawText(kTabs[i].label, (int)(r.x + r.width / 2 - tw / 2), (int)(r.y + 7), 18,
|
||||||
|
active ? RAYWHITE : Color{180, 180, 195, 255});
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle body = mainMenuBody(v);
|
||||||
|
switch (m.tab) {
|
||||||
|
case MenuTab::Home: drawHomeTab(v, body); break;
|
||||||
|
case MenuTab::Settings: drawSettingsTab(v, body); break;
|
||||||
|
case MenuTab::LoadWorld: drawLoadWorldTab(v, body); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleMainMenuInput(Viewer& v) {
|
||||||
|
MainMenuState& m = v.menuState;
|
||||||
|
|
||||||
|
// Typed numeric entry swallows other input while a field is focused (mirrors Edit Mode's
|
||||||
|
// ViewerInput.cpp typing loop / Viewer::commitEditField).
|
||||||
|
if (m.editingConfigField >= 0) {
|
||||||
|
int ch = GetCharPressed();
|
||||||
|
while (ch > 0) {
|
||||||
|
if ((ch >= '0' && ch <= '9') || ch == '.' || ch == '-') m.editBuffer += (char)ch;
|
||||||
|
ch = GetCharPressed();
|
||||||
|
}
|
||||||
|
if (IsKeyPressed(KEY_BACKSPACE) && !m.editBuffer.empty()) m.editBuffer.pop_back();
|
||||||
|
if (IsKeyPressed(KEY_ENTER)) v.commitMenuField();
|
||||||
|
if (IsKeyPressed(KEY_ESCAPE)) { m.editingConfigField = -1; m.editBuffer.clear(); }
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) return;
|
||||||
|
Vector2 mp = GetMousePosition();
|
||||||
|
|
||||||
|
for (size_t i = 0; i < std::size(kTabs); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, tabButtonRect((int)i))) continue;
|
||||||
|
m.tab = kTabs[i].tab;
|
||||||
|
m.editingConfigField = -1; m.editBuffer.clear();
|
||||||
|
if (m.tab == MenuTab::LoadWorld) m.saveFiles = listSaveFiles();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle body = mainMenuBody(v);
|
||||||
|
if (m.tab == MenuTab::Home) {
|
||||||
|
HomeLayout L = computeHomeLayout(body);
|
||||||
|
if (CheckCollisionPointRec(mp, L.newBtn)) { v.startNewWorld(); return; }
|
||||||
|
if (m.worldEverGenerated && CheckCollisionPointRec(mp, L.backBtn)) { v.screen = AppScreen::World; return; }
|
||||||
|
if (CheckCollisionPointRec(mp, L.randBtn)) {
|
||||||
|
v.cfg.seed = (uint32_t)(GetTime() * 100000.0) | 1u; // odd, non-zero: matches the R-key reseed convention
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (m.tab == MenuTab::Settings) {
|
||||||
|
for (size_t i = 0; i < m.categoryRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, m.categoryRects[i])) continue;
|
||||||
|
m.settingsCategory = (int)i;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else { // LoadWorld
|
||||||
|
for (size_t i = 0; i < m.saveFileRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, m.saveFileRects[i])) continue;
|
||||||
|
if (i < m.saveFiles.size()) v.loadGame(m.saveFiles[i].c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generic: click on any drawn config-field row (Home's seed row or a Settings-tab field)
|
||||||
|
// focuses it for typed entry, seeded with its current value.
|
||||||
|
for (size_t i = 0; i < m.fieldRects.size(); ++i) {
|
||||||
|
if (!CheckCollisionPointRec(mp, m.fieldRects[i])) continue;
|
||||||
|
int fieldIdx = m.fieldIndices[i];
|
||||||
|
const auto& d = configFieldTable()[fieldIdx];
|
||||||
|
m.editingConfigField = fieldIdx;
|
||||||
|
m.editBuffer = TextFormat(d.isInteger ? "%.0f" : "%.4g", d.get(v.cfg));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/render/MainMenu.hpp
Normal file
35
src/render/MainMenu.hpp
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "raylib.h"
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
struct Viewer;
|
||||||
|
|
||||||
|
// The full-screen Main Menu: settings editor over every PlanetConfig field, a seed field,
|
||||||
|
// "New World" / "Load World" / "Back to World" actions. Shown at app startup (before any world
|
||||||
|
// has ever been generated) and reachable again later via a toolbar button, without disturbing a
|
||||||
|
// running world unless the user explicitly picks New or Load.
|
||||||
|
enum class AppScreen { MainMenu, World };
|
||||||
|
enum class MenuTab { Home, Settings, LoadWorld };
|
||||||
|
|
||||||
|
// Transient menu-UI state, owned by the Viewer, rebuilt each frame by drawMainMenu() (render
|
||||||
|
// side) and consumed by handleMainMenuInput() -- mirrors EditPanelState's pattern in Panels.hpp.
|
||||||
|
struct MainMenuState {
|
||||||
|
MenuTab tab = MenuTab::Home;
|
||||||
|
int settingsCategory = 0; // which left-rail category is open
|
||||||
|
int editingConfigField = -1; // index into configFieldTable(), -1 = nothing focused
|
||||||
|
std::string editBuffer;
|
||||||
|
bool worldEverGenerated = false; // gates the "Back to World" button
|
||||||
|
Vector2 settingsScroll{0, 0}; // GuiScrollPanel offset for the field list
|
||||||
|
|
||||||
|
std::vector<Rectangle> categoryRects; // left-rail hit rects
|
||||||
|
std::vector<Rectangle> fieldRects; // parallel to fieldIndices: the open category's fields
|
||||||
|
std::vector<int> fieldIndices; // parallel to fieldRects: index into configFieldTable()
|
||||||
|
std::vector<std::string> saveFiles; // Load World: paths, newest first
|
||||||
|
std::vector<Rectangle> saveFileRects; // parallel to saveFiles
|
||||||
|
};
|
||||||
|
|
||||||
|
// No separate style setup: raygui's re-skin (setupToolbarStyle(), called once in Viewer::init())
|
||||||
|
// is a global style context, already applied before either screen ever renders.
|
||||||
|
void drawMainMenu(Viewer& v); // draw the whole menu screen for this frame
|
||||||
|
void handleMainMenuInput(Viewer& v); // mouse/keyboard handling while screen == AppScreen::MainMenu
|
||||||
@ -195,12 +195,19 @@ void drawToolbar(Viewer& v) {
|
|||||||
if (GuiButton(takeRow(cx, cw, cy), "Reseed (R)")) v.reseed();
|
if (GuiButton(takeRow(cx, cw, cy), "Reseed (R)")) v.reseed();
|
||||||
{
|
{
|
||||||
Rectangle r = takeRow(cx, cw, cy);
|
Rectangle r = takeRow(cx, cw, cy);
|
||||||
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(v.SAVE_PATH);
|
if (GuiButton(Rectangle{ r.x, r.y, halfW, r.height }, "Save (F5)")) v.saveGame(makeTimestampedSavePath().c_str());
|
||||||
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(v.SAVE_PATH);
|
if (GuiButton(Rectangle{ r.x + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) {
|
||||||
|
auto files = listSaveFiles();
|
||||||
|
if (!files.empty()) v.loadGame(files.front().c_str());
|
||||||
|
else v.setStatus("No saves found");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Deferred to after drawToolbar() returns -- see Viewer::pendingMapExport/pendingAtlasExport.
|
// Deferred to after drawToolbar() returns -- see Viewer::pendingMapExport/pendingAtlasExport.
|
||||||
if (GuiButton(takeRow(cx, cw, cy), "Export Map (F11)")) v.pendingMapExport = true;
|
if (GuiButton(takeRow(cx, cw, cy), "Export Map (F11)")) v.pendingMapExport = true;
|
||||||
if (GuiButton(takeRow(cx, cw, cy), "Export Atlas (Shift+F11)")) v.pendingAtlasExport = true;
|
if (GuiButton(takeRow(cx, cw, cy), "Export Atlas (Shift+F11)")) v.pendingAtlasExport = true;
|
||||||
|
// Doesn't touch planet/cfg/mode/overlays -- "Back to World" on the Home tab is a pure no-op
|
||||||
|
// return, so the running world is completely undisturbed.
|
||||||
|
if (GuiButton(takeRow(cx, cw, cy), "Main Menu")) { v.screen = AppScreen::MainMenu; v.menuState.tab = MenuTab::Home; }
|
||||||
cy += SECTION_GAP;
|
cy += SECTION_GAP;
|
||||||
|
|
||||||
// --- Civilization & Ecology (all need a settled world) ------------------
|
// --- Civilization & Ecology (all need a settled world) ------------------
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
|
#include <ctime>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@ -129,18 +130,15 @@ bool Viewer::init(int argc, char** argv) {
|
|||||||
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
|
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
|
||||||
std::string cfgErr = validateConfig(cfg);
|
std::string cfgErr = validateConfig(cfg);
|
||||||
if (!cfgErr.empty()) cfg = PlanetConfig{}; // revert to safe defaults
|
if (!cfgErr.empty()) cfg = PlanetConfig{}; // revert to safe defaults
|
||||||
planet.generate(cfg);
|
// No planet.generate() here anymore -- the app opens on the Main Menu; a world is only
|
||||||
|
// built once the user picks "New World" (startNewWorld()) or "Load World" (loadGame()).
|
||||||
|
|
||||||
cam.position = {0, 0, 6}; cam.target = {0, 0, 0}; cam.up = {0, 1, 0};
|
cam.position = {0, 0, 6}; cam.target = {0, 0, 0}; cam.up = {0, 1, 0};
|
||||||
cam.fovy = 45; cam.projection = CAMERA_PERSPECTIVE;
|
cam.fovy = 45; cam.projection = CAMERA_PERSPECTIVE;
|
||||||
|
|
||||||
buildBorders(planet, borderR, borders, ridgeBorders);
|
graticule = buildGraticule(); // planet-independent, safe to build before any world exists
|
||||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
|
||||||
graticule = buildGraticule();
|
|
||||||
buildMap2D(planet, mapRect, map2D);
|
|
||||||
phase3PromptAt = planet.cfg.phase3AfterMy;
|
|
||||||
|
|
||||||
refreshView(); // colour the freshly generated (flat) world
|
screen = AppScreen::MainMenu;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -364,6 +362,22 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
|||||||
|
|
||||||
void Viewer::regen() { planet.generate(cfg); regenWorld(); }
|
void Viewer::regen() { planet.generate(cfg); regenWorld(); }
|
||||||
|
|
||||||
|
void Viewer::startNewWorld() { // Main Menu "New World": validate, generate, switch screens
|
||||||
|
std::string err = validateConfig(cfg);
|
||||||
|
if (!err.empty()) { cfg = PlanetConfig{}; setStatus("Bad settings -- using defaults"); }
|
||||||
|
regen();
|
||||||
|
menuState.worldEverGenerated = true;
|
||||||
|
menuState.editingConfigField = -1; menuState.editBuffer.clear();
|
||||||
|
screen = AppScreen::World;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Viewer::commitMenuField() { // parse menuState.editBuffer, apply to the focused config field
|
||||||
|
const auto& t = configFieldTable();
|
||||||
|
if (menuState.editingConfigField < 0 || menuState.editingConfigField >= (int)t.size()) return;
|
||||||
|
try { t[menuState.editingConfigField].set(cfg, std::stod(menuState.editBuffer)); } catch (...) {}
|
||||||
|
menuState.editingConfigField = -1; menuState.editBuffer.clear();
|
||||||
|
}
|
||||||
|
|
||||||
void Viewer::stepOnce() { // one tick + settle bookkeeping
|
void Viewer::stepOnce() { // one tick + settle bookkeeping
|
||||||
maxChange = planet.step(); ++stepCount;
|
maxChange = planet.step(); ++stepCount;
|
||||||
if (maxChange < settleThresh) { if (++settleRun >= settleNeed) settled = true; }
|
if (maxChange < settleThresh) { if (++settleRun >= settleNeed) settled = true; }
|
||||||
@ -552,6 +566,26 @@ void Viewer::focusCell(int idx, const std::string& status) {
|
|||||||
if (!status.empty()) setStatus(status);
|
if (!status.empty()) setStatus(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// F5 / the toolbar "Save" button always write a fresh timestamped file (mirrors the
|
||||||
|
// map/atlas export naming scheme, ViewerRender.cpp); F9 / "Load" pick the newest one.
|
||||||
|
std::string makeTimestampedSavePath() {
|
||||||
|
time_t now = time(nullptr);
|
||||||
|
struct tm tmv; localtime_r(&now, &tmv);
|
||||||
|
char name[64];
|
||||||
|
strftime(name, sizeof(name), "world_%Y-%m-%d_%H%M%S.save", &tmv);
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> listSaveFiles() {
|
||||||
|
FilePathList files = LoadDirectoryFilesEx(".", ".save", false);
|
||||||
|
std::vector<std::string> out;
|
||||||
|
out.reserve(files.count);
|
||||||
|
for (unsigned i = 0; i < files.count; ++i) out.emplace_back(files.paths[i]);
|
||||||
|
UnloadDirectoryFiles(files);
|
||||||
|
std::sort(out.begin(), out.end(), std::greater<>()); // fixed timestamp format sorts chronologically
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// F5: write seed + config + full planet state. F9: read it back and resume.
|
// F5: write seed + config + full planet state. F9: read it back and resume.
|
||||||
void Viewer::saveGame(const char* path) {
|
void Viewer::saveGame(const char* path) {
|
||||||
std::ofstream os(path, std::ios::binary);
|
std::ofstream os(path, std::ios::binary);
|
||||||
@ -614,6 +648,25 @@ void Viewer::saveGame(const char* path) {
|
|||||||
os.write((char*)&e.sourceId, 4);
|
os.write((char*)&e.sourceId, 4);
|
||||||
wS(e.title); wS(e.detail);
|
wS(e.title); wS(e.detail);
|
||||||
}
|
}
|
||||||
|
// v25: the viewer's colour mode + overlay toggles, so a load restores what you were looking at,
|
||||||
|
// not just the simulation. Camera angle / map zoom-pan / selected cell stay session-local.
|
||||||
|
uint8_t modeByte = (uint8_t)mode;
|
||||||
|
os.write((char*)&modeByte, 1);
|
||||||
|
uint16_t overlayBits = 0;
|
||||||
|
overlayBits |= showBorders ? 1u << 0 : 0;
|
||||||
|
overlayBits |= showDrift ? 1u << 1 : 0;
|
||||||
|
overlayBits |= showRivers ? 1u << 2 : 0;
|
||||||
|
overlayBits |= showGrat ? 1u << 3 : 0;
|
||||||
|
overlayBits |= dayNightOn ? 1u << 4 : 0;
|
||||||
|
overlayBits |= showTides ? 1u << 5 : 0;
|
||||||
|
overlayBits |= showCurrents ? 1u << 6 : 0;
|
||||||
|
overlayBits |= showClouds ? 1u << 7 : 0;
|
||||||
|
overlayBits |= showVolcanoes ? 1u << 8 : 0;
|
||||||
|
overlayBits |= showNames ? 1u << 9 : 0;
|
||||||
|
overlayBits |= showSettlements ? 1u << 10 : 0;
|
||||||
|
overlayBits |= showDiplomacy ? 1u << 11 : 0;
|
||||||
|
overlayBits |= showRealms ? 1u << 12 : 0;
|
||||||
|
os.write((char*)&overlayBits, sizeof overlayBits);
|
||||||
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
setStatus(os ? std::string("Saved ") + path : "Save failed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -759,6 +812,13 @@ void Viewer::loadGame(const char* path) {
|
|||||||
}
|
}
|
||||||
if (!eventsOk) { events.clear(); nextEventId = 1; skippedHistory = true; }
|
if (!eventsOk) { events.clear(); nextEventId = 1; skippedHistory = true; }
|
||||||
}
|
}
|
||||||
|
// v25: colour mode + overlay toggles, written at the very end of saveGame() (after the events
|
||||||
|
// block), so read them from the same position here.
|
||||||
|
uint8_t modeByte = 0; uint16_t overlayBits = 0;
|
||||||
|
if (ver >= 25) {
|
||||||
|
is.read(reinterpret_cast<char*>(&modeByte), 1);
|
||||||
|
is.read(reinterpret_cast<char*>(&overlayBits), sizeof overlayBits);
|
||||||
|
}
|
||||||
paused = true; selectedCell = -1; subgrids.clear();
|
paused = true; selectedCell = -1; subgrids.clear();
|
||||||
// Pre-v14 save already in Live World: it has no volcano block, so place a set now (v14+ saves
|
// Pre-v14 save already in Live World: it has no volcano block, so place a set now (v14+ saves
|
||||||
// restore their own). A non-live save places them when the user first presses W.
|
// restore their own). A non-live save places them when the user first presses W.
|
||||||
@ -767,7 +827,27 @@ void Viewer::loadGame(const char* path) {
|
|||||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||||
buildMap2D(planet, mapRect, map2D);
|
buildMap2D(planet, mapRect, map2D);
|
||||||
if (planet.settlementsPlaced()) rebuildTerritory(); // territory/nations are derived -> recompute
|
if (planet.settlementsPlaced()) rebuildTerritory(); // territory/nations are derived -> recompute
|
||||||
|
// v25: restore the colour mode + overlay toggles. Pre-v25 saves leave them at whatever the
|
||||||
|
// Viewer already held (same "session state survives a load" behaviour saves have always had).
|
||||||
|
if (ver >= 25) {
|
||||||
|
if (modeByte <= (uint8_t)ColorMode::Seasonality) mode = (ColorMode)modeByte;
|
||||||
|
showBorders = overlayBits & (1u << 0);
|
||||||
|
showDrift = overlayBits & (1u << 1);
|
||||||
|
showRivers = overlayBits & (1u << 2);
|
||||||
|
showGrat = overlayBits & (1u << 3);
|
||||||
|
dayNightOn = overlayBits & (1u << 4);
|
||||||
|
showTides = overlayBits & (1u << 5);
|
||||||
|
showCurrents = overlayBits & (1u << 6);
|
||||||
|
showClouds = overlayBits & (1u << 7);
|
||||||
|
showVolcanoes = overlayBits & (1u << 8);
|
||||||
|
showNames = overlayBits & (1u << 9);
|
||||||
|
showSettlements = overlayBits & (1u << 10);
|
||||||
|
showDiplomacy = overlayBits & (1u << 11);
|
||||||
|
showRealms = overlayBits & (1u << 12);
|
||||||
|
}
|
||||||
refreshView();
|
refreshView();
|
||||||
|
menuState.worldEverGenerated = true; // a loaded world can always be returned to from the Main Menu
|
||||||
|
screen = AppScreen::World;
|
||||||
setStatus(skippedHistory ? std::string("Loaded ") + path + " (history skipped)"
|
setStatus(skippedHistory ? std::string("Loaded ") + path + " (history skipped)"
|
||||||
: std::string("Loaded ") + path);
|
: std::string("Loaded ") + path);
|
||||||
}
|
}
|
||||||
@ -1104,9 +1184,11 @@ Rectangle Viewer::mapViewRect() const {
|
|||||||
void Viewer::run() {
|
void Viewer::run() {
|
||||||
while (!WindowShouldClose()) {
|
while (!WindowShouldClose()) {
|
||||||
handleInput();
|
handleInput();
|
||||||
stepSim();
|
if (screen == AppScreen::World) {
|
||||||
// A regenerate this frame may have shrunk the planet; keep indices valid.
|
stepSim();
|
||||||
if (hovered >= (int)planet.cells.size()) hovered = -1;
|
// A regenerate this frame may have shrunk the planet; keep indices valid.
|
||||||
|
if (hovered >= (int)planet.cells.size()) hovered = -1;
|
||||||
|
}
|
||||||
renderFrame();
|
renderFrame();
|
||||||
}
|
}
|
||||||
UnloadRenderTexture(rt3d);
|
UnloadRenderTexture(rt3d);
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
#include "Overlays.hpp" // PlateLabel
|
#include "Overlays.hpp" // PlateLabel
|
||||||
#include "Map2D.hpp" // Map2D
|
#include "Map2D.hpp" // Map2D
|
||||||
#include "Panels.hpp" // EditField, EditPanelState
|
#include "Panels.hpp" // EditField, EditPanelState
|
||||||
|
#include "MainMenu.hpp" // AppScreen, MenuTab, MainMenuState
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@ -16,13 +17,16 @@
|
|||||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||||
struct Viewer {
|
struct Viewer {
|
||||||
// ---- Files / save format ------------------------------------------------
|
// ---- Files / save format ------------------------------------------------
|
||||||
static constexpr uint32_t SAVE_VERSION = 24; // v24: edit mode (per-cell lock bits + locked-value maps); v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
static constexpr uint32_t SAVE_VERSION = 25; // v25: +viewer colour-mode/overlay state (Viewer::saveGame/loadGame only, not Planet); v24: edit mode (per-cell lock bits + locked-value maps); v23: civ cultural evolution; v22: civ diplomacy; v21: civ conflict/wars; v20: civ settlements; v19: ecoregions; v18: geography reshuffle salt; v17: +geography/atlas; v16: +event log; v15: stateful volcanoes; v14: old volcanoes; v13: +liveRate; v12: +step-back history; v11: +weather systems; v10: +weather fields; v9: +moons; v8: +Live World clock; v7: +biota; v6: self-describing config; v4: +biome; v3: +phase3
|
||||||
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
static constexpr int wxSaveMax = 40; // most recent step-back frames persisted in a save
|
||||||
static constexpr int EVENT_LOG_MAX = 200;
|
static constexpr int EVENT_LOG_MAX = 200;
|
||||||
const char* CONFIG_PATH = "planet.cfg";
|
const char* CONFIG_PATH = "planet.cfg";
|
||||||
const char* SAVE_PATH = "planet.save";
|
|
||||||
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
std::string configPath = "planet.cfg"; // initial config (--config overrides)
|
||||||
|
|
||||||
|
// ---- App screen (Main Menu vs. a live world) ----------------------------
|
||||||
|
AppScreen screen = AppScreen::MainMenu; // init() no longer auto-generates a world
|
||||||
|
MainMenuState menuState;
|
||||||
|
|
||||||
// ---- Window / layout (set in init) --------------------------------------
|
// ---- Window / layout (set in init) --------------------------------------
|
||||||
int screenW = 1920, screenH = 1080;
|
int screenW = 1920, screenH = 1080;
|
||||||
int leftW = 0, rightX = 0, rightW = 0, rightH = 0;
|
int leftW = 0, rightX = 0, rightW = 0, rightH = 0;
|
||||||
@ -211,6 +215,8 @@ struct Viewer {
|
|||||||
const std::vector<Color>& displayColors() const { return liveWorld ? shadedColors : vcolors; }
|
const std::vector<Color>& displayColors() const { return liveWorld ? shadedColors : vcolors; }
|
||||||
void regenWorld(); // after generate(): geometry changed
|
void regenWorld(); // after generate(): geometry changed
|
||||||
void regen(); // generate(cfg) + regenWorld()
|
void regen(); // generate(cfg) + regenWorld()
|
||||||
|
void startNewWorld(); // validate cfg, regen(), switch screen to World (Main Menu "New World")
|
||||||
|
void commitMenuField(); // parse menuState.editBuffer and apply it to the focused config field
|
||||||
void stepOnce(); // one tick + settle bookkeeping
|
void stepOnce(); // one tick + settle bookkeeping
|
||||||
void pauseAction();
|
void pauseAction();
|
||||||
void setStatus(const std::string& m);
|
void setStatus(const std::string& m);
|
||||||
@ -278,3 +284,8 @@ struct Viewer {
|
|||||||
void renderHUD();
|
void renderHUD();
|
||||||
void renderPrompt();
|
void renderPrompt();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Free helpers for the timestamped save scheme (no Viewer state needed):
|
||||||
|
// F5 / the toolbar "Save" button always write a fresh file; F9 / "Load" pick the newest.
|
||||||
|
std::string makeTimestampedSavePath(); // "world_YYYY-MM-DD_HHMMSS.save"
|
||||||
|
std::vector<std::string> listSaveFiles(); // *.save in the working dir, newest first
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
#include "Picking.hpp"
|
#include "Picking.hpp"
|
||||||
#include "Map2D.hpp" // wrapPi
|
#include "Map2D.hpp" // wrapPi
|
||||||
#include "Projection.hpp" // EqualEarth, lonLatToDir
|
#include "Projection.hpp" // EqualEarth, lonLatToDir
|
||||||
|
#include "MainMenu.hpp" // handleMainMenuInput
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
|
|
||||||
@ -10,6 +11,7 @@
|
|||||||
// members (mp/onPause/hovered/hasHoverSub/hoverSub/hoveredSubIdx) for the render
|
// members (mp/onPause/hovered/hasHoverSub/hoverSub/hoveredSubIdx) for the render
|
||||||
// pass, and may step / regenerate / save / load the world.
|
// pass, and may step / regenerate / save / load the world.
|
||||||
void Viewer::handleInput() {
|
void Viewer::handleInput() {
|
||||||
|
if (screen == AppScreen::MainMenu) { handleMainMenuInput(*this); return; }
|
||||||
mp = GetMousePosition();
|
mp = GetMousePosition();
|
||||||
bool in3D = (mp.x < view3DW && mp.y < view3DH); // top-left quadrant
|
bool in3D = (mp.x < view3DW && mp.y < view3DH); // top-left quadrant
|
||||||
bool inMap = CheckCollisionPointRec(mp, mapRect);
|
bool inMap = CheckCollisionPointRec(mp, mapRect);
|
||||||
@ -241,8 +243,12 @@ void Viewer::handleInput() {
|
|||||||
regen();
|
regen();
|
||||||
setStatus(cerr.empty() ? std::string("Reloaded ") + configPath : "Bad config — using defaults");
|
setStatus(cerr.empty() ? std::string("Reloaded ") + configPath : "Bad config — using defaults");
|
||||||
} else setStatus(std::string("No ") + configPath); }
|
} else setStatus(std::string("No ") + configPath); }
|
||||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F5)) saveGame(makeTimestampedSavePath().c_str());
|
||||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
if (IsKeyPressed(KEY_F9)) {
|
||||||
|
auto files = listSaveFiles();
|
||||||
|
if (!files.empty()) loadGame(files.front().c_str());
|
||||||
|
else setStatus("No saves found");
|
||||||
|
}
|
||||||
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
if (IsKeyPressed(KEY_F12)) { TakeScreenshot("screenshot.png"); setStatus("Screenshot saved to screenshot.png"); }
|
||||||
if (IsKeyPressed(KEY_F11)) {
|
if (IsKeyPressed(KEY_F11)) {
|
||||||
if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) exportAtlasImage();
|
if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) exportAtlasImage();
|
||||||
|
|||||||
@ -3,6 +3,7 @@
|
|||||||
#include "Map2D.hpp"
|
#include "Map2D.hpp"
|
||||||
#include "Panels.hpp"
|
#include "Panels.hpp"
|
||||||
#include "Toolbar.hpp" // drawToolbar
|
#include "Toolbar.hpp" // drawToolbar
|
||||||
|
#include "MainMenu.hpp" // drawMainMenu
|
||||||
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
|
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
|
||||||
#include "rlgl.h"
|
#include "rlgl.h"
|
||||||
#include "Projection.hpp" // dirToLonLat (plate labels)
|
#include "Projection.hpp" // dirToLonLat (plate labels)
|
||||||
@ -1169,6 +1170,15 @@ void Viewer::renderPrompt() {
|
|||||||
// One full frame: globe texture, then composite + 3D labels + map + panels +
|
// One full frame: globe texture, then composite + 3D labels + map + panels +
|
||||||
// HUD + prompt onto the screen.
|
// HUD + prompt onto the screen.
|
||||||
void Viewer::renderFrame() {
|
void Viewer::renderFrame() {
|
||||||
|
// A complete early return, not a dim overlay over a running world (unlike the Phase-3 modal
|
||||||
|
// below) -- the Main Menu must render before any world/planet.cells even exists.
|
||||||
|
if (screen == AppScreen::MainMenu) {
|
||||||
|
BeginDrawing();
|
||||||
|
ClearBackground(Color{8, 10, 16, 255});
|
||||||
|
drawMainMenu(*this);
|
||||||
|
EndDrawing();
|
||||||
|
return;
|
||||||
|
}
|
||||||
renderGlobe3D();
|
renderGlobe3D();
|
||||||
|
|
||||||
BeginDrawing();
|
BeginDrawing();
|
||||||
|
|||||||
@ -483,3 +483,19 @@ private:
|
|||||||
bool loadConfig(const std::string& path, PlanetConfig& cfg);
|
bool loadConfig(const std::string& path, PlanetConfig& cfg);
|
||||||
bool saveConfig(const std::string& path, const PlanetConfig& cfg);
|
bool saveConfig(const std::string& path, const PlanetConfig& cfg);
|
||||||
std::string validateConfig(const PlanetConfig& cfg);
|
std::string validateConfig(const PlanetConfig& cfg);
|
||||||
|
|
||||||
|
// One entry per CONFIG_FIELDS token (see PlanetIO.cpp), in PlanetConfig declaration order, built
|
||||||
|
// once at first use. Lets a single generic UI loop (the Main Menu settings editor) drive all ~240
|
||||||
|
// config fields instead of 240 hand-written widgets -- the same "add a field, everything else keeps
|
||||||
|
// working" property writeConfigFields()/parseConfigStream() already have for planet.cfg/saves.
|
||||||
|
struct ConfigFieldDesc {
|
||||||
|
const char* name; // matches the CONFIG_FIELDS token, e.g. "seaLevel"
|
||||||
|
const char* category; // section label, e.g. "Tectonic tuning"
|
||||||
|
bool isInteger; // true for I()/U() fields (whole-number display/parse)
|
||||||
|
double (*get)(const PlanetConfig&);
|
||||||
|
void (*set)(PlanetConfig&, double);
|
||||||
|
};
|
||||||
|
const std::vector<ConfigFieldDesc>& configFieldTable();
|
||||||
|
// The distinct ConfigFieldDesc::category labels, in display order (25 groups) -- the Main Menu
|
||||||
|
// settings editor's left-rail category list.
|
||||||
|
const std::vector<const char*>& configCategoryList();
|
||||||
|
|||||||
@ -10,6 +10,7 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
// --- Config file (text) + save/load (binary) --------------------------------
|
// --- Config file (text) + save/load (binary) --------------------------------
|
||||||
|
|
||||||
@ -115,6 +116,206 @@ static void parseConfigStream(std::istream& is, PlanetConfig& cfg) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Category assignment: an explicit name->category table, built by hand from PlanetConfig's actual
|
||||||
|
// struct declaration order in PlanetTypes.hpp (25 "// ---" / plain-comment sections; "Plate
|
||||||
|
// lifecycle" absorbs the trailing "Spreading & volcanic (legacy)" block and "Cultural evolution"
|
||||||
|
// absorbs "Dawn seeding", matching how CLAUDE.md documents those as one knob group each). File
|
||||||
|
// scope (not local to buildConfigFieldTable()) so configCategoryList() can share it below.
|
||||||
|
//
|
||||||
|
// This CANNOT be a "first field of each section" boundary scan over configFieldTable()'s own
|
||||||
|
// iteration order -- CONFIG_FIELDS expands to all D() fields, then all I() fields, then U(seed),
|
||||||
|
// so within a section whose fields mix types (nearly all of them) the section's members are
|
||||||
|
// scattered across two separate runs of the table, not contiguous. A name->category lookup sidesteps
|
||||||
|
// that entirely. This array IS written in one contiguous block per category, though (unlike
|
||||||
|
// configFieldTable()), so it doubles as the source of truth for configCategoryList()'s display order.
|
||||||
|
static const struct { const char* name; const char* category; } kConfigFieldCategory[] = {
|
||||||
|
{"radius", "Core"}, {"subdivisions", "Core"}, {"seaLevel", "Core"}, {"plateCount", "Core"},
|
||||||
|
{"seed", "Core"}, {"axialTilt", "Core"},
|
||||||
|
|
||||||
|
{"continentBase", "Tectonic tuning"}, {"oceanBase", "Tectonic tuning"},
|
||||||
|
{"upliftGain", "Tectonic tuning"}, {"beltWidth", "Tectonic tuning"}, {"relax", "Tectonic tuning"},
|
||||||
|
|
||||||
|
{"collisionFactor", "Orogeny"}, {"arcFactor", "Orogeny"}, {"isostaticPersist", "Orogeny"},
|
||||||
|
{"rootScale", "Orogeny"},
|
||||||
|
|
||||||
|
{"peakSoftCapStart", "Soft peak cap"}, {"peakSoftCapEnd", "Soft peak cap"},
|
||||||
|
{"peakFailDrop", "Soft peak cap"},
|
||||||
|
|
||||||
|
{"maxDriftSpeed", "Plate drift"}, {"ridgeDepth", "Plate drift"},
|
||||||
|
|
||||||
|
{"seafloorSubsidence", "Seafloor aging"}, {"seafloorSeedAge", "Seafloor aging"},
|
||||||
|
|
||||||
|
{"splitCheckEvery", "Plate lifecycle"}, {"splitFraction", "Plate lifecycle"},
|
||||||
|
{"splitProbBase", "Plate lifecycle"}, {"splitProbSlope", "Plate lifecycle"},
|
||||||
|
{"stalemateEps", "Plate lifecycle"}, {"stalemateWindows", "Plate lifecycle"},
|
||||||
|
{"stalemateBoost", "Plate lifecycle"}, {"miniPlateCells", "Plate lifecycle"},
|
||||||
|
{"fuseMinPlates", "Plate lifecycle"}, {"babyMinCells", "Plate lifecycle"},
|
||||||
|
{"babyPromoteFrac", "Plate lifecycle"}, {"volcanicLandFrac", "Plate lifecycle"},
|
||||||
|
{"volcanicElev", "Plate lifecycle"}, {"landBand", "Plate lifecycle"},
|
||||||
|
|
||||||
|
{"erosionLandRate", "Erosion & sea level"}, {"erosionSeaRate", "Erosion & sea level"},
|
||||||
|
{"landFractionTarget", "Erosion & sea level"}, {"seaLevelStep", "Erosion & sea level"},
|
||||||
|
{"seaLevelTol", "Erosion & sea level"}, {"seaLevelEvery", "Erosion & sea level"},
|
||||||
|
|
||||||
|
{"phase3AfterMy", "Hydrology"}, {"phase3DtScale", "Hydrology"}, {"rainfall", "Hydrology"},
|
||||||
|
{"riverThreshold", "Hydrology"}, {"riverIncision", "Hydrology"},
|
||||||
|
{"riverDischargeExp", "Hydrology"}, {"riverSlopeExp", "Hydrology"},
|
||||||
|
{"riverTransport", "Hydrology"}, {"depFrac", "Hydrology"},
|
||||||
|
|
||||||
|
{"biomeEquatorTemp", "Biome classification"}, {"biomePoleDrop", "Biome classification"},
|
||||||
|
{"biomeLatExp", "Biome classification"}, {"biomeElevLapse", "Biome classification"},
|
||||||
|
{"biomeIceTemp", "Biome classification"}, {"biomeTundraTemp", "Biome classification"},
|
||||||
|
{"biomeTaigaTemp", "Biome classification"}, {"biomeSavannaTemp", "Biome classification"},
|
||||||
|
{"biomeMountainElev", "Biome classification"}, {"biomeHillsElev", "Biome classification"},
|
||||||
|
{"biomeBeachBand", "Biome classification"}, {"biomeLowlandElev", "Biome classification"},
|
||||||
|
{"biomeWetlandMoist", "Biome classification"}, {"biomeDesertMoist", "Biome classification"},
|
||||||
|
{"biomeGrassMoist", "Biome classification"}, {"biomeTaigaMoist", "Biome classification"},
|
||||||
|
{"biomeLakeMinDepth", "Biome classification"}, {"biomeSeasonWeight", "Biome classification"},
|
||||||
|
|
||||||
|
{"climateOceanMoisture", "Climate"}, {"climateRainEfficiency", "Climate"},
|
||||||
|
{"climateOrographic", "Climate"}, {"climateOroRefHeight", "Climate"},
|
||||||
|
{"climateContinentality", "Climate"}, {"climateWindPasses", "Climate"},
|
||||||
|
{"climateMoistureSmooth", "Climate"}, {"climateCurrentFactor", "Climate"},
|
||||||
|
|
||||||
|
{"seasonAmpMax", "Seasons"}, {"seasonLatExp", "Seasons"}, {"seasonOceanFactor", "Seasons"},
|
||||||
|
{"seasonContinentRings", "Seasons"},
|
||||||
|
|
||||||
|
{"bioVegTempMin", "Biota"}, {"bioVegTempOpt", "Biota"}, {"bioVegMoistRef", "Biota"},
|
||||||
|
{"bioFaunaProductivity", "Biota"}, {"bioCarnPreyMin", "Biota"}, {"bioCarnScale", "Biota"},
|
||||||
|
{"bioFungaMoistRef", "Biota"}, {"bioFungaFloraWeight", "Biota"}, {"bioFungaTempMin", "Biota"},
|
||||||
|
{"bioRegionBonus", "Biota"}, {"bioMarineBase", "Biota"}, {"bioMarineShelfDepth", "Biota"},
|
||||||
|
{"bioMarineCoastRings", "Biota"}, {"bioFloraSlots", "Biota"}, {"bioFaunaSlots", "Biota"},
|
||||||
|
{"bioFungaSlots", "Biota"}, {"bioFloraPoints", "Biota"}, {"bioFaunaPoints", "Biota"},
|
||||||
|
{"bioFungaPoints", "Biota"},
|
||||||
|
|
||||||
|
{"dayLengthHours", "Live World"}, {"yearLengthDays", "Live World"}, {"snowTemp", "Live World"},
|
||||||
|
{"seaIceTemp", "Live World"}, {"tideAmplitude", "Live World"}, {"tideSunFactor", "Live World"},
|
||||||
|
|
||||||
|
{"weatherEvapRate", "Weather"}, {"weatherWindKmh", "Weather"}, {"weatherSatBase", "Weather"},
|
||||||
|
{"weatherSatTempCoef", "Weather"}, {"weatherCondense", "Weather"},
|
||||||
|
{"weatherOrographic", "Weather"}, {"weatherRainThresh", "Weather"},
|
||||||
|
{"weatherRainRate", "Weather"}, {"weatherCloudDissip", "Weather"},
|
||||||
|
|
||||||
|
{"weatherSystemMax", "Weather systems"}, {"weatherSpawnRate", "Weather systems"},
|
||||||
|
{"weatherSystemSpeed", "Weather systems"}, {"weatherTropicalSST", "Weather systems"},
|
||||||
|
{"weatherSystemRadius", "Weather systems"}, {"weatherSystemCloud", "Weather systems"},
|
||||||
|
{"weatherSystemRain", "Weather systems"}, {"weatherHurricaneStr", "Weather systems"},
|
||||||
|
|
||||||
|
{"volcanoProbRidge", "Volcanoes"}, {"volcanoProbBorder", "Volcanoes"},
|
||||||
|
{"volcanoProbInterior", "Volcanoes"}, {"volcanoMaxCount", "Volcanoes"},
|
||||||
|
{"volcanoBuildRate", "Volcanoes"}, {"volcanoFreeHeight", "Volcanoes"},
|
||||||
|
{"volcanoInitialBuildMax", "Volcanoes"}, {"volcanoMaxHeight", "Volcanoes"},
|
||||||
|
{"volcanoDormancyRate", "Volcanoes"}, {"volcanoDormantMinYears", "Volcanoes"},
|
||||||
|
{"volcanoDormantMaxYears", "Volcanoes"}, {"volcanoExplodeDropFrac", "Volcanoes"},
|
||||||
|
{"volcanoActivityDecay", "Volcanoes"}, {"volcanoDeadActivity", "Volcanoes"},
|
||||||
|
{"volcanoBlastRadius", "Volcanoes"}, {"volcanoBlastCloud", "Volcanoes"},
|
||||||
|
{"volcanoAshMinYears", "Volcanoes"}, {"volcanoAshMaxYears", "Volcanoes"},
|
||||||
|
{"volcanoAshPuffCellsPerWeek", "Volcanoes"}, {"volcanoAshCloud", "Volcanoes"},
|
||||||
|
{"volcanoAshCooling", "Volcanoes"},
|
||||||
|
|
||||||
|
{"geoContinentMinCells", "Geography"}, {"geoSeaMaxCells", "Geography"},
|
||||||
|
{"geoOceanSepRadians", "Geography"}, {"geoOceanDeep", "Geography"},
|
||||||
|
{"geoMountainElev", "Geography"}, {"geoRangeMinCells", "Geography"},
|
||||||
|
{"geoRiverMinDischarge", "Geography"}, {"geoMaxRivers", "Geography"}, {"geoMaxPeaks", "Geography"},
|
||||||
|
|
||||||
|
{"civMaxSettlements", "Civilization & settlements"}, {"civMinSpacingRadians", "Civilization & settlements"},
|
||||||
|
{"civClusterExp", "Civilization & settlements"}, {"civMinHabitability", "Civilization & settlements"},
|
||||||
|
{"civSeedPopulation", "Civilization & settlements"}, {"civGrowthRate", "Civilization & settlements"},
|
||||||
|
{"civMaxPopulation", "Civilization & settlements"}, {"civTownPop", "Civilization & settlements"},
|
||||||
|
{"civCityPop", "Civilization & settlements"}, {"civAbandonPop", "Civilization & settlements"},
|
||||||
|
{"civHabWaterWeight", "Civilization & settlements"}, {"civHabFoodWeight", "Civilization & settlements"},
|
||||||
|
{"civHabTempOpt", "Civilization & settlements"}, {"civHabElevPenalty", "Civilization & settlements"},
|
||||||
|
{"civSiteVariety", "Civilization & settlements"}, {"civGrowthMin", "Civilization & settlements"},
|
||||||
|
{"civHarvestVar", "Civilization & settlements"}, {"civDroughtStrength", "Civilization & settlements"},
|
||||||
|
{"civDroughtPeriod", "Civilization & settlements"}, {"civDroughtThresh", "Civilization & settlements"},
|
||||||
|
{"civDroughtArid", "Civilization & settlements"}, {"civColdYearStrength", "Civilization & settlements"},
|
||||||
|
{"civFloodBonus", "Civilization & settlements"}, {"civFamineRate", "Civilization & settlements"},
|
||||||
|
{"civStormDeathRate", "Civilization & settlements"}, {"civHurricaneDeathMult", "Civilization & settlements"},
|
||||||
|
{"civMetropolisPop", "Civilization & settlements"}, {"civCrowdingLoss", "Civilization & settlements"},
|
||||||
|
{"civCondBoomCap", "Civilization & settlements"}, {"civPlagueRate", "Civilization & settlements"},
|
||||||
|
{"civPlagueDeathMin", "Civilization & settlements"}, {"civPlagueDeathMax", "Civilization & settlements"},
|
||||||
|
{"civPlagueTradeWeight", "Civilization & settlements"},
|
||||||
|
|
||||||
|
{"civTerritoryBase", "Territory & nations"}, {"civTerritoryScale", "Territory & nations"},
|
||||||
|
{"civTerritoryMax", "Territory & nations"}, {"civVassalRange", "Territory & nations"},
|
||||||
|
{"civEmpireMinMembers", "Territory & nations"}, {"civEmpirePop", "Territory & nations"},
|
||||||
|
|
||||||
|
{"warMaxConcurrent", "Conflict & war"}, {"warDeclareRate", "Conflict & war"},
|
||||||
|
{"warAmbition", "Conflict & war"}, {"warIdeology", "Conflict & war"}, {"warBorder", "Conflict & war"},
|
||||||
|
{"warWarlikeMult", "Conflict & war"}, {"warCasualtyRate", "Conflict & war"},
|
||||||
|
{"warConquerScore", "Conflict & war"}, {"warSackChance", "Conflict & war"},
|
||||||
|
{"warExhaustion", "Conflict & war"}, {"warRevoltRate", "Conflict & war"},
|
||||||
|
{"warMinRealmPop", "Conflict & war"},
|
||||||
|
|
||||||
|
{"diploDriftRate", "Diplomacy"}, {"diploAffinity", "Diplomacy"}, {"diploWarPenalty", "Diplomacy"},
|
||||||
|
{"diploTruceYears", "Diplomacy"}, {"diploWarGrudge", "Diplomacy"},
|
||||||
|
{"diploAllyThreshold", "Diplomacy"}, {"diploNonAggThreshold", "Diplomacy"},
|
||||||
|
{"diploRivalThreshold", "Diplomacy"},
|
||||||
|
|
||||||
|
{"tradeLandRange", "Trade & economy"}, {"tradeSeaRange", "Trade & economy"},
|
||||||
|
{"tradeRiverBonus", "Trade & economy"}, {"tradeMinVolume", "Trade & economy"},
|
||||||
|
{"tradeProsperityWeight", "Trade & economy"}, {"tradeWarBlock", "Trade & economy"},
|
||||||
|
{"tradeAllyBonus", "Trade & economy"}, {"tradeDiploBonus", "Trade & economy"},
|
||||||
|
{"tradeTemptWar", "Trade & economy"},
|
||||||
|
|
||||||
|
{"civColonizeRate", "Colonization"}, {"civColonyMinPop", "Colonization"},
|
||||||
|
{"civColonyMinHab", "Colonization"}, {"civColonyReach", "Colonization"},
|
||||||
|
{"civColonySeaReach", "Colonization"}, {"civColonySpacing", "Colonization"},
|
||||||
|
{"civMaxColonies", "Colonization"}, {"civColonySupply", "Colonization"},
|
||||||
|
|
||||||
|
{"cultAssimRate", "Cultural evolution"}, {"cultConvertRate", "Cultural evolution"},
|
||||||
|
{"cultConvertDominance", "Cultural evolution"}, {"cultSpreadRange", "Cultural evolution"},
|
||||||
|
{"cultPrestigeWeight", "Cultural evolution"}, {"cultSchismMinMembers", "Cultural evolution"},
|
||||||
|
{"cultSchismRange", "Cultural evolution"}, {"cultSchismMinCluster", "Cultural evolution"},
|
||||||
|
{"cultSchismRate", "Cultural evolution"}, {"cultDawnSettlementsPerCulture", "Cultural evolution"},
|
||||||
|
{"cultDawnMaxPerContinent", "Cultural evolution"},
|
||||||
|
};
|
||||||
|
|
||||||
|
// A runtime-iterable field table built from the same CONFIG_FIELDS macro, once, so a single
|
||||||
|
// generic UI loop (the Main Menu settings editor) can drive all ~240 fields instead of 240
|
||||||
|
// hand-written widgets -- the same "add a field, everything else keeps working" property
|
||||||
|
// writeConfigFields()/parseConfigStream() already give planet.cfg/saves above.
|
||||||
|
static std::vector<ConfigFieldDesc> buildConfigFieldTable() {
|
||||||
|
std::vector<ConfigFieldDesc> t;
|
||||||
|
#define FIELD_D(name) t.push_back({#name, "", false, \
|
||||||
|
[](const PlanetConfig& c){ return c.name; }, \
|
||||||
|
[](PlanetConfig& c, double v){ c.name = v; }});
|
||||||
|
#define FIELD_I(name) t.push_back({#name, "", true, \
|
||||||
|
[](const PlanetConfig& c){ return (double)c.name; }, \
|
||||||
|
[](PlanetConfig& c, double v){ c.name = (int)std::lround(v); }});
|
||||||
|
#define FIELD_U(name) t.push_back({#name, "", true, \
|
||||||
|
[](const PlanetConfig& c){ return (double)c.name; }, \
|
||||||
|
[](PlanetConfig& c, double v){ c.name = (uint32_t)std::llround(v); }});
|
||||||
|
CONFIG_FIELDS(FIELD_D, FIELD_I, FIELD_U)
|
||||||
|
#undef FIELD_D
|
||||||
|
#undef FIELD_I
|
||||||
|
#undef FIELD_U
|
||||||
|
|
||||||
|
for (auto& d : t) {
|
||||||
|
d.category = "Uncategorized"; // must never survive: every name appears in kConfigFieldCategory
|
||||||
|
for (const auto& c : kConfigFieldCategory)
|
||||||
|
if (std::strcmp(d.name, c.name) == 0) { d.category = c.category; break; }
|
||||||
|
}
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
const std::vector<ConfigFieldDesc>& configFieldTable() {
|
||||||
|
static const std::vector<ConfigFieldDesc> t = buildConfigFieldTable();
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The distinct category labels in kConfigFieldCategory's own written order (which, unlike
|
||||||
|
// configFieldTable(), IS one contiguous block per category) -- the Main Menu settings editor's
|
||||||
|
// left-rail category order.
|
||||||
|
const std::vector<const char*>& configCategoryList() {
|
||||||
|
static const std::vector<const char*> cats = [] {
|
||||||
|
std::vector<const char*> out;
|
||||||
|
for (const auto& c : kConfigFieldCategory)
|
||||||
|
if (out.empty() || std::strcmp(out.back(), c.category) != 0) out.push_back(c.category);
|
||||||
|
return out;
|
||||||
|
}();
|
||||||
|
return cats;
|
||||||
|
}
|
||||||
|
|
||||||
bool saveConfig(const std::string& path, const PlanetConfig& cfg) {
|
bool saveConfig(const std::string& path, const PlanetConfig& cfg) {
|
||||||
std::ofstream os(path);
|
std::ofstream os(path);
|
||||||
if (!os) return false;
|
if (!os) return false;
|
||||||
|
|||||||
80
test_viewerstate.cpp
Normal file
80
test_viewerstate.cpp
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
// Headless checks for the v25 viewer colour-mode/overlay save block (no window needed).
|
||||||
|
// Modeled on test_events.cpp.
|
||||||
|
|
||||||
|
#include "Viewer.hpp"
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
static void check(bool cond, const char* what) {
|
||||||
|
std::printf(" [%s] %s\n", cond ? "PASS" : "FAIL", what);
|
||||||
|
if (!cond) ++failures;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
std::printf("Viewer state: v25 colour-mode/overlay save/load round-trip\n");
|
||||||
|
Viewer v;
|
||||||
|
PlanetConfig cfg; cfg.subdivisions = 2; cfg.seed = 777;
|
||||||
|
v.cfg = cfg;
|
||||||
|
v.planet.generate(cfg);
|
||||||
|
|
||||||
|
// Set every overlay to a value that differs from the Viewer's own defaults, plus a non-default
|
||||||
|
// colour mode, so a bug that left fields untouched (rather than round-tripped) would show up.
|
||||||
|
v.mode = ColorMode::Biome;
|
||||||
|
v.showBorders = false; // default true
|
||||||
|
v.showDrift = false; // default true
|
||||||
|
v.showRivers = false; // default true
|
||||||
|
v.showGrat = true; // default false
|
||||||
|
v.dayNightOn = false; // default true
|
||||||
|
v.showTides = true; // default false
|
||||||
|
v.showCurrents = true; // default false
|
||||||
|
v.showClouds = false; // default true
|
||||||
|
v.showVolcanoes = false; // default true
|
||||||
|
v.showNames = true; // default false
|
||||||
|
v.showSettlements = false; // default true
|
||||||
|
v.showDiplomacy = false; // default true
|
||||||
|
v.showRealms = true; // default false
|
||||||
|
|
||||||
|
const char* path = "/tmp/fanworgen_viewerstate_test.save";
|
||||||
|
v.saveGame(path);
|
||||||
|
|
||||||
|
Viewer r;
|
||||||
|
r.loadGame(path);
|
||||||
|
check(r.mode == ColorMode::Biome, "v25 save/load restores the colour mode");
|
||||||
|
check(r.showBorders == false, "v25 restores showBorders");
|
||||||
|
check(r.showDrift == false, "v25 restores showDrift");
|
||||||
|
check(r.showRivers == false, "v25 restores showRivers");
|
||||||
|
check(r.showGrat == true, "v25 restores showGrat");
|
||||||
|
check(r.dayNightOn == false, "v25 restores dayNightOn");
|
||||||
|
check(r.showTides == true, "v25 restores showTides");
|
||||||
|
check(r.showCurrents == true, "v25 restores showCurrents");
|
||||||
|
check(r.showClouds == false, "v25 restores showClouds");
|
||||||
|
check(r.showVolcanoes == false, "v25 restores showVolcanoes");
|
||||||
|
check(r.showNames == true, "v25 restores showNames");
|
||||||
|
check(r.showSettlements == false, "v25 restores showSettlements");
|
||||||
|
check(r.showDiplomacy == false, "v25 restores showDiplomacy");
|
||||||
|
check(r.showRealms == true, "v25 restores showRealms");
|
||||||
|
check(r.screen == AppScreen::World, "loadGame() switches the app screen to World");
|
||||||
|
check(r.menuState.worldEverGenerated, "loadGame() marks a world as having been generated");
|
||||||
|
|
||||||
|
// Negative test: patch the save's version back to 24 (pre-v25, no mode/overlay block) and load
|
||||||
|
// into a fresh Viewer whose defaults differ from the saved-but-truncated values above -- must
|
||||||
|
// not crash, and must leave mode/overlays exactly at the fresh Viewer's own pre-load values.
|
||||||
|
{
|
||||||
|
std::fstream fs(path, std::ios::in | std::ios::out | std::ios::binary);
|
||||||
|
uint32_t oldVer = 24;
|
||||||
|
fs.seekp(4);
|
||||||
|
fs.write((char*)&oldVer, 4);
|
||||||
|
}
|
||||||
|
Viewer old;
|
||||||
|
ColorMode preLoadMode = old.mode;
|
||||||
|
bool preLoadBorders = old.showBorders, preLoadRealms = old.showRealms;
|
||||||
|
old.loadGame(path);
|
||||||
|
check(old.mode == preLoadMode, "pre-v25 saves leave the colour mode at its pre-load value");
|
||||||
|
check(old.showBorders == preLoadBorders, "pre-v25 saves leave showBorders at its pre-load value");
|
||||||
|
check(old.showRealms == preLoadRealms, "pre-v25 saves leave showRealms at its pre-load value");
|
||||||
|
|
||||||
|
std::printf(failures ? "\nFAILURES: %d\n" : "\nALL VIEWER STATE CHECKS PASSED\n", failures);
|
||||||
|
return failures ? 1 : 0;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user