Improved data handling
This commit is contained in:
@@ -1457,7 +1457,42 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
inline in the scene JSON when not streaming, in `roads/towns.json`
|
||||
when streaming; the Town Generator panel edits the area list
|
||||
(Pick Center reuses the origin-pick arm) and area outlines draw in
|
||||
dark blue next to the lot loops in road edit mode. **Building
|
||||
dark blue next to the lot loops in road edit mode. **Layout metadata
|
||||
and towns** (phase 5.9): library entries accept
|
||||
`(layout name [:tags (tag ...)] [:score n] body...)` — tags match
|
||||
the area's Preferred Types (`RoadTownArea::preferredTypes`, else the
|
||||
district default tag set), `:score` weights the per-lot pick
|
||||
(`TownBuilding::layoutName` records the choice); a stock library
|
||||
draws no RNG (bit-identical towns). `RoadGraph::towns`
|
||||
(`RoadTown{name,seed}`) groups areas into settlements —
|
||||
`RoadTownArea::town` picks the town and
|
||||
`RoadNetworkGen::effectiveAreaSeed()` mixes town+area seeds (every
|
||||
seed consumer uses it); population NPCs record it in the `npcTown`
|
||||
registry column. The panel has a Towns block (per-town
|
||||
`TownGenSystem::generateAreasForTown()` regenerates only that
|
||||
town's containers; membership is editable both ways — the area's
|
||||
Town dropdown, or the town's member-area list with Remove/Add and
|
||||
rename propagation) and per-area Town/Preferred Types widgets; the
|
||||
demo-taxi ships `town_layouts.txt` (c-shape-house/cottage/
|
||||
two-floor-house/corner-shop) with `preferredTypes ["house",
|
||||
"village"]` on its area. The panel's **Layout Library...** window
|
||||
(phase 5.9.6, also **Tools -> Layout Library Editor**; rendered from
|
||||
EditorUISystem via `TerrainEditor::renderLayoutWindows()`) edits the
|
||||
whole `town_layouts.txt` in place (live
|
||||
reparse, entry list with tags/score, Save/Reload/Insert Template)
|
||||
and spawns one-off preview buildings at the camera target via
|
||||
`TownGenSystem::previewLayout()` — district bindings, Exterior/
|
||||
Interior/Both mode, seed; the preview lives in a "Layout Preview"
|
||||
container that clear()/countTowns() skip and scene serialization
|
||||
excludes. demo-taxi stages the file as a symlink to the source
|
||||
tree, so Save edits the versioned file directly (no sync-back
|
||||
step); likewise the runtime-written per-terrain data directories
|
||||
(`roads/`, `terrain_fixup/`, `heightmap.bin_blendmaps/` under
|
||||
`heightmaps/4242424300000001/`) are symlinked to the source tree
|
||||
by stage_runtime.cmake, so road/lot-param/terrain saves land in
|
||||
versioned files directly — commit them after editing (this
|
||||
replaced the old configure_file copies + `sync_road_regions`,
|
||||
which let a reconfigure overwrite unsynced staged edits). **Building
|
||||
streaming** (phase 4.5 item 5): `TownGen::generateTown()` takes a
|
||||
`PageScope` (render-XZ -> physical-page `PageMapper` + filter) that
|
||||
switches the RNG to per-page streams (`seed ^ hash(page)`), so a
|
||||
|
||||
@@ -2141,3 +2141,127 @@ Headless coverage per the plan: params round-trip and regeneration
|
||||
stability (1), DSL evaluation incl. lot-size conditionals and parking
|
||||
(2), letter-suffix splitting (3), two-area independence (4),
|
||||
per-page == global building sets (5).
|
||||
|
||||
## 22. Layout Metadata, Weighted Template Selection, Towns (taxi demo phase 5.9)
|
||||
|
||||
Phase 5.9 makes the building layout library an authoring tool instead
|
||||
of a fixed district table: library entries carry type tags and a
|
||||
selection weight, town areas declare which building types they prefer,
|
||||
and several areas can be grouped into a named town sharing one
|
||||
settlement identity.
|
||||
|
||||
### Layout metadata
|
||||
|
||||
`town_layouts.txt` entries (and any `LayoutLibrary::loadSource`
|
||||
input) accept two optional clauses between the layout name and the
|
||||
body:
|
||||
|
||||
```
|
||||
(layout name [:tags (tag ...)] [:score n] body...)
|
||||
```
|
||||
|
||||
- `:tags` — free-form type keywords (`house`, `village`, `tower`,
|
||||
`commercial`, ...). Matched against the town area's *Preferred
|
||||
Types* (see below); only layouts sharing at least one tag are
|
||||
candidates for a lot.
|
||||
- `:score` — floating point weight of the random pick between
|
||||
candidates (default 1; 0 allowed — all-zero candidate lists fall
|
||||
back to a uniform pick).
|
||||
|
||||
`LayoutLibrary::Entry` stores `body`/`tags`/`score`;
|
||||
`findEntry(name)` exposes the metadata and
|
||||
`matching(preferredTags)` returns the tag-intersecting entries (an
|
||||
empty preferred set matches nothing — callers substitute their own
|
||||
default set first). Malformed clauses are hard load errors. The
|
||||
built-ins carry fixed tags: `village-house` {house, village},
|
||||
`town-rowhouse` {house, town}, `city-block` {apartments,
|
||||
residential}, `city-tower` {tower, center, commercial}.
|
||||
|
||||
### Selection algorithm (TownGen::generateTown)
|
||||
|
||||
A lot without an explicit edge-side `layout` picks its template by
|
||||
weighted Monte-Carlo:
|
||||
|
||||
1. Preferred set = the area's `preferredTypes`, else the district
|
||||
default set (Village {village}, Town {town}, CityResidential
|
||||
{residential}, CityCenter {center} — each matches exactly its own
|
||||
built-in in a stock library).
|
||||
2. Candidates = `LayoutLibrary::matching(preferred)`. No candidates
|
||||
-> the district default layout (silent fallback).
|
||||
3. If the candidate list is exactly the district default (stock
|
||||
library), **no RNG is drawn** — stock towns stay bit-identical to
|
||||
the pre-5.9 generator (probe-verified).
|
||||
4. Otherwise one `unit(rng)` draw picks a candidate with probability
|
||||
proportional to its score; the draw comes from the lot's own RNG
|
||||
stream, so per-lot picks are independent of edits to other lots.
|
||||
|
||||
The chosen library name is recorded on `TownBuilding::layoutName`
|
||||
(`<inline>` for edge-side inline source), and the towns JSON stores
|
||||
`preferredTypes` per area (sparse).
|
||||
|
||||
### Towns (area grouping)
|
||||
|
||||
`RoadGraph::towns` holds named `RoadTown { name, seed }` settlements;
|
||||
`RoadTownArea::town` (empty = standalone) assigns an area to one.
|
||||
The effective generation seed mixes both seeds:
|
||||
|
||||
```
|
||||
effectiveAreaSeed = area.seed (standalone or dangling ref)
|
||||
= town.seed ^ (area.seed * 0x9e3779b9 + 0x85ebca6b) (town member)
|
||||
```
|
||||
|
||||
so the whole settlement shifts with the town seed while each area
|
||||
stays individually tunable; every seed consumer (town generation,
|
||||
population, locations, schedules) goes through
|
||||
`RoadNetworkGen::effectiveAreaSeed()`. Population NPCs record the
|
||||
town name in the `npcTown` registry column (the standalone area name
|
||||
otherwise; `npcArea` is unchanged). `towns` and the per-area `town`
|
||||
reference serialize sparsely next to `townAreas`; old scenes load
|
||||
unchanged.
|
||||
|
||||
### Editor and demo
|
||||
|
||||
The Town Generator panel gained a **Towns** block (add/remove/rename,
|
||||
seed, per-town **Generate Town** — regenerates only that town's area
|
||||
containers via `TownGenSystem::generateAreasForTown()`, leaving other
|
||||
generated towns untouched) and per-area **Town** dropdown plus
|
||||
**Preferred Types** comma-separated tag field. Membership is editable
|
||||
from both sides: the area's Town dropdown, or the town's own block,
|
||||
which lists its member areas with Remove buttons and an "Add Area"
|
||||
combo (entries show the current owner); renaming a town rewrites the
|
||||
`RoadTownArea::town` reference of all its members. The DSL help window
|
||||
documents the metadata clauses.
|
||||
|
||||
The **Layout Library...** button (Town Generator panel) or **Tools ->
|
||||
Layout Library Editor** opens a whole-file editor for the
|
||||
project's `town_layouts.txt` (phase 5.9.6): the entry list (built-ins
|
||||
plus the buffer's `(layout name ...)` forms, with tags/score), a
|
||||
live-reparsed library (parse errors shown inline), DSL Help / Reload /
|
||||
Save / Insert Template buttons, and a preview row — district, mode
|
||||
(Exterior/Interior/Both = the `regenerateBuilding` modes), seed, and
|
||||
**Spawn / Update Preview** / **Clear Preview**. The preview evaluates
|
||||
the selected entry against representative bindings (40x40 m lot, 35x35
|
||||
m setback rect, the district's footprint/floor ranges, density pass)
|
||||
with a scratch RNG and spawns one building under a persistent
|
||||
"Layout Preview" container (no "TownGen " prefix, so
|
||||
clear()/countTowns()/regenerate* skip it; still GeneratedTownTag, so it
|
||||
never serializes) at the editor camera target, Y-snapped to the
|
||||
terrain — `TownGenSystem::previewLayout()` / `clearLayoutPreview()`.
|
||||
Evaluation warnings surface in the Town Generator warnings list with a
|
||||
"preview:" prefix. demo-taxi stages `town_layouts.txt` next to the
|
||||
executable as a **symlink to the source file** (not a configure_file
|
||||
copy), so the editor's Save writes the versioned source file directly —
|
||||
just commit it; there is no sync-back step.
|
||||
|
||||
The demo-taxi area sets `preferredTypes: ["house", "village"]` and
|
||||
ships `demos/demo-taxi/town_layouts.txt` (symlinked by CMake next to
|
||||
the executable): the C-shape 3-room house authored on road page 2_2
|
||||
(`c-shape-house`, score 3), a `cottage` (2), a `two-floor-house` (1)
|
||||
and a `corner-shop` (1), all with `:type`-tagged rooms for the
|
||||
interior generator.
|
||||
|
||||
Headless coverage: metadata parse round trip + malformed clause
|
||||
errors, `matching()` tag query, weighted pick distribution and
|
||||
determinism under the seed, preferredTypes override and unmatched
|
||||
fallback, stock-library bit-identity, towns JSON round trip,
|
||||
`effectiveAreaSeed` stability (standalone / mixed / dangling).
|
||||
|
||||
@@ -725,6 +725,20 @@ struct RoadPopulationRules {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A named town/city grouping several town areas (taxi demo phase
|
||||
* 5.9). Purely organisational: a town gives its areas a shared
|
||||
* identity (name shown in the UI and the NPC registry `npcTown`) and
|
||||
* a shared seed component — an area whose `town` names a RoadTown
|
||||
* derives its effective seed from BOTH seeds, so the whole settlement
|
||||
* shifts together while each area stays individually tunable.
|
||||
* Areas without a town generate exactly as before (phase 4.5).
|
||||
*/
|
||||
struct RoadTown {
|
||||
std::string name;
|
||||
uint32_t seed = 1;
|
||||
};
|
||||
|
||||
/**
|
||||
* A town generation scope on the road graph (taxi demo phase 4.5 item
|
||||
* 4). Areas are the only generation scope of
|
||||
@@ -737,6 +751,9 @@ struct RoadPopulationRules {
|
||||
*/
|
||||
struct RoadTownArea {
|
||||
std::string name;
|
||||
/** Owning town (phase 5.9): a name from RoadGraph::towns, empty
|
||||
* = standalone area. */
|
||||
std::string town;
|
||||
/** false: axis-aligned square (halfSize); true: circle (radius). */
|
||||
bool circular = false;
|
||||
/** Area center (world/render XZ; y is a display fallback). */
|
||||
@@ -748,6 +765,11 @@ struct RoadTownArea {
|
||||
RoadTownDistrict district = RoadTownDistrict::Village;
|
||||
float densityScale = 1.0f;
|
||||
uint32_t seed = 1;
|
||||
/** Preferred building-type tags for the layout template
|
||||
* selection (phase 5.9): library layouts whose tags intersect
|
||||
* this set compete for the area's lots (weight = layout score).
|
||||
* Empty = the district's default tag set. */
|
||||
std::vector<std::string> preferredTypes;
|
||||
/** Population generation rules (phase 4.7.2; inherit sentinels
|
||||
* fall back to the TownPopulation defaults). */
|
||||
RoadPopulationRules population;
|
||||
@@ -812,6 +834,12 @@ struct RoadGraph {
|
||||
*/
|
||||
std::vector<RoadTownArea> townAreas;
|
||||
|
||||
/**
|
||||
* Named towns grouping town areas (phase 5.9; see RoadTown).
|
||||
* Persisted alongside townAreas.
|
||||
*/
|
||||
std::vector<RoadTown> towns;
|
||||
|
||||
/**
|
||||
* Monotonic version counter incremented by every mutating operation.
|
||||
*
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
# (demo_scene_interior.json / demo_scene_exterior.json, so scene edits in
|
||||
# the source tree are visible without a rebuild) plus the big pre-staged
|
||||
# runtime directories (resources/, characters/, lua-scripts/) from the
|
||||
# editScene binary directory; the terrain heightmap is staged separately
|
||||
# by configure_file (see below) so source changes re-copy it on the next
|
||||
# build. Run it from here:
|
||||
# editScene binary directory, plus the runtime-written per-terrain data
|
||||
# directories (roads/, terrain_fixup/, heightmap.bin_blendmaps/) from
|
||||
# the source tree so saves land in versioned files directly; the
|
||||
# terrain heightmap is staged separately by configure_file (see below)
|
||||
# so source changes re-copy it on the next build. Run it from here:
|
||||
# cd <build>/src/features/editScene/demos/demo-taxi
|
||||
# ./demoTaxi
|
||||
# Controls: mouse = look, W/A/S/D = move, Shift = run, E = use door,
|
||||
@@ -76,35 +78,26 @@ configure_file("${CMAKE_CURRENT_SOURCE_DIR}/prefabs/sedan-traffic.json"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/prefabs/sedan-traffic.json"
|
||||
COPYONLY)
|
||||
|
||||
# Road region files (heightmaps/<terrainId>/roads/<px>_<py>.json). The
|
||||
# source tree is authoritative — edit the road network while running from
|
||||
# the source directory; every file registered here is re-copied into the
|
||||
# staging directory on the next build (configure_file tracks the input, so
|
||||
# saving the road in the editor triggers a re-copy on the next build).
|
||||
# Edits made while running from the build directory are overwritten by the
|
||||
# source copy at the next build. CONFIGURE_DEPENDS picks up region files
|
||||
# newly created in the source tree (e.g. the road crosses into a new page).
|
||||
file(GLOB DEMO_ROAD_REGION_FILES CONFIGURE_DEPENDS
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/heightmaps/4242424300000001/roads/*.json")
|
||||
foreach(DEMO_ROAD_REGION_FILE ${DEMO_ROAD_REGION_FILES})
|
||||
get_filename_component(DEMO_ROAD_REGION_NAME
|
||||
"${DEMO_ROAD_REGION_FILE}" NAME)
|
||||
configure_file("${DEMO_ROAD_REGION_FILE}"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/heightmaps/4242424300000001/roads/${DEMO_ROAD_REGION_NAME}"
|
||||
COPYONLY)
|
||||
endforeach()
|
||||
# Town layout library (phase 5.9): tagged/scored building layout
|
||||
# templates loaded by TownGenSystem::loadLayoutLibrary() from the CWD.
|
||||
# Staged as a symlink to the source file, not a copy: the Layout
|
||||
# Library editor's Save writes through the link straight into the
|
||||
# source tree (plain ofstream trunc, no rename), so there is no
|
||||
# sync-back step and no stale staged copy. The rm -f clears the
|
||||
# regular file left behind by the previous configure_file staging.
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E rm -f
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/town_layouts.txt")
|
||||
execute_process(COMMAND ${CMAKE_COMMAND} -E create_symlink
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/town_layouts.txt"
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/town_layouts.txt")
|
||||
|
||||
# Sync-back helper for the source-authoritative region files above:
|
||||
# edits made while running from the build directory (lot params, layout
|
||||
# DSL, road tweaks saved by the editor) live only in the staging copy
|
||||
# until they are copied back, and the next configure overwrites them.
|
||||
# Run `cmake --build build-vscode --target sync_road_regions` after
|
||||
# editing roads/lot params in the editor, then commit the source files.
|
||||
add_custom_target(sync_road_regions
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
"${CMAKE_CURRENT_BINARY_DIR}/heightmaps/4242424300000001/roads"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/heightmaps/4242424300000001/roads"
|
||||
COMMENT "Syncing road region files back to the source tree")
|
||||
# Per-terrain runtime data directories (roads/, terrain_fixup/,
|
||||
# heightmap.bin_blendmaps/ under heightmaps/4242424300000001/) are
|
||||
# SYMLINKED to the source tree by stage_runtime.cmake at build time,
|
||||
# like the demo scenes: editor/game saves (road regions, lot params,
|
||||
# terrain fixups, blend maps) write straight into the versioned source
|
||||
# files, so there is no sync-back step and no staged copy a reconfigure
|
||||
# could overwrite. Commit new/changed files after editing.
|
||||
|
||||
add_executable(demoTaxi
|
||||
demo_main.cpp
|
||||
|
||||
@@ -996,7 +996,12 @@ Follow-ups (2026-09-30, apron terrain interface):
|
||||
Workflow rule: anything that persists road/terrain data at runtime
|
||||
must land in (or be synced to) the SOURCE-tree
|
||||
`heightmaps/4242424300000001/` — the build-dir copy is disposable
|
||||
staging.
|
||||
staging. **Update (2026-10-23)**: this rule is now enforced by
|
||||
construction — `roads/`, `terrain_fixup/` and
|
||||
`heightmap.bin_blendmaps/` are symlinked to the source tree by
|
||||
stage_runtime.cmake (configure_file copies and the
|
||||
`sync_road_regions` target were removed), so runtime saves write
|
||||
the versioned files directly; just commit them.
|
||||
- **Pin-zone float precision fix (2026-10-01, edge-21 notch)**: the
|
||||
edge-21 top-view notch (a rectangular apron-width hole mid-edge, plus
|
||||
side "pinching") turned out to be real after all — reproducible only
|
||||
@@ -3004,7 +3009,91 @@ taxi's lights should look distinct.
|
||||
- We need to implement template layouts which can be used instead of default layout
|
||||
- Town area should be able to select preferred types when one is not declared explicitly from templates, using type tag keywords.
|
||||
- Weighted/scored Monte-Carlo selection should be used. Low-scored templates still should be able to show up but under lower frequency.
|
||||
- Should be able to group town areas into single town/city and have multiple towns each potentially having multiple areas. That is needed for flexibility like having industrial, residential, commercial part and town center, for example, with templatelayouts and some unique buildings.
|
||||
- Should be able to group town areas into single town/city and have multiple towns each potentially having multiple areas. That is needed for flexibility like having industrial, residential, commercial part and town center, for example, with template layouts and some unique buildings.
|
||||
|
||||
**Status (2026-10-22): done.** What was implemented:
|
||||
|
||||
- Layout library metadata: entries accept
|
||||
`(layout name [:tags (tag ...)] [:score n] body...)`
|
||||
(`LayoutLibrary::Entry{body,tags,score}`, `findEntry()`,
|
||||
`matching(preferredTags)`); malformed clauses are hard load errors;
|
||||
`;` comments supported.
|
||||
- Built-in layouts carry fixed tag sets matching their district:
|
||||
`village-house`{house,village}, `town-rowhouse`{house,town},
|
||||
`city-block`{apartments,residential},
|
||||
`city-tower`{tower,center,commercial}.
|
||||
- Weighted per-lot template selection in `TownGen::generateTown()`
|
||||
for lots without an explicit edge-side layout: candidates =
|
||||
`matching(preferred)` where preferred = the area's
|
||||
`RoadTownArea::preferredTypes`, else the district default tag set;
|
||||
weight = `:score` (all-zero scores fall back to uniform); the pick
|
||||
comes from the lot's own RNG stream and is recorded on
|
||||
`TownBuilding::layoutName` (`<inline>` for inline source).
|
||||
- No-match fallback to the district default layout; stock-library
|
||||
shortcut (candidates == exactly the district default) draws no RNG,
|
||||
keeping stock towns bit-identical to the pre-5.9 generator.
|
||||
- Towns grouping: `RoadGraph::towns` (`RoadTown{name,seed}`),
|
||||
`RoadTownArea::town` (empty = standalone),
|
||||
`RoadNetworkGen::effectiveAreaSeed()` mixes town+area seeds
|
||||
(`t.seed ^ (a.seed * 0x9e3779b9 + 0x85ebca6b)`; standalone/dangling
|
||||
ref -> `a.seed`) and feeds every seed consumer (town gen,
|
||||
population, locations, schedules).
|
||||
- Population NPCs record the town in the new `npcTown` registry
|
||||
column (standalone areas record the area name; `npcArea`
|
||||
unchanged).
|
||||
- Serialization: `towns` array + per-area `town`/`preferredTypes`
|
||||
stored sparsely in the towns JSON (`roads/towns.json` when
|
||||
streaming); old scenes load unchanged.
|
||||
- Editor UI (Town Generator panel): Towns block (add/remove/rename,
|
||||
seed, per-town **Generate Town** via
|
||||
`TownGenSystem::generateAreasForTown()` — replaces only that town's
|
||||
containers, other towns untouched); per-area **Town** dropdown and
|
||||
**Preferred Types** comma-separated tag field; DSL help window
|
||||
documents the metadata clauses and selection.
|
||||
- Follow-up (2026-10-22): town-side membership management — each town
|
||||
lists its member areas with Remove buttons plus an "Add Area" combo
|
||||
(labels show the current owner), and renaming a town rewrites the
|
||||
`RoadTownArea::town` of all members.
|
||||
- Follow-up (2026-10-22): **Layout Library...** editor window (Town
|
||||
Generator panel, or **Tools -> Layout Library Editor**; rendered from
|
||||
EditorUISystem via `TerrainEditor::renderLayoutWindows()` so it does
|
||||
not need the terrain panel open) — whole-file editing of the project's
|
||||
`town_layouts.txt` with live reparse (inline parse errors), an entry
|
||||
list (built-ins + buffer entries with tags/score), DSL Help / Reload
|
||||
/ Save / Insert Template, and a preview row (district, Exterior/
|
||||
Interior/Both mode, seed) spawning a one-off preview building at the
|
||||
camera target via `TownGenSystem::previewLayout()` /
|
||||
`clearLayoutPreview()` ("Layout Preview" container: no "TownGen "
|
||||
prefix, GeneratedTownTag, never serialized; evaluation warnings land
|
||||
in the panel's warnings list prefixed "preview:"). The demo stages
|
||||
`town_layouts.txt` as a symlink to the source file, so Save edits
|
||||
the versioned file directly — commit, no sync step.
|
||||
- Follow-up fix (2026-10-22): Jolt `EPAPenetrationDepth` assert
|
||||
(`!ioV.IsNearZero()`) — sliver triangles in road chunk colliders and
|
||||
CellGrid roofSide meshes passed the old absolute degenerate filter;
|
||||
`createMeshShape` now also drops triangles with
|
||||
`cross2 < 4e-12 * maxLen2^2` (relative sliver guard). Road chunks
|
||||
produce 4-22 slivers each — harmless (no collision surface) but
|
||||
worth a geometry-side look later.
|
||||
- Demo content: `demos/demo-taxi/town_layouts.txt` (symlinked next
|
||||
to the executable, so editor Saves land in the source tree directly) with four tagged/scored templates —
|
||||
`c-shape-house` (the restored 3-room C-shape from road page 2_2,
|
||||
score 3), `cottage` (2), `two-floor-house` (1), `corner-shop` (1),
|
||||
all with `:type`-tagged rooms for the interior generator; the demo
|
||||
area prefers `["house", "village"]`.
|
||||
- Headless probes (`road_geometry_overlap_test` PASSED): metadata
|
||||
parse round trip + malformed clause errors, `matching()` tag query,
|
||||
weighted pick distribution + determinism under the seed,
|
||||
preferredTypes override and unmatched fallback, stock-library
|
||||
bit-identity, towns JSON round trip, `effectiveAreaSeed()` stability
|
||||
(standalone / mixed / dangling).
|
||||
- Docs: `ProceduralRoadGeometry.md` §22, `src/features/editScene/AGENTS.md`
|
||||
town-areas paragraph, this status block.
|
||||
|
||||
**Follow-up requirements (added 2026-10-22)** — population/resident
|
||||
control for towns, areas and layouts: moved to **Phase 8.5** (they
|
||||
depend on furniture in 6.5, beds/interiors in 8 and the 4.7
|
||||
population/schedule systems).
|
||||
|
||||
### Phase 6 — Foot traffic NPCs
|
||||
|
||||
@@ -3114,7 +3203,48 @@ taxi-specific):
|
||||
produce identical interiors (determinism check); a multi-storey
|
||||
building's upper floor is reachable by stairs.
|
||||
|
||||
### Phase 8.5 car effects
|
||||
### Phase 8.5 — Population, residents & schedule POIs
|
||||
|
||||
Population/resident control for towns, areas and layouts (follow-up
|
||||
requirements moved from Phase 5.9). Builds on the 4.7
|
||||
population/schedule systems, the 5.9 layout metadata (tags/score) and
|
||||
the Phase 6.5/8 furniture/interior work — layouts already carry
|
||||
`:type`-tagged rooms for this.
|
||||
|
||||
- **Population control at three levels**: town, area and layout must
|
||||
each be able to define and control the population in numbers
|
||||
(household counts/sizes) and types (age bands, roles).
|
||||
- **Layout-level resident specification** (building layout DSL):
|
||||
declare residents per room with age and sex (e.g. `adult`, `male`)
|
||||
as a list with template/optional parts — e.g. "5-10 people" with
|
||||
explicit relationships between them — supporting both strict
|
||||
adherence and randomized variation; resident NPC metadata is given
|
||||
in keyword form.
|
||||
- Resident lists are DSL data too: the existing condition and loop
|
||||
blocks (`if`/`cond`/`dotimes`/`for-range`) must work for building
|
||||
resident lists, not just geometry.
|
||||
- Resident specs can be conditional on the same bindings layouts
|
||||
already use — lot size (`lot-w`/`lot-d`/`avail-w`/`avail-d`),
|
||||
district ranges, density roll — so a bigger lot can house a
|
||||
bigger household by the same mechanism a bigger lot gets a bigger
|
||||
shell.
|
||||
- **Layout-level job/interior POIs**: layouts specify interior
|
||||
POIs/jobs consumed by the NPC schedule; these drive job-related
|
||||
furniture spawns in the rooms (Phase 6.5 furniture), and for the
|
||||
exterior they give more control over where interior POIs are
|
||||
placed. POIs must carry a work time range. Behavior driven by
|
||||
GOAP/HTN, behavior trees and the Lua API.
|
||||
- **Area-level NPC relationships**: relationships between NPCs from
|
||||
different groups (needs research).
|
||||
- **Default resident generation**: when a layout specifies no
|
||||
residents, generate them deterministically using the room numbers
|
||||
as reference; must stay consistent with bed placement (Phase 6.5/8
|
||||
interiors); default family relationships are still generated
|
||||
(needs research).
|
||||
- **Exterior schedule POIs**: same data model as interior POIs plus
|
||||
an actionable action to play, with good Lua scripting/event APIs.
|
||||
|
||||
### Phase 8.6 car effects
|
||||
|
||||
- With help of asset pipeline car effects need to be implemented
|
||||
- Doors should be opened before NPC or player enters vehicle
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"edges": [
|
||||
{
|
||||
"lanesAtoB": 2,
|
||||
"lanesBtoA": 2,
|
||||
"lanesPerDirectionOverride": 0,
|
||||
"nodeA": 61,
|
||||
"nodeB": 62,
|
||||
"roadLevelA": 0.0,
|
||||
"roadLevelB": 0.0,
|
||||
"sidewalkLeft": true,
|
||||
"sidewalkLeftMode": 1,
|
||||
"sidewalkRight": true,
|
||||
"sidewalkRightMode": 1
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": 61,
|
||||
"verticalOffset": 0.1762075424194336,
|
||||
"x": 3910.80126953125,
|
||||
"y": 7.148712158203125,
|
||||
"z": 2962.83984375
|
||||
},
|
||||
{
|
||||
"id": 62,
|
||||
"verticalOffset": 0.22691726684570313,
|
||||
"x": 3951.0302734375,
|
||||
"y": 9.944311141967773,
|
||||
"z": 3000.0
|
||||
}
|
||||
],
|
||||
"page": [
|
||||
2,
|
||||
1
|
||||
],
|
||||
"version": "1.0"
|
||||
}
|
||||
@@ -11,6 +11,10 @@
|
||||
"district": 0,
|
||||
"halfSize": 300.0,
|
||||
"name": "Area 1",
|
||||
"preferredTypes": [
|
||||
"house",
|
||||
"village"
|
||||
],
|
||||
"radius": 500.0,
|
||||
"seed": 1
|
||||
}
|
||||
|
||||
+1
File diff suppressed because one or more lines are too long
+1
File diff suppressed because one or more lines are too long
@@ -28,8 +28,24 @@ endforeach()
|
||||
|
||||
# Terrain heightmap: staged at configure time by a configure_file COPYONLY
|
||||
# in CMakeLists.txt (to heightmaps/4242424300000001/), which re-copies it
|
||||
# automatically when the source file changes. Not symlinked: the fixup
|
||||
# layer and save path write next to it.
|
||||
# automatically when the source file changes. Not symlinked: the file
|
||||
# itself is never written at runtime in streaming mode.
|
||||
#
|
||||
# The runtime-WRITTEN per-terrain data directories next to it (roads/,
|
||||
# terrain_fixup/, heightmap.bin_blendmaps/) are SYMLINKED to the source
|
||||
# tree like the demo scenes above: editor/game saves (road regions, lot
|
||||
# params, terrain fixups, blend maps) then write straight into the
|
||||
# versioned source files — no sync-back step, and no staged copy for a
|
||||
# reconfigure to overwrite (this used to cause road-edit data loss).
|
||||
foreach(sub roads terrain_fixup heightmap.bin_blendmaps)
|
||||
set(link "${DEMO_DIR}/heightmaps/4242424300000001/${sub}")
|
||||
if(EXISTS "${link}" OR IS_SYMLINK "${link}")
|
||||
file(REMOVE_RECURSE "${link}")
|
||||
endif()
|
||||
file(CREATE_LINK
|
||||
"${SRC_DIR}/heightmaps/4242424300000001/${sub}" "${link}"
|
||||
SYMBOLIC)
|
||||
endforeach()
|
||||
|
||||
# Character prefab for the spawner's registry entry (registryId 2). It is
|
||||
# written by a previous editor/game run (CharacterRegistry::savePrefab...),
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
; demo-taxi town layout library (phase 5.9)
|
||||
;
|
||||
; Entries extend/replace the built-in layouts (village-house,
|
||||
; town-rowhouse, city-block, city-tower). Syntax:
|
||||
; (layout name [:tags (tag ...)] [:score n] body...)
|
||||
; :tags — matched against the town area's Preferred Types (empty area
|
||||
; list = the district default tag set); only layouts sharing a
|
||||
; tag are candidates for a lot.
|
||||
; :score — weight of the random pick between candidates (default 1).
|
||||
; Rooms carry :type so the interior generator knows where beds and
|
||||
; living-room furniture go (phase 4.7.3).
|
||||
|
||||
; The 3-room C-shape house authored on edge 0 of road page 2_2
|
||||
; (corridor spine with the living room and bedroom branching off the
|
||||
; far side; the open middle rect is the courtyard notch).
|
||||
(layout c-shape-house :tags (house village town) :score 3
|
||||
(let ((w 20) (d 25))
|
||||
(shell w d :floors 1 :empty)
|
||||
(floor 0
|
||||
(room corridor1 (rect 0 0 0.2 1))
|
||||
(room livingroom1 (rect 0.2 0 1 0.35) :type living)
|
||||
(room bedroom1 (rect 0.2 0.65 1 1) :type bedroom)
|
||||
(connect corridor1 livingroom1)
|
||||
(connect corridor1 bedroom1)
|
||||
(door front -8)
|
||||
(windows all))
|
||||
(roof normal 0 0 w d)))
|
||||
|
||||
; Small one-floor cottage: living room + bedroom side by side.
|
||||
(layout cottage :tags (house village) :score 2
|
||||
(let ((w 12) (d 10))
|
||||
(shell w d :floors 1 :empty)
|
||||
(floor 0
|
||||
(room living1 (rect 0 0 0.55 1) :type living)
|
||||
(room bedroom1 (rect 0.55 0 1 1) :type bedroom)
|
||||
(connect living1 bedroom1)
|
||||
(door front center)
|
||||
(windows all))
|
||||
(roof gable 0 0 w d)))
|
||||
|
||||
; Two-floor family house: living+kitchen downstairs, bedrooms up.
|
||||
(layout two-floor-house :tags (house village town) :score 1
|
||||
(let ((w 14) (d 12))
|
||||
(shell w d :floors 2 :empty)
|
||||
(floor 0
|
||||
(room living1 (rect 0 0 0.5 1) :type living)
|
||||
(room kitchen1 (rect 0.5 0 1 1) :type kitchen)
|
||||
(connect living1 kitchen1)
|
||||
(door front center)
|
||||
(windows all))
|
||||
(floor 1
|
||||
(room bedroom1 (rect 0 0 0.5 1) :type bedroom)
|
||||
(room bedroom2 (rect 0.5 0 1 1) :type bedroom)
|
||||
(connect bedroom1 bedroom2)
|
||||
(windows all))
|
||||
(roof gable 0 0 w d)))
|
||||
|
||||
; Single-floor corner shop with a flat roof.
|
||||
(layout corner-shop :tags (commercial shop village town) :score 1
|
||||
(let ((w 16) (d 12))
|
||||
(shell w d :floors 1 :empty)
|
||||
(floor 0
|
||||
(room shop1 (rect 0 0 0.7 1) :type shop)
|
||||
(room storage1 (rect 0.7 0 1 1) :windows none)
|
||||
(connect shop1 storage1)
|
||||
(door front center)
|
||||
(windows front))
|
||||
(roof flat 0 0 w d)))
|
||||
@@ -15,7 +15,20 @@ namespace LayoutLib {
|
||||
void LayoutLibrary::registerLayout(const std::string &name,
|
||||
std::vector<SExpr> body)
|
||||
{
|
||||
m_layouts[name] = std::move(body);
|
||||
Entry &e = m_layouts[name];
|
||||
e.body = std::move(body);
|
||||
e.tags.clear();
|
||||
e.score = 1.0;
|
||||
}
|
||||
|
||||
void LayoutLibrary::registerLayout(const std::string &name,
|
||||
std::vector<SExpr> body,
|
||||
std::vector<std::string> tags, double score)
|
||||
{
|
||||
Entry &e = m_layouts[name];
|
||||
e.body = std::move(body);
|
||||
e.tags = std::move(tags);
|
||||
e.score = score;
|
||||
}
|
||||
|
||||
bool LayoutLibrary::loadSource(const std::string &source, std::string *error)
|
||||
@@ -28,11 +41,69 @@ bool LayoutLibrary::loadSource(const std::string &source, std::string *error)
|
||||
!f.list[0].isSymbol("layout") || !f.list[1].isSymbol()) {
|
||||
if (error)
|
||||
*error = "line " + std::to_string(f.line) +
|
||||
": expected (layout name body...)";
|
||||
": expected (layout name [:tags "
|
||||
"(tag ...)] [:score n] body...)";
|
||||
return false;
|
||||
}
|
||||
std::vector<SExpr> body(f.list.begin() + 2, f.list.end());
|
||||
registerLayout(f.list[1].text, std::move(body));
|
||||
/* Optional metadata clauses between the name and the
|
||||
* body: :tags (tag ...) and :score n (phase 5.9). */
|
||||
std::vector<std::string> tags;
|
||||
double score = 1.0;
|
||||
size_t bodyStart = 2;
|
||||
while (bodyStart < f.list.size() &&
|
||||
f.list[bodyStart].isSymbol()) {
|
||||
const SExpr &kw = f.list[bodyStart];
|
||||
if (kw.isSymbol(":tags")) {
|
||||
if (bodyStart + 1 >= f.list.size() ||
|
||||
!f.list[bodyStart + 1].isList()) {
|
||||
if (error)
|
||||
*error = "line " +
|
||||
std::to_string(kw.line) +
|
||||
": :tags expects a "
|
||||
"(tag ...) list";
|
||||
return false;
|
||||
}
|
||||
for (const SExpr &t :
|
||||
f.list[bodyStart + 1].list) {
|
||||
if (!t.isSymbol()) {
|
||||
if (error)
|
||||
*error = "line " +
|
||||
std::to_string(
|
||||
t.line) +
|
||||
": :tags entries "
|
||||
"must be symbols";
|
||||
return false;
|
||||
}
|
||||
tags.push_back(t.text);
|
||||
}
|
||||
bodyStart += 2;
|
||||
} else if (kw.isSymbol(":score")) {
|
||||
if (bodyStart + 1 >= f.list.size() ||
|
||||
!f.list[bodyStart + 1].isNumber()) {
|
||||
if (error)
|
||||
*error = "line " +
|
||||
std::to_string(kw.line) +
|
||||
": :score expects a "
|
||||
"number";
|
||||
return false;
|
||||
}
|
||||
score = f.list[bodyStart + 1].number;
|
||||
bodyStart += 2;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (bodyStart >= f.list.size()) {
|
||||
if (error)
|
||||
*error = "line " + std::to_string(f.line) +
|
||||
": layout '" + f.list[1].text +
|
||||
"' has no body";
|
||||
return false;
|
||||
}
|
||||
std::vector<SExpr> body(f.list.begin() + bodyStart,
|
||||
f.list.end());
|
||||
registerLayout(f.list[1].text, std::move(body),
|
||||
std::move(tags), score);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -51,11 +122,42 @@ bool LayoutLibrary::loadFile(const std::string &path, std::string *error)
|
||||
}
|
||||
|
||||
const std::vector<SExpr> *LayoutLibrary::find(const std::string &name) const
|
||||
{
|
||||
auto it = m_layouts.find(name);
|
||||
return it == m_layouts.end() ? nullptr : &it->second.body;
|
||||
}
|
||||
|
||||
const LayoutLibrary::Entry *
|
||||
LayoutLibrary::findEntry(const std::string &name) const
|
||||
{
|
||||
auto it = m_layouts.find(name);
|
||||
return it == m_layouts.end() ? nullptr : &it->second;
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, const LayoutLibrary::Entry *> >
|
||||
LayoutLibrary::matching(const std::vector<std::string> &preferredTags) const
|
||||
{
|
||||
std::vector<std::pair<std::string, const Entry *> > out;
|
||||
if (preferredTags.empty())
|
||||
return out;
|
||||
for (const auto &kv : m_layouts) {
|
||||
bool hit = false;
|
||||
for (const std::string &want : preferredTags) {
|
||||
for (const std::string &have : kv.second.tags) {
|
||||
if (want == have) {
|
||||
hit = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (hit)
|
||||
break;
|
||||
}
|
||||
if (hit)
|
||||
out.push_back(std::make_pair(kv.first, &kv.second));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
LayoutLibrary LayoutLibrary::withBuiltins()
|
||||
{
|
||||
LayoutLibrary lib;
|
||||
@@ -63,7 +165,8 @@ LayoutLibrary LayoutLibrary::withBuiltins()
|
||||
std::vector<SExpr> forms;
|
||||
std::string err;
|
||||
if (parseSExprs(defaultTemplateSource(), forms, &err))
|
||||
lib.registerLayout(name, std::move(forms));
|
||||
lib.registerLayout(name, std::move(forms),
|
||||
builtinTags(name), 1.0);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
@@ -74,6 +177,22 @@ std::vector<std::string> LayoutLibrary::builtinNames()
|
||||
"city-tower" };
|
||||
}
|
||||
|
||||
std::vector<std::string> LayoutLibrary::builtinTags(const std::string &name)
|
||||
{
|
||||
/* Keep in sync with the district default tag sets in
|
||||
* TownGen.cpp (phase 5.9): each district's set matches exactly
|
||||
* its own built-in in a stock library. */
|
||||
if (name == "village-house")
|
||||
return { "house", "village" };
|
||||
if (name == "town-rowhouse")
|
||||
return { "house", "town" };
|
||||
if (name == "city-block")
|
||||
return { "apartments", "residential" };
|
||||
if (name == "city-tower")
|
||||
return { "tower", "center", "commercial" };
|
||||
return {};
|
||||
}
|
||||
|
||||
const char *LayoutLibrary::defaultTemplateSource()
|
||||
{
|
||||
/* District defaults share one parametric body for now: the
|
||||
|
||||
@@ -88,15 +88,37 @@ struct LayoutBindings {
|
||||
class LayoutLibrary
|
||||
{
|
||||
public:
|
||||
/** One named layout: the body forms plus selection metadata
|
||||
* (phase 5.9). Tags classify the building type (house, shop,
|
||||
* industrial, ...); score is the weight in the weighted
|
||||
* Monte-Carlo template selection (default 1 — low-scored
|
||||
* templates still appear, just less often). */
|
||||
struct Entry {
|
||||
std::vector<SExpr> body;
|
||||
std::vector<std::string> tags;
|
||||
double score = 1.0;
|
||||
};
|
||||
|
||||
/** Register (or replace) a named layout body. */
|
||||
void registerLayout(const std::string &name,
|
||||
std::vector<SExpr> body);
|
||||
/** Load "(layout name body...)" forms from source text. */
|
||||
/** Register (or replace) a named layout with metadata. */
|
||||
void registerLayout(const std::string &name, std::vector<SExpr> body,
|
||||
std::vector<std::string> tags, double score);
|
||||
/** Load "(layout name [:tags (tag ...)] [:score n] body...)" forms
|
||||
* from source text. */
|
||||
bool loadSource(const std::string &source, std::string *error);
|
||||
/** Load a layout file (same form as loadSource). Missing file
|
||||
* is an error only when mustExist is set. */
|
||||
bool loadFile(const std::string &path, std::string *error);
|
||||
const std::vector<SExpr> *find(const std::string &name) const;
|
||||
const Entry *findEntry(const std::string &name) const;
|
||||
|
||||
/** Entries whose tags intersect `preferredTags`, in deterministic
|
||||
* (name) order; an empty preferred set matches nothing. Used by
|
||||
* TownGen's weighted template selection (phase 5.9). */
|
||||
std::vector<std::pair<std::string, const Entry *> >
|
||||
matching(const std::vector<std::string> &preferredTags) const;
|
||||
|
||||
/** The built-in district default layouts (village-house,
|
||||
* town-rowhouse, city-block, city-tower — all currently sharing
|
||||
@@ -106,6 +128,9 @@ public:
|
||||
|
||||
/** Names of the built-in district default layouts. */
|
||||
static std::vector<std::string> builtinNames();
|
||||
/** Type tags of a built-in district default layout (empty for
|
||||
* unknown names). */
|
||||
static std::vector<std::string> builtinTags(const std::string &name);
|
||||
/** Source of the shared parametric body behind the built-ins —
|
||||
* the canonical starting template for a custom layout (used by
|
||||
* the editor's Insert Template button). */
|
||||
@@ -122,7 +147,7 @@ public:
|
||||
static const std::vector<LayoutExample> &examples();
|
||||
|
||||
private:
|
||||
std::map<std::string, std::vector<SExpr> > m_layouts;
|
||||
std::map<std::string, Entry> m_layouts;
|
||||
};
|
||||
|
||||
class LayoutEval
|
||||
|
||||
@@ -1479,10 +1479,17 @@ public:
|
||||
}
|
||||
currentVertexOffset = vertices.size();
|
||||
}
|
||||
/* Skip degenerate (zero-area) triangles: they carry no
|
||||
* collision surface, but a near-zero GJK separation vector
|
||||
* against them trips the EPAPenetrationDepth assertion
|
||||
* (!ioV.IsNearZero()) and crashes the simulation. */
|
||||
/* Skip degenerate triangles: they carry no collision
|
||||
* surface, but a near-zero GJK separation vector against
|
||||
* them trips the EPAPenetrationDepth assertion
|
||||
* (!ioV.IsNearZero()) and crashes the simulation. Two
|
||||
* guards: an absolute one for exactly-zero-area triangles
|
||||
* and a relative one (cross^2 < 4e-12 * maxEdge^4, i.e.
|
||||
* area/maxEdge^2 < 1e-6) for needle slivers that are
|
||||
* non-zero in double math but degenerate in the float
|
||||
* precision Jolt collides in. Legit thin features pass:
|
||||
* a 2 m x 2 mm window-frame triangle is ~5 orders of
|
||||
* magnitude above the threshold. */
|
||||
triangles.reserve(indices.size() / 3);
|
||||
int degenerate = 0;
|
||||
for (j = 0; j < indices.size() / 3; j++) {
|
||||
@@ -1496,7 +1503,13 @@ public:
|
||||
double cx = aby * acz - abz * acy;
|
||||
double cy = abz * acx - abx * acz;
|
||||
double cz = abx * acy - aby * acx;
|
||||
if (cx * cx + cy * cy + cz * cz < 1e-16) {
|
||||
double cross2 = cx * cx + cy * cy + cz * cz;
|
||||
double len2ab = abx * abx + aby * aby + abz * abz;
|
||||
double len2ac = acx * acx + acy * acy + acz * acz;
|
||||
double maxLen2 =
|
||||
len2ab > len2ac ? len2ab : len2ac;
|
||||
if (cross2 < 1e-16 ||
|
||||
cross2 < 4e-12 * maxLen2 * maxLen2) {
|
||||
degenerate++;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1049,6 +1049,8 @@ void townAreasToJson(const RoadGraph &graph, nlohmann::json &out)
|
||||
for (const RoadTownArea &a : graph.townAreas) {
|
||||
nlohmann::json ja;
|
||||
ja["name"] = a.name;
|
||||
if (!a.town.empty())
|
||||
ja["town"] = a.town;
|
||||
ja["circular"] = a.circular;
|
||||
ja["center"] = { a.center.x, a.center.y, a.center.z };
|
||||
ja["halfSize"] = a.halfSize;
|
||||
@@ -1056,6 +1058,9 @@ void townAreasToJson(const RoadGraph &graph, nlohmann::json &out)
|
||||
ja["district"] = (int)a.district;
|
||||
ja["densityScale"] = a.densityScale;
|
||||
ja["seed"] = a.seed;
|
||||
/* Phase 5.9: preferred building-type tags (sparse). */
|
||||
if (!a.preferredTypes.empty())
|
||||
ja["preferredTypes"] = a.preferredTypes;
|
||||
/* Phase 4.7.2: population rules, sparse (an all-inherit
|
||||
* ruleset is omitted). */
|
||||
if (!a.population.isDefault()) {
|
||||
@@ -1090,17 +1095,40 @@ void townAreasToJson(const RoadGraph &graph, nlohmann::json &out)
|
||||
arr.push_back(ja);
|
||||
}
|
||||
out["townAreas"] = arr;
|
||||
|
||||
/* Phase 5.9: named towns grouping the areas (sparse). */
|
||||
if (!graph.towns.empty()) {
|
||||
nlohmann::json tarr = nlohmann::json::array();
|
||||
for (const RoadTown &t : graph.towns) {
|
||||
nlohmann::json jt;
|
||||
jt["name"] = t.name;
|
||||
jt["seed"] = t.seed;
|
||||
tarr.push_back(jt);
|
||||
}
|
||||
out["towns"] = tarr;
|
||||
}
|
||||
}
|
||||
|
||||
void townAreasFromJson(RoadGraph &graph, const nlohmann::json &j)
|
||||
{
|
||||
graph.townAreas.clear();
|
||||
graph.towns.clear();
|
||||
/* Phase 5.9: named towns (absent = no grouping). */
|
||||
for (const auto &jt : j.value("towns", nlohmann::json::array())) {
|
||||
RoadTown t;
|
||||
t.name = jt.value("name", std::string());
|
||||
if (t.name.empty())
|
||||
continue;
|
||||
t.seed = jt.value("seed", 1u);
|
||||
graph.towns.push_back(t);
|
||||
}
|
||||
for (const auto &ja :
|
||||
j.value("townAreas", nlohmann::json::array())) {
|
||||
RoadTownArea a;
|
||||
a.name = ja.value("name", std::string());
|
||||
if (a.name.empty())
|
||||
continue;
|
||||
a.town = ja.value("town", std::string());
|
||||
a.circular = ja.value("circular", false);
|
||||
if (ja.contains("center") && ja["center"].is_array() &&
|
||||
ja["center"].size() == 3) {
|
||||
@@ -1116,6 +1144,14 @@ void townAreasFromJson(RoadGraph &graph, const nlohmann::json &j)
|
||||
a.district = (RoadTownDistrict)d;
|
||||
a.densityScale = ja.value("densityScale", 1.0f);
|
||||
a.seed = ja.value("seed", 1u);
|
||||
if (ja.contains("preferredTypes") &&
|
||||
ja["preferredTypes"].is_array()) {
|
||||
for (const auto &jt : ja["preferredTypes"]) {
|
||||
if (jt.is_string())
|
||||
a.preferredTypes.push_back(
|
||||
jt.get<std::string>());
|
||||
}
|
||||
}
|
||||
/* Phase 4.7.2: population rules (absent = all-inherit
|
||||
* defaults). */
|
||||
if (ja.contains("population")) {
|
||||
@@ -1140,4 +1176,21 @@ void townAreasFromJson(RoadGraph &graph, const nlohmann::json &j)
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t effectiveAreaSeed(const RoadGraph &graph, const RoadTownArea &area)
|
||||
{
|
||||
if (area.town.empty())
|
||||
return area.seed;
|
||||
for (const RoadTown &t : graph.towns) {
|
||||
if (t.name == area.town) {
|
||||
/* Both seeds feed the area: the whole town shifts
|
||||
* with its town seed while each area keeps its own
|
||||
* offset within it. */
|
||||
return t.seed ^
|
||||
(area.seed * 0x9e3779b9u + 0x85ebca6bu);
|
||||
}
|
||||
}
|
||||
/* Dangling town reference: treat as standalone. */
|
||||
return area.seed;
|
||||
}
|
||||
|
||||
} /* namespace RoadNetworkGen */
|
||||
|
||||
@@ -253,13 +253,27 @@ void streetsFromJson(RoadGraph &graph, const nlohmann::json &j,
|
||||
* Town-area serialization (phase 4.5 item 4). Same dual-path pattern
|
||||
* as streets: inline in the scene JSON when not streaming,
|
||||
* `roads/towns.json` via RoadRegionStore when streaming. Shape:
|
||||
* `{ "townAreas": [ { "name", "circular", "center": [x,y,z],
|
||||
* "halfSize", "radius", "district", "densityScale", "seed" } ] }`;
|
||||
* fromJson tolerates a missing/empty array.
|
||||
* `{ "townAreas": [ { "name", "town"?, "circular", "center": [x,y,z],
|
||||
* "halfSize", "radius", "district", "densityScale", "seed",
|
||||
* "preferredTypes"?, "population"? } ], "towns": [ { "name", "seed" } ]? }`;
|
||||
* fromJson tolerates missing arrays.
|
||||
*/
|
||||
void townAreasToJson(const RoadGraph &graph, nlohmann::json &out);
|
||||
void townAreasFromJson(RoadGraph &graph, const nlohmann::json &j);
|
||||
|
||||
/**
|
||||
* The seed an area generates with (phase 5.9): the area's own seed
|
||||
* for a standalone area, or town.seed mixed with the area seed when
|
||||
* the area belongs to a town (so the whole settlement shifts with the
|
||||
* town seed while each area keeps its own offset; stable under
|
||||
* townAreas reordering). A dangling town reference falls back to the
|
||||
* area seed. EVERY consumer of an area's seed must go through this
|
||||
* helper (town generation and all population seed derivations) or
|
||||
* buildings and population diverge.
|
||||
*/
|
||||
uint32_t effectiveAreaSeed(const RoadGraph &graph,
|
||||
const RoadTownArea &area);
|
||||
|
||||
} /* namespace RoadNetworkGen */
|
||||
|
||||
#endif /* EDITSCENE_ROADNETWORKGEN_HPP */
|
||||
|
||||
@@ -185,6 +185,94 @@ static const char *defaultLayoutForDistrict(District d)
|
||||
}
|
||||
}
|
||||
|
||||
/** District -> default preferred-type tag set (phase 5.9). Each set
|
||||
* matches exactly its own district default built-in in a stock
|
||||
* library (LayoutLibrary::builtinTags), so towns without templates or
|
||||
* area preferred types generate bit-identically to the pre-5.9
|
||||
* generator. */
|
||||
static const std::vector<std::string> &defaultTagsForDistrict(District d)
|
||||
{
|
||||
static const std::vector<std::string> village = { "village" };
|
||||
static const std::vector<std::string> town = { "town" };
|
||||
static const std::vector<std::string> residential = {
|
||||
"residential"
|
||||
};
|
||||
static const std::vector<std::string> center = { "center" };
|
||||
static const std::vector<std::string> fallback = { "village" };
|
||||
switch (d) {
|
||||
case District::Village:
|
||||
return village;
|
||||
case District::Town:
|
||||
return town;
|
||||
case District::CityResidential:
|
||||
return residential;
|
||||
case District::CityCenter:
|
||||
return center;
|
||||
default:
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 5.9: pick the layout for a lot without an explicit edge-side
|
||||
* layout — weighted Monte-Carlo among the library entries whose tags
|
||||
* intersect the preferred set (area preferredTypes, else the district
|
||||
* default set), weight = layout score. Falls back to the district
|
||||
* default when nothing matches. When the candidate list is exactly
|
||||
* the district default (stock library) no RNG is drawn, keeping
|
||||
* stock towns bit-identical to the pre-5.9 generator. *usedName
|
||||
* gets the chosen library name.
|
||||
*/
|
||||
static std::string selectLayout(const TownParams ¶ms, District district,
|
||||
const LayoutLib::LayoutLibrary *layouts,
|
||||
std::mt19937 &rng,
|
||||
std::uniform_real_distribution<float> &unit,
|
||||
std::string *usedName)
|
||||
{
|
||||
const char *defLayout = defaultLayoutForDistrict(district);
|
||||
if (layouts) {
|
||||
const std::vector<std::string> &pref =
|
||||
!params.preferredTypes.empty() ?
|
||||
params.preferredTypes :
|
||||
defaultTagsForDistrict(district);
|
||||
std::vector<std::pair<std::string,
|
||||
const LayoutLib::LayoutLibrary::Entry *> >
|
||||
cands = layouts->matching(pref);
|
||||
if (cands.empty()) {
|
||||
*usedName = defLayout;
|
||||
return defLayout;
|
||||
}
|
||||
if (cands.size() == 1 && cands[0].first == defLayout) {
|
||||
*usedName = defLayout;
|
||||
return defLayout;
|
||||
}
|
||||
double total = 0.0;
|
||||
for (const auto &c : cands)
|
||||
total += std::max(0.0, c.second->score);
|
||||
std::string picked;
|
||||
if (total <= 0.0) {
|
||||
/* All scores zero: uniform pick. */
|
||||
picked = cands[(size_t)(unit(rng) * cands.size()) %
|
||||
cands.size()]
|
||||
.first;
|
||||
} else {
|
||||
double roll = unit(rng) * total;
|
||||
picked = cands.back().first;
|
||||
for (const auto &c : cands) {
|
||||
roll -= std::max(0.0, c.second->score);
|
||||
if (roll < 0.0) {
|
||||
picked = c.first;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
*usedName = picked;
|
||||
return picked;
|
||||
}
|
||||
*usedName = defLayout;
|
||||
return defLayout;
|
||||
}
|
||||
|
||||
TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
const RoadNetworkGen::HeightSampler &sampleHeight,
|
||||
const LayoutLib::LayoutLibrary *layouts,
|
||||
@@ -573,8 +661,27 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
}
|
||||
const char *defLayout =
|
||||
defaultLayoutForDistrict(params.district);
|
||||
const std::string layoutSrc =
|
||||
layout.empty() ? std::string(defLayout) : layout;
|
||||
/* Phase 5.9: an explicit edge-side layout wins
|
||||
* outright; otherwise the weighted template
|
||||
* selection picks among tag-matching library
|
||||
* entries. */
|
||||
std::string usedLayoutName;
|
||||
std::string layoutSrc;
|
||||
if (!layout.empty()) {
|
||||
layoutSrc = layout;
|
||||
usedLayoutName =
|
||||
layout.find_first_not_of(" \t\n\r") ==
|
||||
0 &&
|
||||
layout[layout.find_first_not_of(
|
||||
" \t\n\r")] == '(' ?
|
||||
"<inline>" :
|
||||
layout;
|
||||
} else {
|
||||
layoutSrc = selectLayout(params,
|
||||
params.district,
|
||||
layouts, rng, unit,
|
||||
&usedLayoutName);
|
||||
}
|
||||
LayoutLib::BuildingPlan plan;
|
||||
std::string layoutErr;
|
||||
{
|
||||
@@ -603,6 +710,7 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
result.skippedLayoutError++;
|
||||
continue;
|
||||
}
|
||||
usedLayoutName = defLayout;
|
||||
}
|
||||
for (const std::string &w : layoutWarns)
|
||||
result.warnings.push_back(
|
||||
@@ -664,6 +772,7 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
b.floors = floors;
|
||||
b.roofType = roofType;
|
||||
b.layout = layout;
|
||||
b.layoutName = usedLayoutName;
|
||||
b.floorY = floorY;
|
||||
b.minGroundY = minGroundY;
|
||||
b.plan = std::move(plan);
|
||||
|
||||
@@ -62,6 +62,13 @@ struct TownParams {
|
||||
float densityScale = 1.0f;
|
||||
/** Lots whose floor level is below this are skipped (water). */
|
||||
float waterLevel = -1.0e30f;
|
||||
/** Preferred building-type tags for template selection (phase
|
||||
* 5.9): library layouts whose tags intersect this set compete in
|
||||
* the weighted Monte-Carlo pick (weight = layout score). Empty =
|
||||
* the district's default tag set, which matches exactly the
|
||||
* district default built-in in a stock library (bit-identical
|
||||
* towns until templates join the set). */
|
||||
std::vector<std::string> preferredTypes;
|
||||
};
|
||||
|
||||
struct TownBuilding {
|
||||
@@ -86,6 +93,12 @@ struct TownBuilding {
|
||||
/** Layout DSL string from the lot params (phase 4.5 item 2);
|
||||
* empty = default procedural layout. */
|
||||
std::string layout;
|
||||
/** Name of the library layout actually used (phase 5.9): the
|
||||
* template picked by the weighted selection, the district default,
|
||||
* or the explicit edge-side library name; "<inline>" when the
|
||||
* edge-side layout was inline source. Debug/test visibility only
|
||||
* — the plan below is the build truth. */
|
||||
std::string layoutName;
|
||||
/**
|
||||
* Evaluated building plan (phase 4.5 item 2): grid metrics,
|
||||
* per-cell flags, roofs, lot features, placements. Always
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "../components/GeneratedPhysicsTag.hpp"
|
||||
#include "../components/Door.hpp"
|
||||
#include "../ui/DoorPickState.hpp"
|
||||
#include "../ui/TerrainEditor.hpp"
|
||||
#include "GlobalStateStore.hpp"
|
||||
#include "EditorUISystem.hpp"
|
||||
#include "../EditorApp.hpp"
|
||||
@@ -503,6 +504,10 @@ void EditorUISystem::update(float deltaTime)
|
||||
renderInventoryConfigWindow();
|
||||
}
|
||||
|
||||
// Layout DSL help + layout library editor windows (open state
|
||||
// lives in TerrainEditor; they early-out when closed)
|
||||
TerrainEditor::renderLayoutWindows();
|
||||
|
||||
// Render FPS overlay
|
||||
renderFPSOverlay(deltaTime);
|
||||
}
|
||||
@@ -598,6 +603,9 @@ void EditorUISystem::renderHierarchyWindow()
|
||||
if (ImGui::MenuItem("World Map")) {
|
||||
m_showWorldMap = true;
|
||||
}
|
||||
if (ImGui::MenuItem("Layout Library Editor")) {
|
||||
TerrainEditor::openLayoutLibraryEditor();
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(
|
||||
"Action Database (Singleton)")) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include "../GameMode.hpp"
|
||||
#include "../components/CharacterSlots.hpp"
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
|
||||
/* Deterministic per-NPC look from the character-part catalog: a
|
||||
* hairstyle (layer 1) plus, for clothed NPCs, layer-2 garments per
|
||||
@@ -146,7 +147,12 @@ int NpcPopulationSystem::generateAreaPopulation(int areaIndex,
|
||||
rules.maxOutfitLevel = rp.maxOutfitLevel;
|
||||
if (rules.maxOutfitLevel < rules.minOutfitLevel)
|
||||
rules.maxOutfitLevel = rules.minOutfitLevel;
|
||||
uint32_t popSeed = area.seed ^ kPopSeedXor;
|
||||
/* Phase 5.9: all population seeds derive from the area's
|
||||
* EFFECTIVE seed (town-mixed when grouped) so population,
|
||||
* locations and schedules always match the buildings. */
|
||||
const uint32_t effSeed =
|
||||
RoadNetworkGen::effectiveAreaSeed(*graph, area);
|
||||
uint32_t popSeed = effSeed ^ kPopSeedXor;
|
||||
uint64_t idBase = TownPopulation::NPC_ID_BASE +
|
||||
(uint64_t)areaIndex * TownPopulation::AREA_POP_CAP;
|
||||
if (town.buildings.size() * TownPopulation::HOUSEHOLD_ID_CAP >=
|
||||
@@ -165,7 +171,7 @@ int NpcPopulationSystem::generateAreaPopulation(int areaIndex,
|
||||
* building, curb per pickup POI, parking per parking POI,
|
||||
* interest subset). */
|
||||
TownLocations::LocationSet locations =
|
||||
TownLocations::buildLocations(town, area.seed ^ kLocSeedXor);
|
||||
TownLocations::buildLocations(town, effSeed ^ kLocSeedXor);
|
||||
|
||||
CharacterRegistry ® = CharacterRegistry::getSingleton();
|
||||
reg.setAutoSaveSuspended(true);
|
||||
@@ -200,6 +206,10 @@ int NpcPopulationSystem::generateAreaPopulation(int areaIndex,
|
||||
rec->stringColumns["npcKey"] = pm.stableKey;
|
||||
rec->stringColumns["npcHome"] = hh.address;
|
||||
rec->stringColumns["npcArea"] = area.name;
|
||||
/* Phase 5.9: owning town for grouped areas (F7
|
||||
* overlay can group by town). */
|
||||
rec->stringColumns["npcTown"] =
|
||||
!area.town.empty() ? area.town : area.name;
|
||||
/* Phase 4.7.3 location bindings (shared with
|
||||
* the 4.7.4 schedule generator): work picks
|
||||
* from the area's Interest locations (a
|
||||
@@ -251,7 +261,7 @@ int NpcPopulationSystem::generateAreaPopulation(int areaIndex,
|
||||
* locations (same town state -> same schedules; a script
|
||||
* forcing a regeneration stays deterministic). */
|
||||
ap.schedules = TownSchedule::generateSchedules(
|
||||
pop, ap.locations, area.seed ^ kScheduleSeedXor,
|
||||
pop, ap.locations, effSeed ^ kScheduleSeedXor,
|
||||
TownSchedule::ScheduleRules());
|
||||
m_areas[areaIndex] = ap;
|
||||
reg.setAutoSaveSuspended(false);
|
||||
|
||||
@@ -246,6 +246,124 @@ LayoutLib::LayoutLibrary TownGenSystem::loadLayoutLibrary()
|
||||
return lib;
|
||||
}
|
||||
|
||||
/* Name of the layout-preview container (phase 5.9.6). Deliberately
|
||||
* without the "TownGen " prefix so clear()/countTowns()/regenerate*
|
||||
* skip it; createTownContainer still tags it GeneratedTownTag, so the
|
||||
* preview never lands in the scene JSON. */
|
||||
static const char *LAYOUT_PREVIEW_NAME = "Layout Preview";
|
||||
|
||||
bool TownGenSystem::previewLayout(const std::string &layoutSource,
|
||||
const LayoutLib::LayoutLibrary *library,
|
||||
int district, int seed,
|
||||
const Ogre::Vector3 ¢er,
|
||||
const std::string &genMode, std::string *error)
|
||||
{
|
||||
const TownGen::DistrictParams &dp =
|
||||
TownGen::districtParams((TownGen::District)district);
|
||||
|
||||
/* Representative bindings: a fixed 40x40 m lot with a 35x35 m
|
||||
* setback rect, the chosen district's footprint/floor ranges and a
|
||||
* passing density roll. The RNG is a scratch stream seeded by the
|
||||
* editor's seed field — previews are reproducible per seed. */
|
||||
LayoutLib::LayoutBindings lb;
|
||||
lb.lotW = 40.0f;
|
||||
lb.lotD = 40.0f;
|
||||
lb.availW = 35.0f;
|
||||
lb.availD = 35.0f;
|
||||
lb.minFp = dp.minFootprint;
|
||||
lb.maxFp = dp.maxFootprint;
|
||||
lb.minFloors = dp.minFloors;
|
||||
lb.maxFloors = dp.maxFloors;
|
||||
lb.density = 1.0f;
|
||||
lb.densityRoll = 0.0f;
|
||||
lb.densityPass = true;
|
||||
|
||||
std::mt19937 rng((uint32_t)seed);
|
||||
LayoutLib::BuildingPlan plan;
|
||||
std::vector<std::string> warns;
|
||||
LayoutLib::LayoutEval eval(lb, &rng, library);
|
||||
if (!eval.evaluate(layoutSource, plan, error))
|
||||
return false;
|
||||
|
||||
m_lastWarnings.clear();
|
||||
for (const std::string &w : warns) {
|
||||
m_lastWarnings.push_back("preview: " + w);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TownGenSystem: preview: " + w);
|
||||
}
|
||||
|
||||
/* Reuse the existing preview container when present (replace its
|
||||
* previous building); otherwise create it with an empty result
|
||||
* (container + shared settlement material/texture only). */
|
||||
flecs::entity container;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name == LAYOUT_PREVIEW_NAME)
|
||||
container = e;
|
||||
});
|
||||
if (container.is_alive()) {
|
||||
std::vector<flecs::entity> old;
|
||||
container.children([&](flecs::entity child) {
|
||||
if (child.has<CellGridComponent>())
|
||||
old.push_back(child);
|
||||
});
|
||||
for (flecs::entity e : old)
|
||||
destroyEntityDeep(e);
|
||||
} else {
|
||||
TownGen::TownResult empty;
|
||||
container = createTownContainer(LAYOUT_PREVIEW_NAME, empty,
|
||||
dp, (uint32_t)seed);
|
||||
}
|
||||
|
||||
flecs::entity material;
|
||||
if (container.has<TownComponent>())
|
||||
material = container.get<TownComponent>()
|
||||
.proceduralMaterialEntity;
|
||||
flecs::entity texture;
|
||||
if (material.is_alive() &&
|
||||
material.has<ProceduralMaterialComponent>())
|
||||
texture = material.get<ProceduralMaterialComponent>()
|
||||
.diffuseTextureEntity;
|
||||
if (!material.is_alive() || !texture.is_alive()) {
|
||||
if (error)
|
||||
*error = "preview container lost its settlement "
|
||||
"material";
|
||||
return false;
|
||||
}
|
||||
|
||||
TownGen::TownBuilding b;
|
||||
b.address = "Layout Preview Building";
|
||||
b.layoutName = layoutSource;
|
||||
b.center = center;
|
||||
b.yaw = 0.0f;
|
||||
b.plan = plan;
|
||||
b.width = plan.cellsX * plan.cellSize;
|
||||
b.depth = plan.cellsZ * plan.cellSize;
|
||||
b.floors = plan.floors;
|
||||
b.roofType = -1;
|
||||
b.floorY = center.y;
|
||||
b.minGroundY = center.y;
|
||||
b.stairs = false;
|
||||
|
||||
std::vector<const TownGen::TownPoi *> noPois;
|
||||
createBuilding(container, material, texture, b, noPois, genMode);
|
||||
return true;
|
||||
}
|
||||
|
||||
void TownGenSystem::clearLayoutPreview()
|
||||
{
|
||||
flecs::entity container;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name == LAYOUT_PREVIEW_NAME)
|
||||
container = e;
|
||||
});
|
||||
if (container.is_alive())
|
||||
destroyEntityDeep(container);
|
||||
}
|
||||
|
||||
/* Phase 4.5 item 4: RoadTownDistrict mirrors TownGen::District — the
|
||||
* area's district is cast directly onto TownParams. */
|
||||
static_assert((int)RoadTownDistrict::Village ==
|
||||
@@ -379,6 +497,18 @@ int TownGenSystem::generate(const TownGen::TownParams ¶ms,
|
||||
}
|
||||
|
||||
int TownGenSystem::generateAreas(std::string *error)
|
||||
{
|
||||
return generateAreasImpl(nullptr, error);
|
||||
}
|
||||
|
||||
int TownGenSystem::generateAreasForTown(const std::string &townName,
|
||||
std::string *error)
|
||||
{
|
||||
return generateAreasImpl(&townName, error);
|
||||
}
|
||||
|
||||
int TownGenSystem::generateAreasImpl(const std::string *townName,
|
||||
std::string *error)
|
||||
{
|
||||
TerrainSystem *ts = m_terrainSystem ?
|
||||
m_terrainSystem :
|
||||
@@ -389,9 +519,19 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
if (!setupTownRun(ts, &graph, &waterY, &sampler, m_world, error))
|
||||
return 0;
|
||||
|
||||
if (graph->townAreas.empty()) {
|
||||
/* Areas this run builds: all of them, or just the ones owned by
|
||||
* the town (phase 5.9). */
|
||||
std::vector<size_t> indices;
|
||||
for (size_t ai = 0; ai < graph->townAreas.size(); ++ai) {
|
||||
if (townName && graph->townAreas[ai].town != *townName)
|
||||
continue;
|
||||
indices.push_back(ai);
|
||||
}
|
||||
if (indices.empty()) {
|
||||
if (error)
|
||||
*error = "no town areas defined";
|
||||
*error = townName ?
|
||||
"no areas in town '" + *townName + "'" :
|
||||
"no town areas defined";
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -405,19 +545,50 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
scopePtr = &scope;
|
||||
}
|
||||
|
||||
/* Replace any previously generated town(s). */
|
||||
clear();
|
||||
/* Replace previously generated containers: everything on a full
|
||||
* run, just this town's area containers otherwise. */
|
||||
if (!townName) {
|
||||
clear();
|
||||
} else {
|
||||
std::vector<flecs::entity> dead;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&](flecs::entity e, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name.rfind("TownGen ", 0) != 0)
|
||||
return;
|
||||
const std::string areaName =
|
||||
name.name.substr(8);
|
||||
for (size_t ai : indices)
|
||||
if (graph->townAreas[ai].name ==
|
||||
areaName) {
|
||||
dead.push_back(e);
|
||||
return;
|
||||
}
|
||||
});
|
||||
for (flecs::entity e : dead) {
|
||||
auto it = m_containerParams.find(
|
||||
e.get<EntityNameComponent>().name);
|
||||
if (it != m_containerParams.end())
|
||||
m_containerParams.erase(it);
|
||||
auto ait = m_containerArea.find(
|
||||
e.get<EntityNameComponent>().name);
|
||||
if (ait != m_containerArea.end())
|
||||
m_containerArea.erase(ait);
|
||||
destroyEntityDeep(e);
|
||||
}
|
||||
}
|
||||
m_lastWarnings.clear();
|
||||
|
||||
int total = 0;
|
||||
for (size_t ai = 0; ai < graph->townAreas.size(); ++ai) {
|
||||
for (size_t ai : indices) {
|
||||
const RoadTownArea &area = graph->townAreas[ai];
|
||||
|
||||
TownGen::TownParams p;
|
||||
p.district = (TownGen::District)(int)area.district;
|
||||
p.seed = area.seed;
|
||||
p.seed = RoadNetworkGen::effectiveAreaSeed(*graph, area);
|
||||
p.densityScale = area.densityScale;
|
||||
p.waterLevel = waterY;
|
||||
p.preferredTypes = area.preferredTypes;
|
||||
|
||||
TownGen::TownResult result =
|
||||
TownGen::generateTown(*graph, p, sampler, &layouts,
|
||||
@@ -442,7 +613,10 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
|
||||
if (total == 0) {
|
||||
if (error)
|
||||
*error = "no suitable lots in any town area";
|
||||
*error = townName ?
|
||||
"no suitable lots in town '" + *townName +
|
||||
"'" :
|
||||
"no suitable lots in any town area";
|
||||
return 0;
|
||||
}
|
||||
return total;
|
||||
@@ -470,9 +644,10 @@ bool TownGenSystem::runAreaGeneration(int areaIndex,
|
||||
const RoadTownArea &area = graph->townAreas[areaIndex];
|
||||
TownGen::TownParams p;
|
||||
p.district = (TownGen::District)(int)area.district;
|
||||
p.seed = area.seed;
|
||||
p.seed = RoadNetworkGen::effectiveAreaSeed(*graph, area);
|
||||
p.densityScale = area.densityScale;
|
||||
p.waterLevel = waterY;
|
||||
p.preferredTypes = area.preferredTypes;
|
||||
|
||||
LayoutLib::LayoutLibrary layouts = loadLayoutLibrary();
|
||||
|
||||
@@ -562,8 +737,10 @@ int TownGenSystem::regenerateMatching(
|
||||
graph->townAreas[ait->second];
|
||||
p.district =
|
||||
(TownGen::District)(int)area.district;
|
||||
p.seed = area.seed;
|
||||
p.seed = RoadNetworkGen::effectiveAreaSeed(
|
||||
*graph, area);
|
||||
p.densityScale = area.densityScale;
|
||||
p.preferredTypes = area.preferredTypes;
|
||||
areas = &graph->townAreas;
|
||||
areaIndex = ait->second;
|
||||
} else if (pit != m_containerParams.end()) {
|
||||
@@ -807,9 +984,10 @@ void TownGenSystem::buildStreamedPageArea(const RoadGraph &graph,
|
||||
|
||||
TownGen::TownParams p;
|
||||
p.district = (TownGen::District)(int)area.district;
|
||||
p.seed = area.seed;
|
||||
p.seed = RoadNetworkGen::effectiveAreaSeed(graph, area);
|
||||
p.densityScale = area.densityScale;
|
||||
p.waterLevel = waterY;
|
||||
p.preferredTypes = area.preferredTypes;
|
||||
|
||||
TownGen::PageScope scope;
|
||||
scope.mapper = makePageMapper(ts);
|
||||
|
||||
@@ -59,6 +59,14 @@ public:
|
||||
* set — also when the graph defines no town areas). */
|
||||
int generateAreas(std::string *error);
|
||||
|
||||
/** Phase 5.9: like generateAreas(), but builds only the areas
|
||||
* owned by town @p townName (RoadTownArea::town) and replaces only
|
||||
* those areas' previously generated containers — other towns stay
|
||||
* untouched. Returns the number of buildings (0 with @p error set
|
||||
* on failure — also when the town owns no areas). */
|
||||
int generateAreasForTown(const std::string &townName,
|
||||
std::string *error);
|
||||
|
||||
/** Phase 4.7.2: re-run the PURE generation of one town area
|
||||
* (no entities created) and return the TownResult — the
|
||||
* population/location generators consume it. Uses the same
|
||||
@@ -111,6 +119,15 @@ public:
|
||||
* generation time. Returns the number of rebuilt buildings. */
|
||||
int regenerateByLotParams(int edgeIndex, int side, std::string *error);
|
||||
|
||||
private:
|
||||
/** Shared core of generateAreas()/generateAreasForTown(): when
|
||||
* @p townName is null every area is built and all previously
|
||||
* generated towns are replaced; otherwise only the areas owned by
|
||||
* the town and only their containers. */
|
||||
int generateAreasImpl(const std::string *townName, std::string *error);
|
||||
|
||||
public:
|
||||
|
||||
/** Warnings of the last generate()/generateAreas()/regenerate*
|
||||
* run (layout errors that fell back to the district default,
|
||||
* unreachable rooms, ...), for the editor UI; the same lines also
|
||||
@@ -125,6 +142,27 @@ public:
|
||||
* Public for the editor's live DSL validation. */
|
||||
static LayoutLib::LayoutLibrary loadLayoutLibrary();
|
||||
|
||||
/** Phase 5.9: one-off layout preview building for the layout
|
||||
* library editor. Evaluates @p layoutSource (inline DSL source or
|
||||
* a library entry name resolved through @p library) with
|
||||
* representative bindings of district @p district (40x40 m lot,
|
||||
* 35x35 m setback rect, the district's footprint/floor ranges,
|
||||
* density pass) and a scratch RNG seeded by @p seed, then spawns a
|
||||
* single building under a persistent "Layout Preview" container at
|
||||
* @p center (render space), replacing the previous preview.
|
||||
* @p genMode is the CellGrid generation mode ("exteriorOnly",
|
||||
* "interiorOnly", "full"). Evaluation warnings go to
|
||||
* getLastWarnings() (prefixed "preview:") and the log. Returns
|
||||
* false with @p error set on evaluation/setup failure (the old
|
||||
* preview is kept then). */
|
||||
bool previewLayout(const std::string &layoutSource,
|
||||
const LayoutLib::LayoutLibrary *library,
|
||||
int district, int seed, const Ogre::Vector3 ¢er,
|
||||
const std::string &genMode, std::string *error);
|
||||
|
||||
/** Destroy the layout preview container and building (if any). */
|
||||
void clearLayoutPreview();
|
||||
|
||||
private:
|
||||
/** Shared regen core of regenerateBuilding()/
|
||||
* regenerateByLotParams(): for every "TownGen " container with
|
||||
|
||||
@@ -4441,6 +4441,209 @@ static bool runTownGenProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 5.9: layout metadata — :tags / :score clauses on
|
||||
* library entries, the matching() tag query and malformed
|
||||
* clause errors. */
|
||||
{
|
||||
LayoutLib::LayoutLibrary mlib;
|
||||
std::string merr;
|
||||
if (!mlib.loadSource(
|
||||
"(layout meta-a :tags (house village) "
|
||||
" :score 5 (shell 10 8))"
|
||||
"(layout meta-b :tags (tower center) "
|
||||
" :score 0.5 (shell 12 12))"
|
||||
"(layout meta-c (shell 8 8))",
|
||||
&merr)) {
|
||||
printf(" *** layout metadata: load '%s' ***\n",
|
||||
merr.c_str());
|
||||
ok = false;
|
||||
} else {
|
||||
const LayoutLib::LayoutLibrary::Entry *ea =
|
||||
mlib.findEntry("meta-a");
|
||||
const LayoutLib::LayoutLibrary::Entry *eb =
|
||||
mlib.findEntry("meta-b");
|
||||
const LayoutLib::LayoutLibrary::Entry *ec =
|
||||
mlib.findEntry("meta-c");
|
||||
if (!ea || ea->tags.size() != 2 ||
|
||||
ea->tags[0] != "house" ||
|
||||
ea->tags[1] != "village" ||
|
||||
std::fabs(ea->score - 5.0) > 1e-9 ||
|
||||
!eb || eb->tags.size() != 2 ||
|
||||
std::fabs(eb->score - 0.5) > 1e-9 ||
|
||||
!ec || !ec->tags.empty() ||
|
||||
std::fabs(ec->score - 1.0) > 1e-9) {
|
||||
printf(" *** layout metadata: entry "
|
||||
"fields ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* matching(): intersection with the
|
||||
* preferred set; empty preferred matches
|
||||
* nothing (the caller substitutes the
|
||||
* district default set). */
|
||||
std::vector<std::pair<std::string,
|
||||
const LayoutLib::LayoutLibrary::Entry *> >
|
||||
mVillage = mlib.matching({ "village" }),
|
||||
mNone = mlib.matching({});
|
||||
if (mVillage.size() != 1 ||
|
||||
mVillage[0].first != "meta-a" ||
|
||||
!mNone.empty()) {
|
||||
printf(" *** layout matching: %zu/%zu, "
|
||||
"want 1/0 ***\n",
|
||||
mVillage.size(), mNone.size());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
/* Malformed metadata clauses are hard load
|
||||
* errors. */
|
||||
LayoutLib::LayoutLibrary badlib;
|
||||
if (badlib.loadSource("(layout b1 :tags house (shell 8 8))",
|
||||
&merr) ||
|
||||
badlib.loadSource("(layout b2 :tags (a 1) (shell 8 8))",
|
||||
&merr) ||
|
||||
badlib.loadSource("(layout b3 :score x (shell 8 8))",
|
||||
&merr) ||
|
||||
badlib.loadSource("(layout b4 :tags (a))", &merr)) {
|
||||
printf(" *** layout metadata: bad sources "
|
||||
"accepted ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 5.9: weighted tag-filtered selection in
|
||||
* generateTown + the stock-library bit-compat shortcut. */
|
||||
{
|
||||
RoadGraph graph;
|
||||
buildTown(graph);
|
||||
LayoutLib::LayoutLibrary tlib =
|
||||
LayoutLib::LayoutLibrary::withBuiltins();
|
||||
std::string terr;
|
||||
if (!tlib.loadSource(
|
||||
"(layout heavy-b :tags (center) :score 100 "
|
||||
" (shell 10 10 :floors 2))"
|
||||
"(layout light-c :tags (center) :score 1 "
|
||||
" (shell 8 8 :floors 1))",
|
||||
&terr)) {
|
||||
printf(" *** layout select: load '%s' ***\n",
|
||||
terr.c_str());
|
||||
ok = false;
|
||||
} else {
|
||||
/* CityCenter's default tag set is {center}:
|
||||
* heavy-b (100) vs light-c (1) vs builtin
|
||||
* city-tower (1); the recorded layoutName
|
||||
* shows the deterministic pick. */
|
||||
TownGen::TownResult res =
|
||||
TownGen::generateTown(graph, cc, flat,
|
||||
&tlib);
|
||||
int heavy = 0, light = 0, tower = 0, other = 0;
|
||||
for (const TownGen::TownBuilding &b :
|
||||
res.buildings) {
|
||||
if (b.layoutName == "heavy-b")
|
||||
++heavy;
|
||||
else if (b.layoutName == "light-c")
|
||||
++light;
|
||||
else if (b.layoutName == "city-tower")
|
||||
++tower;
|
||||
else
|
||||
++other;
|
||||
}
|
||||
if (res.buildings.size() != 12 || other != 0 ||
|
||||
heavy < 9) {
|
||||
printf(" *** layout select: heavy %d "
|
||||
"light %d tower %d other %d of "
|
||||
"%zu, want heavy >= 9/12 ***\n",
|
||||
heavy, light, tower, other,
|
||||
res.buildings.size());
|
||||
ok = false;
|
||||
}
|
||||
/* Deterministic under the seed. */
|
||||
TownGen::TownResult res2 =
|
||||
TownGen::generateTown(graph, cc, flat,
|
||||
&tlib);
|
||||
bool samePicks =
|
||||
res2.buildings.size() ==
|
||||
res.buildings.size();
|
||||
if (samePicks)
|
||||
for (size_t i = 0;
|
||||
i < res.buildings.size(); ++i)
|
||||
if (res2.buildings[i].layoutName !=
|
||||
res.buildings[i].layoutName) {
|
||||
samePicks = false;
|
||||
break;
|
||||
}
|
||||
if (!samePicks) {
|
||||
printf(" *** layout select: "
|
||||
"nondeterministic ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
/* Area preferredTypes override the district tag
|
||||
* set; an unmatched set falls back to the district
|
||||
* default. */
|
||||
TownGen::TownParams ccVillage = cc;
|
||||
ccVillage.preferredTypes = { "village" };
|
||||
TownGen::TownResult resV =
|
||||
TownGen::generateTown(graph, ccVillage, flat,
|
||||
&tlib);
|
||||
int villageHouses = 0;
|
||||
for (const TownGen::TownBuilding &b : resV.buildings)
|
||||
if (b.layoutName == "village-house")
|
||||
++villageHouses;
|
||||
if (villageHouses != (int)resV.buildings.size() ||
|
||||
resV.buildings.empty()) {
|
||||
printf(" *** layout select preferred: %d/%zu "
|
||||
"village-house ***\n",
|
||||
villageHouses, resV.buildings.size());
|
||||
ok = false;
|
||||
}
|
||||
TownGen::TownParams ccNone = cc;
|
||||
ccNone.preferredTypes = { "no-such-tag" };
|
||||
TownGen::TownResult resN =
|
||||
TownGen::generateTown(graph, ccNone, flat,
|
||||
&tlib);
|
||||
int towers = 0;
|
||||
for (const TownGen::TownBuilding &b : resN.buildings)
|
||||
if (b.layoutName == "city-tower")
|
||||
++towers;
|
||||
if (towers != (int)resN.buildings.size() ||
|
||||
resN.buildings.empty()) {
|
||||
printf(" *** layout select fallback: %d/%zu "
|
||||
"city-tower ***\n",
|
||||
towers, resN.buildings.size());
|
||||
ok = false;
|
||||
}
|
||||
/* Bit-compat: the stock library (only built-ins)
|
||||
* matches exactly the district default per tag set,
|
||||
* so no RNG is drawn and the town is identical to
|
||||
* the no-library run. */
|
||||
LayoutLib::LayoutLibrary stock =
|
||||
LayoutLib::LayoutLibrary::withBuiltins();
|
||||
TownGen::TownResult resStock =
|
||||
TownGen::generateTown(graph, cc, flat, &stock);
|
||||
TownGen::TownResult resLegacy =
|
||||
TownGen::generateTown(graph, cc, flat);
|
||||
bool sameTown = resStock.buildings.size() ==
|
||||
resLegacy.buildings.size();
|
||||
if (sameTown)
|
||||
for (size_t i = 0;
|
||||
i < resStock.buildings.size(); ++i)
|
||||
if (resStock.buildings[i].center !=
|
||||
resLegacy.buildings[i].center ||
|
||||
resStock.buildings[i].floors !=
|
||||
resLegacy.buildings[i].floors ||
|
||||
resStock.buildings[i].width !=
|
||||
resLegacy.buildings[i].width ||
|
||||
resStock.buildings[i].layoutName !=
|
||||
"city-tower") {
|
||||
sameTown = false;
|
||||
break;
|
||||
}
|
||||
if (!sameTown) {
|
||||
printf(" *** layout select: stock library "
|
||||
"not bit-identical ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4.5 item 3: lot splitting along the frontage
|
||||
* (edge-side maxFrontage). The 100 m fallback squares
|
||||
* split into 25 m strips. */
|
||||
@@ -4654,27 +4857,70 @@ static bool runTownGenProbe()
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* townAreas json round trip. */
|
||||
nlohmann::json tj;
|
||||
RoadNetworkGen::townAreasToJson(graph, tj);
|
||||
RoadGraph graph2;
|
||||
RoadNetworkGen::townAreasFromJson(graph2, tj);
|
||||
if (graph2.townAreas.size() != 2) {
|
||||
printf(" *** town areas json: %zu areas, want 2 ***\n",
|
||||
graph2.townAreas.size());
|
||||
ok = false;
|
||||
} else {
|
||||
const RoadTownArea &ra = graph2.townAreas[0];
|
||||
const RoadTownArea &rb = graph2.townAreas[1];
|
||||
if (ra.name != "west" || ra.circular ||
|
||||
ra.center != areaA.center ||
|
||||
ra.halfSize != 60.0f ||
|
||||
rb.name != "east" || !rb.circular ||
|
||||
rb.center != areaB.center ||
|
||||
rb.radius != 120.0f) {
|
||||
printf(" *** town areas json: fields "
|
||||
"mismatch ***\n");
|
||||
/* townAreas json round trip (phase 5.9: towns + preferred
|
||||
* types included). */
|
||||
{
|
||||
RoadTown metro;
|
||||
metro.name = "metro";
|
||||
metro.seed = 42;
|
||||
graph.towns.push_back(metro);
|
||||
graph.townAreas[0].town = "metro";
|
||||
graph.townAreas[0].preferredTypes = { "house",
|
||||
"village" };
|
||||
nlohmann::json tj;
|
||||
RoadNetworkGen::townAreasToJson(graph, tj);
|
||||
RoadGraph graph2;
|
||||
RoadNetworkGen::townAreasFromJson(graph2, tj);
|
||||
if (graph2.townAreas.size() != 2) {
|
||||
printf(" *** town areas json: %zu areas, want 2 ***\n",
|
||||
graph2.townAreas.size());
|
||||
ok = false;
|
||||
} else {
|
||||
const RoadTownArea &ra = graph2.townAreas[0];
|
||||
const RoadTownArea &rb = graph2.townAreas[1];
|
||||
if (ra.name != "west" || ra.circular ||
|
||||
ra.center != areaA.center ||
|
||||
ra.halfSize != 60.0f ||
|
||||
rb.name != "east" || !rb.circular ||
|
||||
rb.center != areaB.center ||
|
||||
rb.radius != 120.0f) {
|
||||
printf(" *** town areas json: fields "
|
||||
"mismatch ***\n");
|
||||
ok = false;
|
||||
}
|
||||
if (graph2.towns.size() != 1 ||
|
||||
graph2.towns[0].name != "metro" ||
|
||||
graph2.towns[0].seed != 42 ||
|
||||
ra.town != "metro" ||
|
||||
ra.preferredTypes.size() != 2 ||
|
||||
ra.preferredTypes[0] != "house" ||
|
||||
ra.preferredTypes[1] != "village" ||
|
||||
!rb.town.empty() ||
|
||||
!rb.preferredTypes.empty()) {
|
||||
printf(" *** town areas json: 5.9 fields "
|
||||
"mismatch ***\n");
|
||||
ok = false;
|
||||
}
|
||||
/* effectiveAreaSeed: a standalone area keeps
|
||||
* its own seed; a town member mixes town and
|
||||
* area seeds; a dangling town reference falls
|
||||
* back to the area seed. */
|
||||
uint32_t mixed =
|
||||
RoadNetworkGen::effectiveAreaSeed(graph2,
|
||||
ra);
|
||||
RoadGraph graph3 = graph2;
|
||||
graph3.towns.clear();
|
||||
if (RoadNetworkGen::effectiveAreaSeed(graph2,
|
||||
rb) !=
|
||||
rb.seed ||
|
||||
mixed == ra.seed ||
|
||||
RoadNetworkGen::effectiveAreaSeed(graph3,
|
||||
graph3.townAreas[0]) !=
|
||||
ra.seed) {
|
||||
printf(" *** effectiveAreaSeed: "
|
||||
"stability ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,9 +24,11 @@
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -532,6 +534,23 @@ public:
|
||||
return "Terrain";
|
||||
}
|
||||
|
||||
/** Open the layout library editor window (Tools menu). */
|
||||
static void openLayoutLibraryEditor()
|
||||
{
|
||||
getLayoutLibraryEditorOpen() = true;
|
||||
}
|
||||
|
||||
/** Render the floating layout windows that live outside the
|
||||
* terrain panel (DSL help + layout library editor, phase 5.9.6),
|
||||
* once per frame from EditorUISystem. */
|
||||
static void renderLayoutWindows()
|
||||
{
|
||||
renderLayoutDslHelp();
|
||||
TerrainSystem *ts = TerrainSystem::getInstance();
|
||||
renderLayoutLibraryEditor(ts ? ts->getTownGenSystem() : nullptr,
|
||||
ts);
|
||||
}
|
||||
|
||||
private:
|
||||
/* Navigation section state (M3). */
|
||||
double m_navX = 0.0;
|
||||
@@ -693,7 +712,12 @@ private:
|
||||
"name: a built-in (village-house, town-rowhouse, "
|
||||
"city-block, city-tower) or a name from "
|
||||
"town_layouts.txt in the project directory "
|
||||
"((layout name body...) forms).");
|
||||
"((layout name [:tags (tag ...)] [:score n] body...) "
|
||||
"forms). Tags match the town area's Preferred Types "
|
||||
"(only layouts sharing a tag are candidates; no match "
|
||||
"falls back to the district default) and :score "
|
||||
"weights the random pick between candidates "
|
||||
"(default 1).");
|
||||
ImGui::Spacing();
|
||||
ImGui::Separator();
|
||||
ImGui::TextUnformatted("Bindings:");
|
||||
@@ -991,6 +1015,238 @@ private:
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
/* Phase 5.9.6: layout library editor — whole-file editing of
|
||||
* town_layouts.txt (CWD = the project directory) with a live entry
|
||||
* list and a one-off preview building spawned at the camera target
|
||||
* (TownGenSystem::previewLayout), so library layouts can be iterated
|
||||
* without touching road lots. */
|
||||
static bool &getLayoutLibraryEditorOpen()
|
||||
{
|
||||
static bool open = false;
|
||||
return open;
|
||||
}
|
||||
|
||||
static void renderLayoutLibraryEditor(TownGenSystem *tgs,
|
||||
TerrainSystem *ts)
|
||||
{
|
||||
bool &open = getLayoutLibraryEditorOpen();
|
||||
if (!open)
|
||||
return;
|
||||
|
||||
static std::string s_buf;
|
||||
static bool s_loaded = false;
|
||||
static std::string s_parsed; /* buffer content behind s_lib */
|
||||
static LayoutLib::LayoutLibrary s_lib;
|
||||
static std::string s_libError;
|
||||
static std::string s_status;
|
||||
static int s_sel = -1;
|
||||
static int s_district = 0;
|
||||
static int s_seed = 1;
|
||||
static int s_mode = 0;
|
||||
if (!s_loaded) {
|
||||
s_loaded = true;
|
||||
std::ifstream in("town_layouts.txt");
|
||||
if (in.good()) {
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
s_buf = ss.str();
|
||||
} else {
|
||||
s_status = "town_layouts.txt not found; a new "
|
||||
"file is created on Save.";
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(680.0f, 640.0f),
|
||||
ImGuiCond_FirstUseEver);
|
||||
if (!ImGui::Begin("Layout Library Editor", &open)) {
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
|
||||
if (ImGui::SmallButton("DSL Help"))
|
||||
getLayoutDslHelpOpen() = !getLayoutDslHelpOpen();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Reload")) {
|
||||
std::ifstream in("town_layouts.txt");
|
||||
if (in.good()) {
|
||||
std::ostringstream ss;
|
||||
ss << in.rdbuf();
|
||||
s_buf = ss.str();
|
||||
s_status = "Reloaded town_layouts.txt.";
|
||||
} else {
|
||||
s_status = "Reload failed: town_layouts.txt not "
|
||||
"found.";
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Save")) {
|
||||
std::ofstream out("town_layouts.txt", std::ios::trunc);
|
||||
if (out.good()) {
|
||||
out << s_buf;
|
||||
out.flush();
|
||||
s_status = out.good() ?
|
||||
"Saved town_layouts.txt." :
|
||||
"Save failed: write error.";
|
||||
} else {
|
||||
s_status = "Save failed: cannot open "
|
||||
"town_layouts.txt for writing.";
|
||||
}
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Write the buffer to town_layouts.txt in the "
|
||||
"project directory. demo-taxi stages this file "
|
||||
"as a symlink to the source tree, so Save edits "
|
||||
"the versioned file directly - just commit it.");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Insert Template")) {
|
||||
s_buf += "\n(layout my-layout\n";
|
||||
s_buf += LayoutLib::LayoutLibrary::
|
||||
defaultTemplateSource();
|
||||
s_buf += ")\n";
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Append a new (layout my-layout ...) entry "
|
||||
"wrapping the default template body");
|
||||
if (!s_status.empty())
|
||||
ImGui::TextWrapped("%s", s_status.c_str());
|
||||
|
||||
/* Reparse the library on buffer change; the parse state is
|
||||
* what the preview evaluates against. */
|
||||
if (s_buf != s_parsed) {
|
||||
s_parsed = s_buf;
|
||||
s_lib = LayoutLib::LayoutLibrary::withBuiltins();
|
||||
s_libError.clear();
|
||||
if (!s_buf.empty())
|
||||
s_lib.loadSource(s_buf, &s_libError);
|
||||
}
|
||||
if (!s_libError.empty())
|
||||
layoutCheckText(ImVec4(1.0f, 0.35f, 0.35f, 1.0f),
|
||||
"Library error: " + s_libError);
|
||||
|
||||
/* Entry list: the built-in district defaults plus the
|
||||
* buffer's (layout name ...) forms. */
|
||||
std::vector<std::string> names =
|
||||
LayoutLib::LayoutLibrary::builtinNames();
|
||||
const size_t builtinCount = names.size();
|
||||
{
|
||||
std::vector<LayoutLib::SExpr> forms;
|
||||
if (LayoutLib::parseSExprs(s_buf, forms, nullptr))
|
||||
for (const LayoutLib::SExpr &f : forms)
|
||||
if (f.isList() && f.list.size() >= 2 &&
|
||||
f.list[0].isSymbol("layout") &&
|
||||
f.list[1].isSymbol())
|
||||
names.push_back(f.list[1].text);
|
||||
}
|
||||
if (s_sel >= (int)names.size())
|
||||
s_sel = -1;
|
||||
ImGui::Text("Entries: %zu (%zu built-in)", names.size(),
|
||||
builtinCount);
|
||||
ImGui::BeginChild("libEntries", ImVec2(0.0f, 120.0f), true);
|
||||
for (size_t i = 0; i < names.size(); ++i) {
|
||||
std::string label = names[i];
|
||||
if (i < builtinCount)
|
||||
label += " (built-in)";
|
||||
if (ImGui::Selectable(label.c_str(),
|
||||
s_sel == (int)i))
|
||||
s_sel = (int)i;
|
||||
}
|
||||
ImGui::EndChild();
|
||||
if (s_sel >= 0) {
|
||||
const LayoutLib::LayoutLibrary::Entry *entry =
|
||||
s_lib.findEntry(names[s_sel]);
|
||||
if (entry) {
|
||||
std::string tags;
|
||||
for (size_t k = 0; k < entry->tags.size(); ++k) {
|
||||
if (k)
|
||||
tags += ", ";
|
||||
tags += entry->tags[k];
|
||||
}
|
||||
ImGui::Text("tags: %s score: %.2f",
|
||||
tags.empty() ? "(none)" :
|
||||
tags.c_str(),
|
||||
entry->score);
|
||||
}
|
||||
}
|
||||
|
||||
if (layoutInputMultiline("##libsrc", s_buf,
|
||||
ImVec2(-1.0f, -200.0f)))
|
||||
s_status.clear();
|
||||
|
||||
/* Preview: evaluate the selected entry with the district's
|
||||
* representative bindings and spawn it as a one-off
|
||||
* building at the editor camera target. */
|
||||
ImGui::Separator();
|
||||
if (!tgs) {
|
||||
ImGui::TextDisabled(
|
||||
"Preview unavailable (no town generator).");
|
||||
} else {
|
||||
const char *districtNames[] = {
|
||||
"Village", "Town", "City Residential",
|
||||
"City Center"
|
||||
};
|
||||
const char *modeNames[] = {
|
||||
"Exterior", "Interior", "Both"
|
||||
};
|
||||
ImGui::Combo("District##libprev", &s_district,
|
||||
districtNames, 4);
|
||||
ImGui::SameLine();
|
||||
ImGui::Combo("Mode##libprev", &s_mode, modeNames, 3);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Exterior = shell only, Interior = "
|
||||
"rooms only, Both = full building");
|
||||
ImGui::InputInt("Seed##libprev", &s_seed);
|
||||
ImGui::BeginDisabled(s_sel < 0);
|
||||
if (ImGui::Button("Spawn / Update Preview")) {
|
||||
Ogre::Vector3 center(0.0f, 0.0f, 0.0f);
|
||||
if (ts && ts->getSceneManager()) {
|
||||
Ogre::SceneManager *sm =
|
||||
ts->getSceneManager();
|
||||
Ogre::SceneNode *n = nullptr;
|
||||
if (sm->hasSceneNode(
|
||||
"EditorCameraTarget"))
|
||||
n = sm->getSceneNode(
|
||||
"EditorCameraTarget");
|
||||
else if (sm->hasSceneNode(
|
||||
"EditorCameraTargetNode"))
|
||||
n = sm->getSceneNode(
|
||||
"EditorCameraTargetNode");
|
||||
if (n)
|
||||
center = n->getPosition();
|
||||
center.y = ts->getHeightAt(center);
|
||||
}
|
||||
static const char *modes[] = {
|
||||
"exteriorOnly", "interiorOnly", "full"
|
||||
};
|
||||
std::string err;
|
||||
if (tgs->previewLayout(names[s_sel], &s_lib,
|
||||
s_district, s_seed,
|
||||
center, modes[s_mode],
|
||||
&err))
|
||||
s_status = "Preview spawned: " +
|
||||
names[s_sel];
|
||||
else
|
||||
s_status = "Preview failed: " + err;
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
if (ImGui::IsItemHovered(
|
||||
ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
ImGui::SetTooltip(
|
||||
"Evaluate the selected entry and "
|
||||
"(re)spawn it as a one-off building "
|
||||
"at the camera target; replaces the "
|
||||
"previous preview");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Clear Preview")) {
|
||||
tgs->clearLayoutPreview();
|
||||
s_status = "Preview cleared.";
|
||||
}
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static void renderRoadSection(flecs::entity entity, TerrainComponent &tc,
|
||||
TerrainSystem *ts)
|
||||
{
|
||||
@@ -1406,9 +1662,172 @@ private:
|
||||
"Removed %d town(s).", removed);
|
||||
s_townResult = buf;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Layout Library..."))
|
||||
getLayoutLibraryEditorOpen() = true;
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Edit town_layouts.txt and spawn "
|
||||
"preview buildings (phase 5.9.6)");
|
||||
if (!s_townResult.empty())
|
||||
ImGui::TextWrapped("%s", s_townResult.c_str());
|
||||
|
||||
ImGui::Separator();
|
||||
/* Phase 5.9: towns group several areas into one
|
||||
* settlement identity — the area's effective seed mixes
|
||||
* the town seed with the area seed, and population
|
||||
* NPCs record the town name. */
|
||||
ImGui::TextUnformatted("Towns");
|
||||
{
|
||||
int removeTown = -1;
|
||||
bool townsChanged = false;
|
||||
for (size_t ti = 0; ti < rg.towns.size(); ++ti) {
|
||||
RoadTown &town = rg.towns[ti];
|
||||
ImGui::PushID((int)ti);
|
||||
if (ImGui::TreeNode("town", "%zu: %s", ti,
|
||||
town.name.c_str())) {
|
||||
char tbuf[64];
|
||||
snprintf(tbuf, sizeof(tbuf), "%s",
|
||||
town.name.c_str());
|
||||
if (ImGui::InputText("Name##town",
|
||||
tbuf,
|
||||
sizeof(tbuf))) {
|
||||
/* Renaming a town updates
|
||||
* the areas referencing the
|
||||
* old name. */
|
||||
std::string oldName =
|
||||
town.name;
|
||||
town.name = tbuf;
|
||||
for (RoadTownArea &a :
|
||||
rg.townAreas)
|
||||
if (a.town == oldName)
|
||||
a.town = town.name;
|
||||
townsChanged = true;
|
||||
}
|
||||
int tSeed = (int)town.seed;
|
||||
if (ImGui::InputInt("Seed##town",
|
||||
&tSeed)) {
|
||||
town.seed =
|
||||
(uint32_t)tSeed;
|
||||
townsChanged = true;
|
||||
}
|
||||
/* Member areas — assign here or
|
||||
* via the area's own Town
|
||||
* dropdown below. */
|
||||
int unassign = -1;
|
||||
for (size_t ai = 0;
|
||||
ai < rg.townAreas.size();
|
||||
++ai) {
|
||||
if (rg.townAreas[ai].town !=
|
||||
town.name)
|
||||
continue;
|
||||
ImGui::PushID((int)ai);
|
||||
ImGui::BulletText(
|
||||
"%s",
|
||||
rg.townAreas[ai]
|
||||
.name.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(
|
||||
"Remove##fromtown"))
|
||||
unassign = (int)ai;
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (unassign >= 0 &&
|
||||
unassign <
|
||||
(int)rg.townAreas
|
||||
.size()) {
|
||||
rg.townAreas[unassign]
|
||||
.town.clear();
|
||||
townsChanged = true;
|
||||
}
|
||||
if (!rg.townAreas.empty() &&
|
||||
ImGui::BeginCombo(
|
||||
"Add Area##totown",
|
||||
"assign area...")) {
|
||||
for (size_t ai = 0;
|
||||
ai < rg.townAreas
|
||||
.size();
|
||||
++ai) {
|
||||
RoadTownArea &a =
|
||||
rg.townAreas
|
||||
[ai];
|
||||
if (a.town ==
|
||||
town.name)
|
||||
continue;
|
||||
std::string label =
|
||||
a.name;
|
||||
if (!a.town.empty())
|
||||
label += " (now: " +
|
||||
a.town +
|
||||
")";
|
||||
if (ImGui::Selectable(
|
||||
label.c_str())) {
|
||||
a.town =
|
||||
town.name;
|
||||
townsChanged =
|
||||
true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::Button(
|
||||
"Generate Town")) {
|
||||
std::string err;
|
||||
int placed =
|
||||
tgs->generateAreasForTown(
|
||||
town.name,
|
||||
&err);
|
||||
char gbuf[256];
|
||||
if (placed == 0)
|
||||
snprintf(
|
||||
gbuf,
|
||||
sizeof(gbuf),
|
||||
"Town generation "
|
||||
"failed: %s",
|
||||
err.c_str());
|
||||
else
|
||||
snprintf(
|
||||
gbuf,
|
||||
sizeof(gbuf),
|
||||
"Placed %d buildings "
|
||||
"in '%s'.",
|
||||
placed,
|
||||
town.name.c_str());
|
||||
s_townResult = gbuf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Regenerate just the areas "
|
||||
"belonging to this town "
|
||||
"(other generated towns "
|
||||
"stay untouched).");
|
||||
if (ImGui::Button("Remove Town"))
|
||||
removeTown = (int)ti;
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
if (removeTown >= 0 &&
|
||||
removeTown < (int)rg.towns.size()) {
|
||||
rg.towns.erase(rg.towns.begin() +
|
||||
removeTown);
|
||||
townsChanged = true;
|
||||
}
|
||||
if (ImGui::Button("Add Town")) {
|
||||
RoadTown t;
|
||||
t.name = "Town " + std::to_string(
|
||||
rg.towns.size() + 1);
|
||||
rg.towns.push_back(t);
|
||||
townsChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Areas join a town via their own "
|
||||
"Town dropdown below.");
|
||||
if (townsChanged)
|
||||
rg.bumpVersion();
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
/* Phase 4.5 item 4: town areas — named square/circular
|
||||
* generation scopes carried by the road graph. A lot is
|
||||
@@ -1498,6 +1917,105 @@ private:
|
||||
&area.densityScale,
|
||||
0.0f, 1.0f, "%.2f"))
|
||||
areasChanged = true;
|
||||
/* Phase 5.9: owning town + preferred
|
||||
* layout type tags. */
|
||||
{
|
||||
int townIdx = 0;
|
||||
for (size_t ti = 0;
|
||||
ti < rg.towns.size(); ++ti)
|
||||
if (rg.towns[ti].name ==
|
||||
area.town)
|
||||
townIdx = (int)ti + 1;
|
||||
if (ImGui::BeginCombo(
|
||||
"Town##area",
|
||||
townIdx == 0 ?
|
||||
"(none)" :
|
||||
rg.towns[townIdx - 1]
|
||||
.name.c_str())) {
|
||||
if (ImGui::Selectable(
|
||||
"(none)",
|
||||
townIdx == 0)) {
|
||||
area.town.clear();
|
||||
areasChanged = true;
|
||||
}
|
||||
for (size_t ti = 0;
|
||||
ti < rg.towns.size();
|
||||
++ti) {
|
||||
if (ImGui::Selectable(
|
||||
rg.towns[ti]
|
||||
.name.c_str(),
|
||||
townIdx ==
|
||||
(int)ti +
|
||||
1)) {
|
||||
area.town =
|
||||
rg.towns[ti]
|
||||
.name;
|
||||
areasChanged =
|
||||
true;
|
||||
}
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Settlement this area "
|
||||
"belongs to (mixes the town "
|
||||
"seed into the area seed; "
|
||||
"groups population).");
|
||||
std::string joined;
|
||||
for (size_t k = 0;
|
||||
k < area.preferredTypes.size();
|
||||
++k) {
|
||||
if (k)
|
||||
joined += ", ";
|
||||
joined +=
|
||||
area.preferredTypes[k];
|
||||
}
|
||||
char typeBuf[256];
|
||||
snprintf(typeBuf, sizeof(typeBuf),
|
||||
"%s", joined.c_str());
|
||||
if (ImGui::InputText(
|
||||
"Preferred Types##area",
|
||||
typeBuf,
|
||||
sizeof(typeBuf))) {
|
||||
area.preferredTypes.clear();
|
||||
std::string cur;
|
||||
for (const char *p =
|
||||
typeBuf;;
|
||||
++p) {
|
||||
if (*p != ',' &&
|
||||
*p != '\0') {
|
||||
cur += *p;
|
||||
continue;
|
||||
}
|
||||
size_t b =
|
||||
cur.find_first_not_of(
|
||||
" \t");
|
||||
if (b != std::string::
|
||||
npos)
|
||||
area.preferredTypes
|
||||
.push_back(
|
||||
cur.substr(
|
||||
b,
|
||||
cur.find_last_not_of(
|
||||
" \t") -
|
||||
b +
|
||||
1));
|
||||
cur.clear();
|
||||
if (*p == '\0')
|
||||
break;
|
||||
}
|
||||
areasChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Comma-separated layout "
|
||||
"tags (town_layouts.txt "
|
||||
":tags); empty = district "
|
||||
"default. Only layouts "
|
||||
"sharing a tag are picked, "
|
||||
"weighted by :score.");
|
||||
}
|
||||
/* Phase 4.7.2: population rule
|
||||
* overrides; inherit sentinels (0 for
|
||||
* ints, < 0 for probabilities) fall
|
||||
@@ -2879,7 +3397,6 @@ private:
|
||||
0);
|
||||
lotParamsEditor("Lot Params Right (A->B)",
|
||||
e.lotRight, 1);
|
||||
renderLayoutDslHelp();
|
||||
|
||||
/* Decals (phase 2.5): atlas-textured quads
|
||||
* painted on the road surface. The preset
|
||||
@@ -3035,7 +3552,10 @@ private:
|
||||
}
|
||||
|
||||
/* Phase 4.6: the pop-out layout editor renders whether or
|
||||
* not an edge is selected (bound by node ids). */
|
||||
* not an edge is selected (bound by node ids). The DSL help
|
||||
* and layout library windows render from EditorUISystem via
|
||||
* renderLayoutWindows() instead, so they stay open without
|
||||
* the terrain panel. */
|
||||
renderLayoutPopout(rg, ts);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user