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
|
||||
planet.cfg
|
||||
planet.save
|
||||
*.save
|
||||
screenshot*.png
|
||||
|
||||
# Claude Code local (machine-specific) settings
|
||||
|
||||
35
BUILD.md
35
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)
|
||||
R reseed planet (restart forming)
|
||||
+ / - 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
|
||||
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)
|
||||
@ -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/
|
||||
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
|
||||
--config PATH use an alternate config file instead of planet.cfg
|
||||
|
||||
## Files (written in the working directory)
|
||||
|
||||
planet.cfg human-editable key=value config of every PlanetConfig parameter;
|
||||
auto-created on first run, reload live with F2. Range-checked on
|
||||
load; an invalid file reverts to safe defaults (not overwritten).
|
||||
planet.save binary snapshot (versioned, currently v22: +diplomacy; v21
|
||||
planet.cfg human-editable key=value config of every PlanetConfig
|
||||
parameter; auto-created on first run, reload live with F2
|
||||
or edit any field in the Main Menu's Settings tab.
|
||||
Range-checked on load/New World; invalid settings revert
|
||||
to safe defaults (planet.cfg itself is not overwritten).
|
||||
world_<time>.save one binary snapshot per save (versioned, currently v25:
|
||||
+viewer colour-mode/overlay state; v24 +edit-mode locks;
|
||||
v23 +cultural evolution; v22 +diplomacy; v21
|
||||
+conflict/wars; v20 +settlements; v19 +ecoregions; v18
|
||||
+geography salt; v17 +geography/atlas; v16 +event log; v15
|
||||
+stateful volcanoes; v13 +Live World clock rate; v12
|
||||
+step-back history (~40 frames, so a load can rewind storms);
|
||||
v11 +weather systems/storms; v10 +weather fields; v9 +moons;
|
||||
v8 +Live World clock; v7 +biota): seed + config + full planet state; F5
|
||||
writes it, F9 reloads and resumes deterministically. 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) -- regenerate them.
|
||||
v8 +Live World clock; v7 +biota): seed + config + full planet
|
||||
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)
|
||||
|
||||
|
||||
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,
|
||||
with realms still correctly mono-cultural. `src/sim` + `src/render`; cultures are derived/not saved,
|
||||
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
|
||||
|
||||
@ -923,6 +974,8 @@ src/
|
||||
Picking.* mouse ray / sphere hit / nearest-cell / angle helpers
|
||||
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)
|
||||
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)
|
||||
Viewer.{hpp,cpp} Viewer struct: all state + setup + sim orchestration
|
||||
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) ·
|
||||
`F1` toggle the clickable **toolbar menu** (top-right of the 3D globe; every key above also has a
|
||||
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)
|
||||
`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) ·
|
||||
@ -1101,15 +1157,20 @@ Mode. Locked out (`GuiDisable`d) while the Phase-3 "start hydrology?" prompt is
|
||||
everything else.
|
||||
|
||||
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
|
||||
PlanetConfig param, auto-created on first run, reload with `F2`) and
|
||||
`planet.save` (binary: seed + config + full planet state, written/read by
|
||||
`Planet::writeState`/`readState`, resumes deterministically). Config is
|
||||
range-checked by `validateConfig()` on load/`F2`; an invalid file reverts to safe
|
||||
defaults (without overwriting your `planet.cfg`) and shows a status message. The
|
||||
save header is versioned (currently **24**; v2 adds the `[`/`]` drift rate, v3 a
|
||||
`planet.cfg` (human-editable key=value of every PlanetConfig param, auto-created
|
||||
on first run, reload with `F2` or edit any field live in the Main Menu's Settings
|
||||
tab) plus **one `world_<timestamp>.save` file per save** in the working dir
|
||||
(binary: seed + config + full planet state, written/read by
|
||||
`Planet::writeState`/`readState`, resumes deterministically) — `F5`/the toolbar
|
||||
"Save" button always write a fresh timestamped file, `F9`/"Load" (or the Main
|
||||
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
|
||||
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**
|
||||
@ -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
|
||||
(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
|
||||
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
|
||||
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
|
||||
@ -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
|
||||
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
|
||||
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
|
||||
saved one.
|
||||
**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/Panels.cpp
|
||||
src/render/Toolbar.cpp
|
||||
src/render/MainMenu.cpp
|
||||
src/render/RayGuiImpl.cpp
|
||||
src/render/Viewer.cpp
|
||||
src/render/ViewerInput.cpp
|
||||
@ -106,3 +107,11 @@ if(UNIX AND NOT APPLE)
|
||||
target_link_libraries(test_events PRIVATE m pthread dl)
|
||||
endif()
|
||||
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();
|
||||
{
|
||||
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 + halfW + GAP, r.y, halfW, r.height }, "Load (F9)")) v.loadGame(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)")) {
|
||||
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.
|
||||
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;
|
||||
// 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;
|
||||
|
||||
// --- Civilization & Ecology (all need a settled world) ------------------
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <set>
|
||||
#include <utility>
|
||||
@ -129,18 +130,15 @@ bool Viewer::init(int argc, char** argv) {
|
||||
if (cliSeed != 0) cfg.seed = cliSeed; // CLI --seed overrides config
|
||||
std::string cfgErr = validateConfig(cfg);
|
||||
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.fovy = 45; cam.projection = CAMERA_PERSPECTIVE;
|
||||
|
||||
buildBorders(planet, borderR, borders, ridgeBorders);
|
||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||
graticule = buildGraticule();
|
||||
buildMap2D(planet, mapRect, map2D);
|
||||
phase3PromptAt = planet.cfg.phase3AfterMy;
|
||||
graticule = buildGraticule(); // planet-independent, safe to build before any world exists
|
||||
|
||||
refreshView(); // colour the freshly generated (flat) world
|
||||
screen = AppScreen::MainMenu;
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -364,6 +362,22 @@ void Viewer::regenWorld() { // after generate(): geometry change
|
||||
|
||||
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
|
||||
maxChange = planet.step(); ++stepCount;
|
||||
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);
|
||||
}
|
||||
|
||||
// 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.
|
||||
void Viewer::saveGame(const char* path) {
|
||||
std::ofstream os(path, std::ios::binary);
|
||||
@ -614,6 +648,25 @@ void Viewer::saveGame(const char* path) {
|
||||
os.write((char*)&e.sourceId, 4);
|
||||
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");
|
||||
}
|
||||
|
||||
@ -759,6 +812,13 @@ void Viewer::loadGame(const char* path) {
|
||||
}
|
||||
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();
|
||||
// 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.
|
||||
@ -767,7 +827,27 @@ void Viewer::loadGame(const char* path) {
|
||||
buildDriftArrows(planet, driftR, driftArrows, plateLabels);
|
||||
buildMap2D(planet, mapRect, map2D);
|
||||
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();
|
||||
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)"
|
||||
: std::string("Loaded ") + path);
|
||||
}
|
||||
@ -1104,9 +1184,11 @@ Rectangle Viewer::mapViewRect() const {
|
||||
void Viewer::run() {
|
||||
while (!WindowShouldClose()) {
|
||||
handleInput();
|
||||
if (screen == AppScreen::World) {
|
||||
stepSim();
|
||||
// A regenerate this frame may have shrunk the planet; keep indices valid.
|
||||
if (hovered >= (int)planet.cells.size()) hovered = -1;
|
||||
}
|
||||
renderFrame();
|
||||
}
|
||||
UnloadRenderTexture(rt3d);
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
#include "Overlays.hpp" // PlateLabel
|
||||
#include "Map2D.hpp" // Map2D
|
||||
#include "Panels.hpp" // EditField, EditPanelState
|
||||
#include "MainMenu.hpp" // AppScreen, MenuTab, MainMenuState
|
||||
#include <vector>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
@ -16,13 +17,16 @@
|
||||
// ViewerInput.cpp (input/picking/keys) and ViewerRender.cpp (drawing).
|
||||
struct Viewer {
|
||||
// ---- 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 EVENT_LOG_MAX = 200;
|
||||
const char* CONFIG_PATH = "planet.cfg";
|
||||
const char* SAVE_PATH = "planet.save";
|
||||
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) --------------------------------------
|
||||
int screenW = 1920, screenH = 1080;
|
||||
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; }
|
||||
void regenWorld(); // after generate(): geometry changed
|
||||
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 pauseAction();
|
||||
void setStatus(const std::string& m);
|
||||
@ -278,3 +284,8 @@ struct Viewer {
|
||||
void renderHUD();
|
||||
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 "Map2D.hpp" // wrapPi
|
||||
#include "Projection.hpp" // EqualEarth, lonLatToDir
|
||||
#include "MainMenu.hpp" // handleMainMenuInput
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
@ -10,6 +11,7 @@
|
||||
// members (mp/onPause/hovered/hasHoverSub/hoverSub/hoveredSubIdx) for the render
|
||||
// pass, and may step / regenerate / save / load the world.
|
||||
void Viewer::handleInput() {
|
||||
if (screen == AppScreen::MainMenu) { handleMainMenuInput(*this); return; }
|
||||
mp = GetMousePosition();
|
||||
bool in3D = (mp.x < view3DW && mp.y < view3DH); // top-left quadrant
|
||||
bool inMap = CheckCollisionPointRec(mp, mapRect);
|
||||
@ -241,8 +243,12 @@ void Viewer::handleInput() {
|
||||
regen();
|
||||
setStatus(cerr.empty() ? std::string("Reloaded ") + configPath : "Bad config — using defaults");
|
||||
} else setStatus(std::string("No ") + configPath); }
|
||||
if (IsKeyPressed(KEY_F5)) saveGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F9)) loadGame(SAVE_PATH);
|
||||
if (IsKeyPressed(KEY_F5)) saveGame(makeTimestampedSavePath().c_str());
|
||||
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_F11)) {
|
||||
if (IsKeyDown(KEY_LEFT_SHIFT) || IsKeyDown(KEY_RIGHT_SHIFT)) exportAtlasImage();
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
#include "Map2D.hpp"
|
||||
#include "Panels.hpp"
|
||||
#include "Toolbar.hpp" // drawToolbar
|
||||
#include "MainMenu.hpp" // drawMainMenu
|
||||
#include "Picking.hpp" // rotateZ (axial-tilt transform for labels)
|
||||
#include "rlgl.h"
|
||||
#include "Projection.hpp" // dirToLonLat (plate labels)
|
||||
@ -1169,6 +1170,15 @@ void Viewer::renderPrompt() {
|
||||
// One full frame: globe texture, then composite + 3D labels + map + panels +
|
||||
// HUD + prompt onto the screen.
|
||||
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();
|
||||
|
||||
BeginDrawing();
|
||||
|
||||
@ -483,3 +483,19 @@ private:
|
||||
bool loadConfig(const std::string& path, PlanetConfig& cfg);
|
||||
bool saveConfig(const std::string& path, 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 <set>
|
||||
#include <utility>
|
||||
#include <cstring>
|
||||
|
||||
// --- 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) {
|
||||
std::ofstream os(path);
|
||||
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