Phase 4.6 complete: buildings made of rooms, per-building regen
This commit is contained in:
@@ -756,7 +756,9 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
`(`, otherwise a `LayoutLibrary`/built-in name; the built-ins
|
||||
`village-house`/`town-rowhouse`/`city-block`/`city-tower` share one
|
||||
parametric body keeping the legacy RNG draw order, so default towns
|
||||
are bit-identical to the pre-DSL generator. The plan carries its
|
||||
at density 1 are bit-identical to the pre-DSL generator (below
|
||||
density 1 the previously skipped lots build the modest clamp
|
||||
variant instead — see the density paragraph below). The plan carries its
|
||||
grid metrics (`cellSize`/`cellHeight`, defaults 2 m / 4 m) — the
|
||||
`(cell-size xz-m [height-m])` verb before the shell overrides them
|
||||
per layout and every meter->cell mapping honors them. Non-rectangular
|
||||
@@ -765,9 +767,31 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
the covered cells (all floors, or the current floor inside a
|
||||
`floor` context) and re-marks the hole-facing surviving neighbors
|
||||
as exterior walls; `roof` takes an optional rect `(roof type x0 z0
|
||||
x1 z1)` for per-wing roofs; openings landing on a cut cell are hard
|
||||
x1 z1)` for per-wing roofs (`:level n` sets the cell row, default
|
||||
above the top floor — a smaller upper floor roofs the lower wing at
|
||||
`:level 1`); openings landing on a cut cell are hard
|
||||
evaluation errors; the worked examples include L- and U-shaped
|
||||
houses. Evaluation failures
|
||||
houses. **Rooms-first layouts** (phase 4.6): `(shell w d [:floors n]
|
||||
:empty)` declares only the bounding rect (cells start empty) and the
|
||||
`room` verbs claim cells — the footprint is the union of the room
|
||||
rects, so L/U/T/courtyard shapes need no `cut`. Room rect edges snap
|
||||
as `lround(frac * cells)` with half-open intervals (neighboring rooms
|
||||
sharing a fraction never overlap or gap), claimed cells get
|
||||
Floor (+Ceiling on the top floor) and a `BuildingPlan::roomIds`/
|
||||
`roomNames` assignment (Phase 8 interior input); a finalize pass
|
||||
derives exterior walls on the union boundary and interior walls
|
||||
between different rooms (openings suppress the wall on their side).
|
||||
`(connect a b)` (alias of `idoor`) places an interior door on the
|
||||
shared wall. Hard errors: room overlap, `connect` without a shared
|
||||
edge, fractions escaping the shell, zero-cell rooms; warnings
|
||||
(appended to `TownResult::warnings` with a lot/strip prefix):
|
||||
rooms unreachable from the entry door, and `(door ... center)` on a
|
||||
non-rect union moving to the nearest floor cell. `(windows all)` on
|
||||
a rooms-first shell covers the whole union boundary (courtyard walls
|
||||
included). Declare rooms before door/windows; shell+cut stays
|
||||
supported for quick massing and density-1 default towns stay
|
||||
bit-identical.
|
||||
Evaluation failures
|
||||
warn and fall back to the district default (`evaluate()` resets the
|
||||
output plan on entry, so the fallback retry starts clean). Parking rects become
|
||||
extra POIs (`"<address> parking N"` — buildings are no longer 1:1
|
||||
@@ -822,7 +846,64 @@ Per-terrain procedural roads (`RoadGraph` component, geometry built by
|
||||
sample / entrance stairs center on the plan's front-door cell
|
||||
`TownBuilding::doorCellX` — cell centers sit at x*cellSize in the
|
||||
cell frame, so the stairs match the actual door frame). Editor UI:
|
||||
terrain panel "Town Generator" section.
|
||||
terrain panel "Town Generator" section, including **per-building
|
||||
regeneration** (phase 4.6): `TownGenSystem::regenerateBuilding()`
|
||||
re-runs the pure town generation with the container's recorded
|
||||
parameters (area params re-read from the graph, so edited lot params
|
||||
apply), rebuilds only the selected building entity (matched by
|
||||
address) and optionally overrides the CellGrid generation mode
|
||||
(exterior/interior/both) as an authoring preview — the preview never
|
||||
persists and a full regen resets to exterior. Phase 4.6 follow-up:
|
||||
every lot draws from its OWN RNG stream seeded
|
||||
`pageSeed ^ hash(centroid, vertex count, strip index)`
|
||||
(TownGen.cpp), so editing one lot's params/layout can never shift
|
||||
another lot's building and single-lot regeneration is exact (only
|
||||
street NUMBERING still depends on how many earlier lots of the same
|
||||
street built). `TownBuilding::paramsEdge`/`paramsSide` record the
|
||||
lot-params source (index into `RoadGraph::edges`, 0 = lotLeft,
|
||||
1 = lotRight) and power
|
||||
`TownGenSystem::regenerateByLotParams(edgeIndex, side)` — the
|
||||
"Regenerate" button next to each edge-side Layout field rebuilds
|
||||
exactly the buildings of that side, no building selection needed.
|
||||
The lot-params Layout field also has live DSL validation (error /
|
||||
warnings / OK summary under the field, TerrainEditor's
|
||||
`checkLayoutSource`) and a "Pop Out" button opening a floating
|
||||
layout editor bound to the edge by node ids (`renderLayoutPopout`);
|
||||
both editors write `RoadLotParams::layout` DIRECTLY through a
|
||||
CallbackResize std::string InputTextMultiline (no side buffers), so
|
||||
they can never desync. The Town Generator panel shows the last
|
||||
run's warnings (`TownGenSystem::getLastWarnings()`). The density
|
||||
roll NEVER drops a lot: a lost roll clamps a DEFAULT layout to a
|
||||
modest variant (small end of the footprint range, minimum floors,
|
||||
counted in `TownResult::densityModest`) and flows into the layout
|
||||
bindings `density`/`density-roll`/`density-pass` so explicit
|
||||
layouts can react themselves (a lost roll used to skip the lot
|
||||
silently, which read as "generation ignored my layout";
|
||||
`buildable == Off` still wins). Rooms can suppress their exterior
|
||||
windows with `(room name (rect ...) :windows none)` (bathroom/
|
||||
storage; works in both shell modes, declare rooms before the
|
||||
windows verb). Regen/clear teardown is synchronous and recursive
|
||||
(`TownGenSystem::destroyEntityDeep`: children first, then physics
|
||||
body via the JoltPhysicsWrapper passed to the ctor, TriangleBuffer
|
||||
Ogre entity + mesh, renderable, settlement material/texture, scene
|
||||
node last) — bare destruct() used to leak physics bodies and leave
|
||||
foundation/stairs meshes and ghost nodes behind; CellGrid shell
|
||||
meshes/frames/doors are still handled by the CellGridSystem
|
||||
dead-entity sweep (dead door children are skipped there, so the
|
||||
synchronous child teardown never double-destroys). The settlement
|
||||
material/texture resource names are unique per creation (the
|
||||
component's persistent id is appended): RTSS material bookkeeping
|
||||
is name-keyed while Ogre resource lifetime is refcounted, so the
|
||||
old deterministic names ("TownGen <area> material") let a stale
|
||||
RTSS entry outlive its material and crashed the next same-name
|
||||
removeAllShaderBasedTechniques (SIGSEGV on repeated Generate All
|
||||
Areas). CLI debug
|
||||
hooks: `--generate-town-areas` (mirrors the
|
||||
"Generate All Areas" panel button) and
|
||||
`--regen-lot-params=<edgeIndex>,<side>` (mirrors the per-side
|
||||
Regenerate button), both printing generation warnings;
|
||||
TOWNGEN_REPEAT_GENERATE=1 in the environment runs the generation a
|
||||
second time a few frames later (headless double-Generate).
|
||||
- **Chunked page meshes**: page assembly splits the page geometry into
|
||||
chunk meshes capped at `ROAD_PAGE_CHUNK_MAX_VERTICES` (60000) vertices,
|
||||
split on piece boundaries — each chunk gets its own mesh, static Jolt
|
||||
|
||||
@@ -1712,7 +1712,19 @@ overridden sides from the Editor paragraph below is not yet
|
||||
implemented). Probes in
|
||||
`runTownGenProbe()` cover buildable-off, floor pinning, district
|
||||
override, roof override and the exact setback shift on a two-lot
|
||||
single-edge graph.
|
||||
single-edge graph. Phase 4.6 follow-up: the density roll NEVER drops
|
||||
a lot — a lost roll clamps a DEFAULT layout to the modest variant
|
||||
(small end of the footprint range, minimum floors; counted in
|
||||
`TownResult::densityModest`) and the roll flows into the layout
|
||||
bindings `density`/`density-roll`/`density-pass` so explicit layouts
|
||||
can react themselves (e.g. `(if density-pass ... ...)` for optional
|
||||
wings/garages). A hand-authored layout on a lot losing the roll used
|
||||
to silently produce no building and no warning, and the first fix
|
||||
(explicit layout = bypass the roll) made a low-density district
|
||||
ignore the district character for authored lots; the modest-variant
|
||||
+ bindings scheme keeps the roll meaningful everywhere without ever
|
||||
dropping a lot (probe: Village seed 3 builds all 12 lots with
|
||||
`densityModest > 0`; explicit layouts on every side clamp nothing).
|
||||
|
||||
**Editor.** Edge inspector gains a "Lot Parameters (left / right)"
|
||||
section; the road-edit lot outline preview (dark grey loops) tints a
|
||||
@@ -1814,8 +1826,9 @@ subtraction; place cuts right after `shell`, before `door`/`windows`
|
||||
built-in district layouts (`village-house`/`town-rowhouse`/
|
||||
`city-block`/`city-tower`) all share one parametric body drawing
|
||||
width, depth, floors from the `min-*`/`max-*` bindings in the legacy
|
||||
order, so an all-default town is bit-identical to the pre-DSL
|
||||
generator (probe-verified). Shaped buildings stay consistent end
|
||||
order, so an all-default town at density 1 is bit-identical to the
|
||||
pre-DSL generator (probe-verified); below density 1 the previously
|
||||
skipped lots now build the modest clamp variant instead. Shaped buildings stay consistent end
|
||||
to end: `TownGen` records the plan's front-door cell
|
||||
(`TownBuilding::doorCellX`) and samples the door ground / places the
|
||||
entrance stairs at the real door instead of the front-face center,
|
||||
@@ -1840,6 +1853,38 @@ partial shell), and only when THAT fails is the lot skipped
|
||||
(`skippedLayoutError`). Parking rects become extra POIs
|
||||
(`"<address> parking N"`) — buildings are no longer 1:1 with POIs,
|
||||
`TownGenSystem::generate()` gathers them via `TownPoi::building`.
|
||||
**Rooms-first layouts (phase 4.6, implemented).**
|
||||
`(shell w d [:floors n] :empty)` declares only the bounding rect —
|
||||
the cells start empty and `room` verbs claim them, so the footprint is
|
||||
the union of the room rects and L/U/T/courtyard shapes need no `cut`.
|
||||
In this mode room rect edges snap as `lround(frac * cells)` and rooms
|
||||
cover the half-open interval `[x0, x1)` (neighboring rooms sharing a
|
||||
fraction meet at the same boundary index — no overlap, no gap);
|
||||
claiming an already-claimed cell is a hard error naming both rooms, as
|
||||
are fractions escaping the shell and rooms snapping to zero cells.
|
||||
Claimed cells get Floor (+Ceiling on the top floor) and a
|
||||
`BuildingPlan::roomIds`/`roomNames` assignment (the Phase 8 interior
|
||||
generator reads the rooms from here); NO walls are set by the room
|
||||
verb — a finalize pass at the end of `evaluate()` derives exterior
|
||||
walls on the union boundary and interior walls between different rooms
|
||||
(both skip sides carrying a door/window opening). `(connect a b)` is
|
||||
an alias of `idoor` (shared-wall IntDoor; the no-shared-edge case is a
|
||||
hard error). Warnings (non-fatal, appended to
|
||||
`TownResult::warnings` with a lot/strip prefix via the new
|
||||
`evaluate()` warnings out-param): a room unreachable from the entry
|
||||
door (flood fill from the floor-0 door cell through same-room cells
|
||||
and IntDoor/IntWindow edges) and `(door ... center)` landing outside a
|
||||
non-rect footprint, which moves to the nearest floor cell on that
|
||||
side (explicit offsets keep the hard error). `(windows all)` on a
|
||||
rooms-first shell covers the whole union boundary (per-direction wall
|
||||
lines with `:spacing`), so courtyard/notch walls get windows; named
|
||||
sides keep the perimeter walk. `roof` gained `:level n` (the cell row
|
||||
the roof sits on, default above the top floor) so a smaller upper
|
||||
floor can roof the lower wing at `:level 1`. Bare keywords (a
|
||||
`:keyword` followed by another keyword or the form end) read as `true`
|
||||
flags — that is how `:empty` parses. The DSL Help window examples
|
||||
gained room-based layouts (L from two rooms, U with a courtyard,
|
||||
two-storey with a smaller upper floor), all probe-verified.
|
||||
`TownGenSystem::fillBuildingCells()` copies the plan flags into the
|
||||
grid; roofs come from `plan.roofs` when non-empty, else the edge-side
|
||||
`roofType`/Flat fallback; an optional `town_layouts.txt` in the
|
||||
@@ -1848,14 +1893,56 @@ the file exists). Editor: the Lot Params tree nodes gained a
|
||||
multiline Layout text field with an Insert Template button (fills an
|
||||
empty field with `LayoutLibrary::defaultTemplateSource()`, the
|
||||
shared built-in body) and a DSL Help window showing the reference
|
||||
plus the worked `LayoutLibrary::examples()`. Probes in `runTownGenProbe()` cover the
|
||||
plus the worked `LayoutLibrary::examples()`. Phase 4.6 follow-up:
|
||||
the field got live validation (`TerrainEditor::checkLayoutSource()` —
|
||||
the source is evaluated against representative 40x40 m bindings with a
|
||||
scratch RNG; red error, yellow warnings or a green
|
||||
"OK: NxM cells, F floor(s)" summary under the field), a "Regenerate"
|
||||
button rebuilding exactly the buildings whose lots take their params
|
||||
from that edge side (`TownGenSystem::regenerateByLotParams()`, matched
|
||||
through `TownBuilding::paramsEdge`/`paramsSide` recorded at generation
|
||||
time — per-lot RNG streams, see §21.4, keep every other lot
|
||||
bit-identical), and a "Pop Out" button opening a floating editor
|
||||
window bound to the edge by its (nodeA, nodeB) pair
|
||||
(`TerrainEditor::renderLayoutPopout()`, with its own validation,
|
||||
template insert and Regenerate button). Both the inline field and
|
||||
the pop-out edit `RoadLotParams::layout` DIRECTLY (a CallbackResize
|
||||
std::string InputTextMultiline, no side buffers) — the earlier
|
||||
fixed 16 KB buffers could go stale against each other and lose edits.
|
||||
Per-room window suppression: `(room name (rect ...) :windows none)`
|
||||
flags the room (bathroom/storage), and both window passes
|
||||
(perimeter walk and union-boundary) skip its cells — declare rooms
|
||||
before the `windows` verb; probe-verified in both shell modes.
|
||||
Regen/clear teardown is synchronous (`TownGenSystem::destroyEntityDeep`
|
||||
— children first, then physics body, TriangleBuffer Ogre entity +
|
||||
mesh, renderable, settlement material/texture, scene node last); the
|
||||
earlier bare `destruct()` leaked physics bodies and left
|
||||
foundation/stairs meshes and ghost scene nodes behind after a
|
||||
single-building regen (CellGrid shell meshes/doors still go through
|
||||
the CellGridSystem dead-entity sweep, which skips the already-torn-down
|
||||
door children). The Town Generator panel
|
||||
lists the last run's warnings (`TownGenSystem::getLastWarnings()`).
|
||||
Probes in `runTownGenProbe()` cover the
|
||||
lot-size conditional, shell clamping, parking rects, room/idoor
|
||||
flags, name-vs-inline resolution, error rejection, the
|
||||
template/example sources, the inline-layout
|
||||
integration (10x8 m, 3 floors, cone roof, front-door flags), the
|
||||
broken-layout fallback, the `cut` verb (L-shape cell flags,
|
||||
exterior walls re-marked on the cut-facing neighbors, per-rect
|
||||
roofs) and the door-on-a-cut-cell rejection. Deferred: `place`/`fence` materialization,
|
||||
roofs) and the door-on-a-cut-cell rejection, plus the phase 4.6
|
||||
rooms-first probes (L/U union footprints with derived exterior and
|
||||
interior walls and roomIds, half-open snapping consistency at odd
|
||||
cell counts, overlap/out-of-shell/zero-cell/no-shared-edge rejections,
|
||||
the unreachable-room warning, the door-center fallback warning,
|
||||
union-boundary windows on a courtyard wall and roof `:level`), the
|
||||
per-room `:windows none` probe (flagged room's boundary cells get no
|
||||
windows in both shell modes while the neighbors keep theirs), the
|
||||
density-modest probe (no lot is ever dropped; lost rolls clamp only
|
||||
default layouts), and the
|
||||
per-lot RNG stream stability probe (an edge-side layout edit leaves
|
||||
every other building bit-identical; `paramsEdge`/`paramsSide` recorded
|
||||
for all 12 buildings).
|
||||
Deferred: `place`/`fence` materialization,
|
||||
the Phase 8 interior consumer.
|
||||
|
||||
### 21.3 Lot splitting along long frontages
|
||||
@@ -1931,15 +2018,17 @@ the union of the per-page runs (probe-verified: 12 lots on 12
|
||||
distinct pages, identical center/yaw/width/depth/floors/address,
|
||||
disjoint lot sets, per-page determinism). Negative page coordinates
|
||||
are the "all pages" sentinel; physical terrain pages are always
|
||||
non-negative. Without a mapper the legacy single stream is kept
|
||||
bit-identical (all pre-existing probes hold). `TownGenSystem::
|
||||
non-negative. Phase 4.6 follow-up: lots no longer share even the
|
||||
per-page stream — before a lot's first draw the RNG is reseeded to
|
||||
`pageSeed ^ hash(centroid in cm, vertex count, strip index)`, so a
|
||||
params/layout edit on one lot can never shift another lot's building
|
||||
and single-lot regeneration is exact (street numbering, which counts
|
||||
built lots in order, is the one remaining cross-lot dependency).
|
||||
`TownGenSystem::
|
||||
generate()`/`generateAreas()` pass the all-pages scope on streaming
|
||||
terrains (the demo-taxi `--generate-town=town,7` count moved 26 -> 28
|
||||
from the reseeding, deterministic; the exact count tracks the saved
|
||||
graph INCLUDING edge-side lot params — a saved per-edge `layout`
|
||||
override changes that lot's RNG draw count and reshuffles the
|
||||
per-page stream, e.g. the demo's current saved lot-params yield 25,
|
||||
the pristine graph 28), so editor-built towns match the
|
||||
terrains (the demo-taxi `--generate-town=town,7` count tracks the
|
||||
reseeding — 27 with the demo's saved lot params — deterministic), so
|
||||
editor-built towns match the
|
||||
streamed union. The game-mode driver `TownGenSystem::update()`
|
||||
(called from `TerrainSystem::update`, active only when
|
||||
`editScene::isGameMode()` + streaming + non-empty `townAreas`) polls
|
||||
|
||||
@@ -1575,38 +1575,175 @@ splitting -> areas -> per-page streaming).
|
||||
|
||||
### Phase 4.6 — Room-composed buildings + per-building regeneration
|
||||
|
||||
- Rooms-first layout DSL (additive, complements shell+cut): the shell
|
||||
declares the overall building rect, rooms fill it by relative
|
||||
offsets/sizes (fractions or meters, e.g. `(room living (rect 0 0 0.5
|
||||
1) :at 0 0)` style entries), and the floor FOOTPRINT is the union of
|
||||
the room rects — L/U/T/courtyard shapes emerge from room placement
|
||||
with no `cut` needed. Exterior walls = boundary of the union,
|
||||
interior walls = shared edges, openings via room connections
|
||||
(`(connect living kitchen)` / door declared on the connection;
|
||||
validated adjacency). Rooms may not overlap; gaps in the union
|
||||
become exterior (courtyard/notch); the entry door targets a room
|
||||
cell on the union boundary. Output stays the same BuildingPlan, so
|
||||
materialization, streaming and the Phase 8 interior consumer are
|
||||
untouched — pure evaluator work. shell+cut stays supported for
|
||||
quick massing.
|
||||
- This becomes the primary authoring path for buildings that get
|
||||
interiors (Phase 8): rooms are the primary data there anyway, so
|
||||
exterior and interior can never disagree.
|
||||
- Per-building regeneration in the editor: re-evaluate one lot's
|
||||
layout and rebuild only that building entity (the lot params are the
|
||||
persisted truth), so layout tweaking doesn't pay a whole-town
|
||||
rebuild. Deterministic for the edited lot; downstream lots in the
|
||||
same page stream can diverge from a full regeneration when the
|
||||
layout's RNG draw count changes — a full regen restores consistency
|
||||
(documented in the UI).
|
||||
- It should be easy to check for overlapping rooms and connected rooms not sharing a wall. Due to fractionable numbers use, care should be taken to make sure neighboring rooms do not disagree on resulting cell positions, making it safe to assume that the fraction gets consistent integer positions in the grid.
|
||||
- Samples in the help should be updated adding room-based layouts example for various building footprints.
|
||||
- Headless: probes for room-union footprints (L/U shapes without cut),
|
||||
overlap/adjacency validation errors, connections -> IntDoor flags,
|
||||
entry-door placement on non-rect unions, per-building regen
|
||||
equivalence with a full regen for the edited lot.
|
||||
**Rooms-first layout DSL (additive, complements shell+cut).** The
|
||||
shell declares the overall building rect, rooms fill it by relative
|
||||
offsets/sizes (fractions or meters, e.g. `(room living (rect 0 0 0.5
|
||||
1))` as today), and the floor FOOTPRINT is the union of the room
|
||||
rects — L/U/T/courtyard shapes emerge from room placement with no
|
||||
`cut` needed. Exterior walls = boundary of the union, interior
|
||||
walls = shared edges, openings via room connections (`(connect
|
||||
living kitchen)` — an IntDoor on the shared wall). Gaps in the
|
||||
union become exterior (courtyard/notch); the entry door targets a
|
||||
Floor cell on the union boundary; per-floor unions may differ (upper
|
||||
floors can be smaller), and the per-row-run roof fallback already
|
||||
covers non-rect footprints. Output stays the same BuildingPlan, so
|
||||
materialization, streaming and the Phase 8 interior consumer are
|
||||
untouched — pure evaluator work. shell+cut stays supported for
|
||||
quick massing.
|
||||
|
||||
**Cell snapping and validation.** Rect edges convert to cell indices
|
||||
as `lround(frac * cells)` (meters go through `cellSize` the same way)
|
||||
and rooms cover the HALF-OPEN interval [x0, x1) — a shared fraction
|
||||
(e.g. left room ends at 0.5, right room starts at 0.5) maps to the
|
||||
same boundary index by construction, so neighboring rooms can never
|
||||
disagree about cell positions or double-claim a cell. On top of that
|
||||
the evaluator reports hard errors for: overlapping rooms (cell-space
|
||||
AABB intersection), a `connect` between rooms with no shared edge of
|
||||
at least one cell, rooms escaping the shell rect, and a room that
|
||||
snaps to zero cells. Warnings (not errors) for unreachable rooms
|
||||
(no connection path to the entry) — easy to check mechanically, so
|
||||
authoring mistakes surface as messages, not broken geometry.
|
||||
|
||||
**Editor UX.** The DSL Help examples gain room-based layouts for
|
||||
various footprints (L from two rooms, U with a courtyard,
|
||||
two-storey with a smaller upper floor) next to the cut-based ones.
|
||||
Per-building regeneration: re-evaluate one lot's layout and rebuild
|
||||
only that building entity (the lot params are the persisted truth),
|
||||
so layout tweaking doesn't pay a whole-town rebuild. The rebuild
|
||||
offers a generation-mode choice — exterior (the default, what the
|
||||
game and full-town regen/streaming always use), interior, or both —
|
||||
purely as an authoring preview, so a room-composed interior layout
|
||||
can be checked in place without entering the building; the preview
|
||||
mode never persists (generated towns aren't serialized, and a full
|
||||
regen resets everything to exterior). Per-lot RNG streams (below)
|
||||
make the rebuild exact for every untouched lot; only street numbering
|
||||
can shift when an edit changes whether a lot builds at all.
|
||||
|
||||
**Phase 8 alignment.** This becomes the primary authoring path for
|
||||
buildings that get interiors: rooms are the primary data there
|
||||
anyway, so exterior and interior can never disagree.
|
||||
|
||||
**Headless probes.** Room-union footprints (L/U shapes without
|
||||
cut), the snapping consistency (neighboring fractional rooms tile
|
||||
with no overlap/gap at odd cell counts), overlap/adjacency/
|
||||
out-of-shell validation errors, `connect` -> IntDoor flags, entry
|
||||
door on non-rect unions, per-building regen equivalence with a full
|
||||
regen for the edited lot, interior-preview regen rebuilding only
|
||||
the selected building with the interior generation mode (the
|
||||
generationMode flag round-trip itself is already covered by
|
||||
cellgrid_door_test).
|
||||
|
||||
Status (2026-10-08, implemented): the rooms-first mode landed as
|
||||
designed — `(shell w d [:floors n] :empty)` + claiming `room` verbs
|
||||
(half-open `lround` snapping, overlap/out-of-shell/zero-cell hard
|
||||
errors), the finalize pass deriving exterior/interior walls from the
|
||||
union, `connect` (idoor alias), the door-center fallback + unreachable
|
||||
rooms as warnings (new `evaluate()` warnings out-param, appended to
|
||||
`TownResult::warnings` with lot/strip prefixes), union-boundary
|
||||
`windows all`, roof `:level n`, `BuildingPlan::roomIds`/`roomNames`,
|
||||
three room-based help examples (L, U-courtyard, two-storey smaller
|
||||
upper floor) and the Town Generator panel's Per-Building Regeneration
|
||||
block (`TownGenSystem::regenerateBuilding()` — container params
|
||||
recorded by `generate()`/`generateAreas()`, the building matched by
|
||||
address, rebuilt in the selected preview mode). Probes in
|
||||
`runTownGenProbe()` cover the DSL side (union footprints, snapping
|
||||
consistency at odd cell counts, validation rejections, the warning
|
||||
paths, `:level`); the per-building regen itself is exercised from the
|
||||
editor panel (it shares `generateTown()` with the probe-covered paths).
|
||||
|
||||
Status (2026-10-08, follow-up — exact single-lot regen + DSL editor
|
||||
UX): every lot now draws from its OWN RNG stream seeded
|
||||
`pageSeed ^ hash(centroid in cm, vertex count, strip index)`
|
||||
(TownGen.cpp), so a params/layout edit on one lot can never shift
|
||||
another lot's building — single-building and single-edge-side regen
|
||||
are exact (street numbering can still shift when an edit changes
|
||||
whether a lot builds at all; the all-default town's building COUNT
|
||||
changes vs the shared stream, 25 -> 27 on the demo scene).
|
||||
`TownBuilding::paramsEdge`/`paramsSide` record the lot-params source
|
||||
edge+side; `TownGenSystem::regenerateByLotParams(edgeIndex, side)`
|
||||
rebuilds exactly the buildings of that edge side from the "Regenerate"
|
||||
button next to the edge-side Layout field (shared core
|
||||
`regenerateMatching()` with `regenerateBuilding()`). The Layout field
|
||||
gained live DSL validation (`TerrainEditor::checkLayoutSource()` —
|
||||
representative bindings, scratch RNG; red error / yellow warnings /
|
||||
green "OK: NxM cells, F floor(s)" under the field), a "Pop Out" button
|
||||
opening a floating 16 KB editor bound to the edge by node ids
|
||||
(`renderLayoutPopout()`, with its own validation, template insert and
|
||||
Regenerate), and the Town Generator panel lists the last run's
|
||||
warnings (`TownGenSystem::getLastWarnings()`). New probe in
|
||||
`runTownGenProbe()`: a layout edit on one edge side leaves every other
|
||||
building bit-identical and `paramsEdge`/`paramsSide` are recorded for
|
||||
all 12 buildings of the probe town.
|
||||
|
||||
Status (2026-10-09, density-bypass fix): a lot whose edge side carries
|
||||
an explicit (non-empty) layout string BYPASSED the district density
|
||||
roll — superseded on 2026-10-10 by the density-modest scheme below
|
||||
(the bypass made authored lots ignore the district's density
|
||||
character). Root cause of "room-first layout renders nothing, no
|
||||
error" stands: the lot lost the Village density roll (0.8, area seed
|
||||
1), so the layout was never evaluated and nothing was reported.
|
||||
CLI debug hooks: `--generate-town-areas` and
|
||||
`--regen-lot-params=<edgeIndex>,<side>`, both printing generation
|
||||
warnings.
|
||||
|
||||
Status (2026-10-10, density modest variant + editor/DSL fixes):
|
||||
|
||||
1. **Density never drops a lot.** A lost roll now clamps a DEFAULT
|
||||
layout to the modest variant (small end of the footprint range,
|
||||
minimum floors; `TownResult::densityModest` replaces
|
||||
`skippedDensity`) and flows into three new layout bindings —
|
||||
`density`, `density-roll`, `density-pass` — so explicit layouts
|
||||
can parametrize conditional features themselves
|
||||
(`(if density-pass ...)`). Building counts change: lots that used
|
||||
to be silently skipped now build (the demo town: 9 instead of 8).
|
||||
2. **Per-room windows.** `(room name (rect ...) :windows none)`
|
||||
suppresses exterior windows on the room's boundary cells
|
||||
(bathroom/storage) in both shell modes; declare rooms before the
|
||||
`windows` verb. New worked example ("Windowless bathroom") in the
|
||||
DSL Help, probe-verified.
|
||||
3. **Pop-out DSL editor sync.** Both the inline Lot Params field and
|
||||
the pop-out window now edit `RoadLotParams::layout` DIRECTLY via a
|
||||
CallbackResize std::string InputTextMultiline — the old fixed 16 KB
|
||||
side buffers could go stale against each other and lose edits.
|
||||
4. **Synchronous regen teardown.** `TownGenSystem::destroyEntityDeep`
|
||||
replaces bare `destruct()` at all four teardown sites (clear,
|
||||
per-building regen, streamed page unload, clearStreamedPages):
|
||||
children recurse first, then physics body removal (TownGenSystem
|
||||
now receives the JoltPhysicsWrapper from TerrainSystem),
|
||||
TriangleBuffer Ogre entity + mesh, renderable, settlement
|
||||
material/texture, and the scene node last. CellGrid shell
|
||||
meshes/frames/doors still go through the CellGridSystem dead-entity
|
||||
sweep (it skips the already-torn-down door children). A regen no
|
||||
longer leaves ghost nodes, leaked bodies or stale foundation/stairs
|
||||
meshes behind.
|
||||
5. **RTSS crash on repeated Generate (2026-10-10).** The settlement
|
||||
material/texture resource names are now unique per creation (the
|
||||
component's auto-generated persistent id is appended). RTSS
|
||||
material bookkeeping is name-keyed while Ogre resource lifetime is
|
||||
refcounted, and the town material/texture names were deterministic
|
||||
("TownGen <area> material", "towngen_tex_<seed>"), so a stale RTSS
|
||||
entry from a previous generation could outlive its material object
|
||||
and SIGSEGV the next same-name removeAllShaderBasedTechniques
|
||||
(ProceduralMaterialSystem::createMaterial). Headless repro hook:
|
||||
TOWNGEN_REPEAT_GENERATE=1 runs the CLI generation twice with frames
|
||||
in between (verified: two generate+regen cycles, clean exit).
|
||||
6. **White foundations after a full Generate (2026-10-10).**
|
||||
`ProceduralMeshSystem::convertToMesh` bound the settlement material
|
||||
only when its component was already `created` and never retried, so
|
||||
foundations/stairs built in the frames before
|
||||
ProceduralMaterialSystem ran kept the default white material
|
||||
forever (single-building regen reuses the created material, so it
|
||||
was fine; pre-teardown the correctly-bound ghost meshes of the
|
||||
previous town masked the race). The conversion now defers (keeps
|
||||
the TriangleBuffer dirty) until the referenced material is created.
|
||||
The CellGrid shell was unaffected — it binds the material by NAME,
|
||||
which Ogre resolves once the material exists.
|
||||
|
||||
### Phase 4.7 NPC motion
|
||||
- it is about general abstract NPC motion simulation in exterior scene, to add life to the scene and still be believable.
|
||||
- the complicates comes from the goal of the motion to be beliavable so it should have a reason.
|
||||
- some of features needed for this are either implemented or partially implemented on NPC level and should be found, if any and reused or modified or discarded i unfeasible within a reason.
|
||||
- There is GOAP implementation in the tree, but if HTN is superior, it should be implemented.
|
||||
- the function is centered around NPC having a target to get to.
|
||||
- should be controlled by dedicated system
|
||||
- NPCs are deterministic and are generated by the town using per-lot settings
|
||||
@@ -1623,6 +1760,7 @@ splitting -> areas -> per-page streaming).
|
||||
- Traffic is used only for exterior. Interior uses completely different systems based on GOAP or HTN and behavior trees. NPC motion system uses goals and delivers NPCs to simulate live town being believable but without complex computations. Basically it simulates NPC presence and motion at location at time so if player gets to that location and NPC is moving to the place they see NPC walking or driving. NPCs should realistically leave buildings, enter vehicles or walk, park and leave vehicles ane enter buildings. Entering building means NPC despawn from exterior scene by activating the front door. Not all targets are inside buildings, exterior locations should be supporterd too. It should be easy to author locations (interior/exterior).
|
||||
- Taxi mission should have NPCs near player to have special goal of using the taxi and waiting to be picked up. However out of the mission, NPCs should be able to call the taxi and transit by it.
|
||||
### Phase 5 — NPC vehicle traffic
|
||||
- the NPC owns the vehicle, the vehicles are registered to the NPC as driver. Other group member can get to the vehicle as passengers if they go to the same location, or the driver can drive them to their different destinations.
|
||||
- the NPC can choose by distance if vehicle is used or going by feet (phase 8).
|
||||
- the vehicle is used when NPC has one parked near their current location.
|
||||
- NPC position is simulated in varying levels of detail
|
||||
|
||||
@@ -849,15 +849,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 3999.490234375,
|
||||
"y": 10.889010429382324,
|
||||
"z": 3961.32958984375
|
||||
"x": 3997.240966796875,
|
||||
"y": 10.873899459838867,
|
||||
"z": 3960.30029296875
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.9959467053413391,
|
||||
"x": -0.05614081025123596,
|
||||
"y": -0.07026758790016174,
|
||||
"z": -0.000987574690952897
|
||||
"w": 0.9887836575508118,
|
||||
"x": -0.05630514770746231,
|
||||
"y": -0.13825948536396027,
|
||||
"z": -0.004587905947118998
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -1254,15 +1254,15 @@
|
||||
},
|
||||
"transform": {
|
||||
"position": {
|
||||
"x": 4014.667724609375,
|
||||
"y": 10.975330352783203,
|
||||
"z": 3964.62451171875
|
||||
"x": 4014.4697265625,
|
||||
"y": 10.973102569580078,
|
||||
"z": 3965.343017578125
|
||||
},
|
||||
"rotation": {
|
||||
"w": 0.8245971202850342,
|
||||
"x": -0.0635930672287941,
|
||||
"y": 0.5606324672698975,
|
||||
"z": 0.04107089713215828
|
||||
"w": 0.7457117438316345,
|
||||
"x": -0.057677898555994034,
|
||||
"y": 0.6619836091995239,
|
||||
"z": 0.04863201826810837
|
||||
},
|
||||
"scale": {
|
||||
"x": 1.0,
|
||||
@@ -1608,7 +1608,7 @@
|
||||
0.800000011920929
|
||||
],
|
||||
"sunSphereSize": 5.0,
|
||||
"timeOfDay": 9.319156646728516,
|
||||
"timeOfDay": 23.656871795654297,
|
||||
"timeSpeed": 0.12999999523162842
|
||||
},
|
||||
"transform": {
|
||||
|
||||
@@ -105,6 +105,15 @@ struct BuildingPlan {
|
||||
* + x]. Only valid when cellsX/cellsZ/floors > 0.
|
||||
*/
|
||||
std::vector<uint64_t> cells;
|
||||
/**
|
||||
* Room assignment per cell (same indexing as cells, -1 = none)
|
||||
* plus the room name table (roomIds index into it). Filled only
|
||||
* by rooms-first layouts (`shell ... :empty`, phase 4.6); empty
|
||||
* for shell+cut layouts. Phase 8's interior generator reads the
|
||||
* rooms from here.
|
||||
*/
|
||||
std::vector<int> roomIds;
|
||||
std::vector<std::string> roomNames;
|
||||
std::vector<PlanRoof> roofs;
|
||||
std::vector<PlanParking> parkings;
|
||||
std::vector<PlanPlacement> placements;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <cmath>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <utility>
|
||||
|
||||
namespace LayoutLib {
|
||||
|
||||
@@ -127,6 +128,48 @@ const std::vector<LayoutLibrary::LayoutExample> &LayoutLibrary::examples()
|
||||
" (roof flat 0 0 w (/ d 2))\n"
|
||||
" (roof flat 0 (/ d 2) 4 d)\n"
|
||||
" (roof flat 8 (/ d 2) w d))\n" },
|
||||
{ "L-shaped house from two rooms (rooms-first)",
|
||||
"(shell 12 8 :empty)\n"
|
||||
"(floor 0 (room living (rect 0 0 0.5 1))\n"
|
||||
" (room kitchen (rect 0.5 0 1 0.5))\n"
|
||||
" (connect living kitchen)\n"
|
||||
" (door front -2)\n"
|
||||
" (windows all :spacing 4))\n"
|
||||
"(roof flat 0 0 6 8)\n"
|
||||
"(roof flat 6 0 12 4)\n" },
|
||||
{ "U-shaped house from three rooms (courtyard)",
|
||||
"(shell 12 12 :empty)\n"
|
||||
"(floor 0 (room hall (rect 0 0 1 0.5))\n"
|
||||
" (room bed-a (rect 0 0.5 0.34 1))\n"
|
||||
" (room bed-b (rect 0.66 0.5 1 1))\n"
|
||||
" (connect hall bed-a)\n"
|
||||
" (connect hall bed-b)\n"
|
||||
" (door front -4)\n"
|
||||
" (windows all :spacing 4))\n"
|
||||
"(roof flat 0 0 12 6)\n"
|
||||
"(roof flat 0 6 4 12)\n"
|
||||
"(roof flat 8 6 12 12)\n" },
|
||||
{ "Two storeys, smaller upper floor (rooms-first)",
|
||||
"(shell 12 8 :floors 2 :empty)\n"
|
||||
"(floor 0 (room living (rect 0 0 0.5 1))\n"
|
||||
" (room kitchen (rect 0.5 0 1 1))\n"
|
||||
" (connect living kitchen)\n"
|
||||
" (door front -2)\n"
|
||||
" (windows all :spacing 4))\n"
|
||||
"(floor 1 (room loft (rect 0 0 0.5 1))\n"
|
||||
" (windows all :spacing 4))\n"
|
||||
"(roof flat 6 0 12 8 :level 1)\n"
|
||||
"(roof gable 0 0 6 8 :height 2)\n" },
|
||||
{ "Windowless bathroom (per-room :windows none)",
|
||||
"(shell 12 8 :empty)\n"
|
||||
"(floor 0 (room living (rect 0 0 0.5 1))\n"
|
||||
" (room kitchen (rect 0.5 0 1 0.5))\n"
|
||||
" (room bath (rect 0.5 0.5 1 1) :windows none)\n"
|
||||
" (connect living kitchen)\n"
|
||||
" (connect kitchen bath)\n"
|
||||
" (door front -2)\n"
|
||||
" (windows all :spacing 4))\n"
|
||||
"(roof gable :height 2)\n" },
|
||||
};
|
||||
return ex;
|
||||
}
|
||||
@@ -225,9 +268,16 @@ bool LayoutEval::collectArgs(const SExpr &form, size_t start,
|
||||
while (i < form.list.size()) {
|
||||
const SExpr &a = form.list[i];
|
||||
if (a.isSymbol() && !a.text.empty() && a.text[0] == ':') {
|
||||
if (i + 1 >= form.list.size())
|
||||
return fail(a, "keyword '" + a.text +
|
||||
"' without a value");
|
||||
/* A keyword followed by another keyword (or nothing)
|
||||
* is a bare flag — (shell 12 8 :empty). */
|
||||
if (i + 1 >= form.list.size() ||
|
||||
(form.list[i + 1].isSymbol() &&
|
||||
!form.list[i + 1].text.empty() &&
|
||||
form.list[i + 1].text[0] == ':')) {
|
||||
keywords[a.text] = Value::booleanV(true);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
Value v;
|
||||
if (!eval(form.list[i + 1], v))
|
||||
return false;
|
||||
@@ -531,8 +581,8 @@ bool LayoutEval::evalList(const SExpr &e, Value &out)
|
||||
/* --- verbs (need a plan) --- */
|
||||
static const char *verbs[] = { "shell", "floor", "cut",
|
||||
"door", "windows", "room",
|
||||
"idoor", "roof", "parking",
|
||||
"place", "cell-size" };
|
||||
"idoor", "connect", "roof",
|
||||
"parking", "place", "cell-size" };
|
||||
bool isVerb = false;
|
||||
for (const char *v : verbs)
|
||||
if (op == v)
|
||||
@@ -566,6 +616,8 @@ bool LayoutEval::evalList(const SExpr &e, Value &out)
|
||||
ok = verbWindows(pos, kw, e);
|
||||
else if (op == "idoor")
|
||||
ok = verbIDoor(pos, e);
|
||||
else if (op == "connect")
|
||||
ok = verbIDoor(pos, e);
|
||||
else if (op == "roof")
|
||||
ok = verbRoof(pos, kw, e);
|
||||
out = Value();
|
||||
@@ -603,6 +655,13 @@ bool LayoutEval::verbShell(const std::vector<Value> &pos,
|
||||
"integer");
|
||||
}
|
||||
|
||||
/* (shell w d [:floors n] :empty) — rooms-first mode: declare
|
||||
* only the bounding rect; the cells start empty and the room
|
||||
* verbs claim them (the footprint is the union of the rooms,
|
||||
* walls are derived by the finalize pass). */
|
||||
auto em = kw.find(":empty");
|
||||
m_emptyShell = em != kw.end() && em->second.truthy();
|
||||
|
||||
/* Clamp to the setback-adjusted buildable rect so the shell
|
||||
* can never violate the lot bounds, whatever the script says. */
|
||||
int cx = (int)std::lround(w / m_cellSize);
|
||||
@@ -618,6 +677,11 @@ bool LayoutEval::verbShell(const std::vector<Value> &pos,
|
||||
m_plan->cellSize = m_cellSize;
|
||||
m_plan->cellHeight = m_cellHeight;
|
||||
m_plan->cells.assign((size_t)cx * cz * floors, 0);
|
||||
if (m_emptyShell)
|
||||
m_plan->roomIds.assign((size_t)cx * cz * floors, -1);
|
||||
|
||||
if (m_emptyShell)
|
||||
return true;
|
||||
|
||||
for (int y = 0; y < floors; ++y) {
|
||||
bool top = y == floors - 1;
|
||||
@@ -700,6 +764,25 @@ int LayoutEval::sideIndex(const std::string &side) const
|
||||
return -1;
|
||||
}
|
||||
|
||||
bool LayoutEval::roomBlocksWindows(int x, int z) const
|
||||
{
|
||||
/* True when (x, z) belongs to a room declared with
|
||||
* `:windows none` on the current floor. Works in both shell
|
||||
* modes (legacy rooms record their rects too); rooms must be
|
||||
* declared before the windows verb. */
|
||||
auto fit = m_rooms.find(m_floor);
|
||||
if (fit == m_rooms.end())
|
||||
return false;
|
||||
for (const auto &kv : fit->second) {
|
||||
const RoomRect &rr = kv.second;
|
||||
if (!rr.noWindows)
|
||||
continue;
|
||||
if (x >= rr.x0 && x <= rr.x1 && z >= rr.z0 && z <= rr.z1)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LayoutEval::placeOpening(int x, int z, uint64_t clearMask,
|
||||
uint64_t setBits, const SExpr &at)
|
||||
{
|
||||
@@ -837,6 +920,45 @@ bool LayoutEval::verbDoor(const std::vector<Value> &pos, const SExpr &at)
|
||||
PlanFlags::IntDoorXPos };
|
||||
int x = horizontal ? (si == 2 ? 0 : m_plan->cellsX - 1) : idx;
|
||||
int z = horizontal ? idx : (si == 0 ? m_plan->cellsZ - 1 : 0);
|
||||
|
||||
/* Rooms-first shells can have a non-rect footprint: the centered
|
||||
* door cell may be a courtyard/notch. Scan outward along the
|
||||
* side for the nearest Floor cell (always on the union boundary,
|
||||
* since the row/column is the shell edge) and warn about the
|
||||
* move; an explicit offset keeps the hard error. */
|
||||
if (m_emptyShell && centered &&
|
||||
!(m_plan->at(x, m_floor, z) & PlanFlags::Floor)) {
|
||||
int best = -1;
|
||||
for (int d = 1; d < cells; ++d) {
|
||||
for (int s = -1; s <= 1; s += 2) {
|
||||
int j = idx + s * d;
|
||||
if (j < 0 || j >= cells)
|
||||
continue;
|
||||
int jx = horizontal ?
|
||||
(si == 2 ? 0 : m_plan->cellsX - 1) :
|
||||
j;
|
||||
int jz = horizontal ?
|
||||
j :
|
||||
(si == 0 ? m_plan->cellsZ - 1 : 0);
|
||||
if (m_plan->at(jx, m_floor, jz) &
|
||||
PlanFlags::Floor) {
|
||||
best = j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (best >= 0)
|
||||
break;
|
||||
}
|
||||
if (best < 0)
|
||||
return fail(at, "door: no floor cell on this side");
|
||||
warn("door " + side +
|
||||
" center is outside the footprint; moved to the "
|
||||
"nearest floor cell");
|
||||
idx = best;
|
||||
x = horizontal ? (si == 2 ? 0 : m_plan->cellsX - 1) : idx;
|
||||
z = horizontal ? idx : (si == 0 ? m_plan->cellsZ - 1 : 0);
|
||||
}
|
||||
|
||||
return placeOpening(x, z, wallBit[si] | winBit[si],
|
||||
doorBit[si] | intDoorBit[si], at);
|
||||
}
|
||||
@@ -870,12 +992,65 @@ void LayoutEval::windowsOnSide(int side, float spacing)
|
||||
continue;
|
||||
if (c & doorBit[side]) /* doors win over windows */
|
||||
continue;
|
||||
if (roomBlocksWindows(x, z)) /* :windows none room */
|
||||
continue;
|
||||
c &= ~wallBit[side];
|
||||
c |= winBit[side];
|
||||
lastPlaced = p;
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutEval::windowsUnionBoundary(float spacing)
|
||||
{
|
||||
/* Rooms-first (:empty) shells: cover the full union boundary —
|
||||
* every face of a Floor cell that touches a Floor-less cell (or
|
||||
* the grid edge), including courtyard/notch walls, which the
|
||||
* perimeter-walking windowsOnSide cannot see. Faces are grouped
|
||||
* per wall line and the spacing rule walks each line in order. */
|
||||
static const int dxs[4] = { -1, 1, 0, 0 };
|
||||
static const int dzs[4] = { 0, 0, 1, -1 };
|
||||
static const uint64_t winBit[4] = { PlanFlags::WindowXNeg,
|
||||
PlanFlags::WindowXPos,
|
||||
PlanFlags::WindowZPos,
|
||||
PlanFlags::WindowZNeg };
|
||||
static const uint64_t doorBit[4] = { PlanFlags::DoorXNeg,
|
||||
PlanFlags::DoorXPos,
|
||||
PlanFlags::DoorZPos,
|
||||
PlanFlags::DoorZNeg };
|
||||
for (int d = 0; d < 4; ++d) {
|
||||
const bool xFace = d < 2; /* X faces walk along Z rows */
|
||||
const int lines = xFace ? m_plan->cellsZ : m_plan->cellsX;
|
||||
for (int ln = 0; ln < lines; ++ln) {
|
||||
float lastPlaced = -1.0e30f;
|
||||
const int along = xFace ? m_plan->cellsX :
|
||||
m_plan->cellsZ;
|
||||
for (int i = 0; i < along; ++i) {
|
||||
int x = xFace ? i : ln;
|
||||
int z = xFace ? ln : i;
|
||||
uint64_t &c = m_plan->at(x, m_floor, z);
|
||||
if (!(c & PlanFlags::Floor))
|
||||
continue;
|
||||
int nx = x + dxs[d];
|
||||
int nz = z + dzs[d];
|
||||
if (nx >= 0 && nx < m_plan->cellsX &&
|
||||
nz >= 0 && nz < m_plan->cellsZ &&
|
||||
(m_plan->at(nx, m_floor, nz) &
|
||||
PlanFlags::Floor))
|
||||
continue; /* not a boundary face */
|
||||
if (c & doorBit[d]) /* doors win */
|
||||
continue;
|
||||
if (roomBlocksWindows(x, z))
|
||||
continue;
|
||||
float p = ((float)i + 0.5f) * m_cellSize;
|
||||
if (p - lastPlaced < spacing)
|
||||
continue;
|
||||
c |= winBit[d];
|
||||
lastPlaced = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool LayoutEval::verbWindows(const std::vector<Value> &pos,
|
||||
const std::map<std::string, Value> &kw,
|
||||
const SExpr &at)
|
||||
@@ -899,6 +1074,10 @@ bool LayoutEval::verbWindows(const std::vector<Value> &pos,
|
||||
if (v.type != Value::String)
|
||||
return fail(at, "window side must be a symbol");
|
||||
if (v.str == "all") {
|
||||
if (m_emptyShell) {
|
||||
windowsUnionBoundary(spacing);
|
||||
continue;
|
||||
}
|
||||
for (int side = 0; side < 4; ++side)
|
||||
windowsOnSide(side, spacing);
|
||||
continue;
|
||||
@@ -915,15 +1094,49 @@ bool LayoutEval::verbWindows(const std::vector<Value> &pos,
|
||||
bool LayoutEval::verbRoomRaw(const SExpr &form)
|
||||
{
|
||||
/* (room name (rect x0 z0 x1 z1)) — rect in fractions of the
|
||||
* footprint. Interior walls go on the room's own cells facing
|
||||
* outward (the CellGrid builder offsets each internal wall
|
||||
* plane into its cell, so two rooms sharing a face each keep
|
||||
* their own wall). */
|
||||
* footprint.
|
||||
*
|
||||
* Legacy (filled shell): interior walls go on the room's own
|
||||
* cells facing outward (the CellGrid builder offsets each
|
||||
* internal wall plane into its cell, so two rooms sharing a face
|
||||
* each keep their own wall).
|
||||
*
|
||||
* Rooms-first (:empty shell, phase 4.6): the room CLAIMS its
|
||||
* cells — the footprint is the union of all rooms. Rect edges
|
||||
* snap as lround(frac * cells) and rooms cover the half-open
|
||||
* interval [x0, x1), so neighboring rooms sharing a fraction
|
||||
* (0.5 / 0.5) meet at the same boundary index and can never
|
||||
* overlap or leave a gap. Claiming an already-claimed cell is
|
||||
* a hard error; walls come from the finalize pass. */
|
||||
if (!m_plan->valid())
|
||||
return fail(form, "room before shell");
|
||||
if (form.list.size() != 3 || !form.list[1].isSymbol())
|
||||
if (form.list.size() < 3 || !form.list[1].isSymbol())
|
||||
return fail(form, "room needs (room name (rect x0 z0 x1 "
|
||||
"z1))");
|
||||
if ((form.list.size() - 3) % 2 != 0)
|
||||
return fail(form, "room options come in :keyword value "
|
||||
"pairs");
|
||||
bool noWindows = false;
|
||||
for (size_t i = 3; i < form.list.size(); i += 2) {
|
||||
if (!form.list[i].isSymbol())
|
||||
return fail(form.list[i], "room option must be a "
|
||||
":keyword");
|
||||
const std::string &key = form.list[i].text;
|
||||
if (key != ":windows")
|
||||
return fail(form.list[i], "unknown room option '" +
|
||||
key + "' (only :windows)");
|
||||
if (!form.list[i + 1].isSymbol())
|
||||
return fail(form.list[i + 1], ":windows needs "
|
||||
"none or all");
|
||||
const std::string &val = form.list[i + 1].text;
|
||||
if (val == "none")
|
||||
noWindows = true;
|
||||
else if (val == "all" || val == "default")
|
||||
noWindows = false;
|
||||
else
|
||||
return fail(form.list[i + 1], ":windows must be "
|
||||
"none or all");
|
||||
}
|
||||
const SExpr &rect = form.list[2];
|
||||
if (!rect.isList() || rect.list.size() != 5 ||
|
||||
!rect.list[0].isSymbol("rect"))
|
||||
@@ -934,6 +1147,69 @@ bool LayoutEval::verbRoomRaw(const SExpr &form)
|
||||
!argNumber(rect.list[3], x1) || !argNumber(rect.list[4], z1))
|
||||
return false;
|
||||
|
||||
if (m_emptyShell) {
|
||||
/* Fractions must stay inside the shell rect — no
|
||||
* silent clamping in rooms-first mode. */
|
||||
if (x0 < 0.0 || z0 < 0.0 || x1 > 1.0 || z1 > 1.0)
|
||||
return fail(rect, "room '" + form.list[1].text +
|
||||
"' escapes the shell rect");
|
||||
if (x1 <= x0 || z1 <= z0)
|
||||
return fail(rect, "room rect is empty");
|
||||
|
||||
RoomRect rr;
|
||||
rr.x0 = (int)std::lround(x0 * m_plan->cellsX);
|
||||
rr.z0 = (int)std::lround(z0 * m_plan->cellsZ);
|
||||
rr.x1 = (int)std::lround(x1 * m_plan->cellsX) - 1;
|
||||
rr.z1 = (int)std::lround(z1 * m_plan->cellsZ) - 1;
|
||||
if (rr.x1 < rr.x0 || rr.z1 < rr.z0)
|
||||
return fail(rect, "room '" + form.list[1].text +
|
||||
"' snaps to no cells");
|
||||
if (rr.x0 < 0 || rr.z0 < 0 || rr.x1 >= m_plan->cellsX ||
|
||||
rr.z1 >= m_plan->cellsZ)
|
||||
return fail(rect, "room '" + form.list[1].text +
|
||||
"' escapes the shell rect");
|
||||
|
||||
auto nit = m_roomNameIds.find(form.list[1].text);
|
||||
if (nit == m_roomNameIds.end()) {
|
||||
int id = (int)m_plan->roomNames.size();
|
||||
m_plan->roomNames.push_back(form.list[1].text);
|
||||
nit = m_roomNameIds.emplace(form.list[1].text, id).first;
|
||||
}
|
||||
const int roomId = nit->second;
|
||||
rr.noWindows = noWindows;
|
||||
m_rooms[m_floor][form.list[1].text] = rr;
|
||||
|
||||
const bool top = m_floor == m_plan->floors - 1;
|
||||
for (int z = rr.z0; z <= rr.z1; ++z) {
|
||||
for (int x = rr.x0; x <= rr.x1; ++x) {
|
||||
uint64_t &c = m_plan->at(x, m_floor, z);
|
||||
if (c & PlanFlags::Floor) {
|
||||
int other =
|
||||
m_plan->roomIds
|
||||
[((size_t)m_floor *
|
||||
m_plan->cellsZ + z) *
|
||||
m_plan->cellsX + x];
|
||||
return fail(rect,
|
||||
"room '" +
|
||||
form.list[1].text +
|
||||
"' overlaps room '" +
|
||||
(other >= 0 ?
|
||||
m_plan->roomNames[other] :
|
||||
std::string("?")) +
|
||||
"'");
|
||||
}
|
||||
c |= PlanFlags::Floor;
|
||||
if (top)
|
||||
c |= PlanFlags::Ceiling;
|
||||
m_plan->roomIds[((size_t)m_floor *
|
||||
m_plan->cellsZ + z) *
|
||||
m_plan->cellsX + x] =
|
||||
roomId;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
auto clamp01 = [](double v) { return std::max(0.0, std::min(1.0, v)); };
|
||||
x0 = clamp01(x0);
|
||||
z0 = clamp01(z0);
|
||||
@@ -952,6 +1228,7 @@ bool LayoutEval::verbRoomRaw(const SExpr &form)
|
||||
if (rr.x1 < rr.x0 || rr.z1 < rr.z0)
|
||||
return fail(rect, "room rect maps to no cells");
|
||||
|
||||
rr.noWindows = noWindows;
|
||||
m_rooms[m_floor][form.list[1].text] = rr;
|
||||
|
||||
for (int z = rr.z0; z <= rr.z1; ++z) {
|
||||
@@ -1039,10 +1316,12 @@ bool LayoutEval::verbRoof(const std::vector<Value> &pos,
|
||||
const SExpr &at)
|
||||
{
|
||||
/* (roof flat|normal|normal2|gable|cone|cylinder [x0 z0 x1 z1]
|
||||
* [:height m]). gable is an alias of normal. The optional
|
||||
* rect (shell-frame meters, like cut) limits the roof to part
|
||||
* of the footprint — needed for cut (L/U-shaped) buildings,
|
||||
* where one roof per wing avoids overhanging the cut. */
|
||||
* [:height m] [:level n]). gable is an alias of normal. The
|
||||
* optional rect (shell-frame meters, like cut) limits the roof to
|
||||
* part of the footprint — needed for cut (L/U-shaped) buildings,
|
||||
* where one roof per wing avoids overhanging the cut. :level is
|
||||
* the cell row the roof sits on (default above the top floor) —
|
||||
* a smaller upper floor roofs the lower wing at :level 1. */
|
||||
if (!m_plan->valid())
|
||||
return fail(at, "roof before shell");
|
||||
if (pos.empty())
|
||||
@@ -1102,6 +1381,18 @@ bool LayoutEval::verbRoof(const std::vector<Value> &pos,
|
||||
if (!ok)
|
||||
return fail(at, ":height must be a number");
|
||||
}
|
||||
/* :level n — the cell row the roof sits on (default: above the
|
||||
* top floor). A two-storey building with a smaller upper floor
|
||||
* roofs the lower wing at :level 1. */
|
||||
auto lv = kw.find(":level");
|
||||
if (lv != kw.end()) {
|
||||
bool ok = true;
|
||||
int level = (int)std::lround(num(lv->second, ok));
|
||||
if (!ok || level < 1 || level > m_plan->floors)
|
||||
return fail(at, ":level must be a cell row in 1.."
|
||||
"floors");
|
||||
roof.posY = level;
|
||||
}
|
||||
m_plan->roofs.push_back(roof);
|
||||
return true;
|
||||
}
|
||||
@@ -1196,12 +1487,186 @@ bool LayoutEval::verbPlace(const SExpr &form)
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Rooms-first finalize (:empty shell) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
void LayoutEval::warn(const std::string &msg)
|
||||
{
|
||||
if (m_warnings)
|
||||
m_warnings->push_back(msg);
|
||||
}
|
||||
|
||||
void LayoutEval::finalizeUnionWalls()
|
||||
{
|
||||
/* Derive the walls from the room union: a Floor cell facing a
|
||||
* cell without Floor (or the grid edge) gets an exterior wall on
|
||||
* that side; a Floor cell facing a Floor cell of a DIFFERENT room
|
||||
* gets an interior wall. Openings placed by door/windows/connect
|
||||
* keep their bits (a door/window suppresses the wall on its
|
||||
* side). */
|
||||
static const int dxs[4] = { -1, 1, 0, 0 };
|
||||
static const int dzs[4] = { 0, 0, 1, -1 };
|
||||
static const uint64_t wallBit[4] = { PlanFlags::WallXNeg,
|
||||
PlanFlags::WallXPos,
|
||||
PlanFlags::WallZPos,
|
||||
PlanFlags::WallZNeg };
|
||||
static const uint64_t openBit[4] = { PlanFlags::DoorXNeg |
|
||||
PlanFlags::WindowXNeg,
|
||||
PlanFlags::DoorXPos |
|
||||
PlanFlags::WindowXPos,
|
||||
PlanFlags::DoorZPos |
|
||||
PlanFlags::WindowZPos,
|
||||
PlanFlags::DoorZNeg |
|
||||
PlanFlags::WindowZNeg };
|
||||
static const uint64_t intWallBit[4] = { PlanFlags::IntWallXNeg,
|
||||
PlanFlags::IntWallXPos,
|
||||
PlanFlags::IntWallZPos,
|
||||
PlanFlags::IntWallZNeg };
|
||||
static const uint64_t intOpenBit[4] = { PlanFlags::IntDoorXNeg |
|
||||
PlanFlags::IntWindowXNeg,
|
||||
PlanFlags::IntDoorXPos |
|
||||
PlanFlags::IntWindowXPos,
|
||||
PlanFlags::IntDoorZPos |
|
||||
PlanFlags::IntWindowZPos,
|
||||
PlanFlags::IntDoorZNeg |
|
||||
PlanFlags::IntWindowZNeg };
|
||||
|
||||
auto roomIdAt = [this](int x, int y, int z) -> int {
|
||||
return m_plan->roomIds[((size_t)y * m_plan->cellsZ + z) *
|
||||
m_plan->cellsX + x];
|
||||
};
|
||||
|
||||
for (int y = 0; y < m_plan->floors; ++y) {
|
||||
for (int z = 0; z < m_plan->cellsZ; ++z) {
|
||||
for (int x = 0; x < m_plan->cellsX; ++x) {
|
||||
uint64_t &c = m_plan->at(x, y, z);
|
||||
if (!(c & PlanFlags::Floor))
|
||||
continue;
|
||||
const int roomId = roomIdAt(x, y, z);
|
||||
for (int d = 0; d < 4; ++d) {
|
||||
int nx = x + dxs[d];
|
||||
int nz = z + dzs[d];
|
||||
uint64_t nc = 0;
|
||||
if (nx >= 0 && nx < m_plan->cellsX &&
|
||||
nz >= 0 && nz < m_plan->cellsZ)
|
||||
nc = m_plan->at(nx, y, nz);
|
||||
if (!(nc & PlanFlags::Floor)) {
|
||||
/* Union boundary ->
|
||||
* exterior wall. */
|
||||
if (!(c & openBit[d]))
|
||||
c |= wallBit[d];
|
||||
continue;
|
||||
}
|
||||
/* Shared edge between rooms ->
|
||||
* interior wall (same-room edges
|
||||
* stay open). */
|
||||
if (roomIdAt(nx, y, nz) != roomId &&
|
||||
!(c & intOpenBit[d]) &&
|
||||
!(nc & intOpenBit[d ^ 1]))
|
||||
c |= intWallBit[d];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LayoutEval::warnUnreachableRooms()
|
||||
{
|
||||
/* Flood-fill floor 0 from the entry door cell; rooms with no
|
||||
* connection path to it get a warning (they would be sealed
|
||||
* rooms). Movement is free inside a room and crosses a room
|
||||
* boundary only through an interior door/window. Skipped when
|
||||
* floor 0 has no exterior door at all. */
|
||||
static const int dxs[4] = { -1, 1, 0, 0 };
|
||||
static const int dzs[4] = { 0, 0, 1, -1 };
|
||||
static const uint64_t doorBit[4] = { PlanFlags::DoorXNeg,
|
||||
PlanFlags::DoorXPos,
|
||||
PlanFlags::DoorZPos,
|
||||
PlanFlags::DoorZNeg };
|
||||
static const uint64_t intPassBit[4] = { PlanFlags::IntDoorXNeg |
|
||||
PlanFlags::IntWindowXNeg,
|
||||
PlanFlags::IntDoorXPos |
|
||||
PlanFlags::IntWindowXPos,
|
||||
PlanFlags::IntDoorZPos |
|
||||
PlanFlags::IntWindowZPos,
|
||||
PlanFlags::IntDoorZNeg |
|
||||
PlanFlags::IntWindowZNeg };
|
||||
|
||||
auto roomIdAt = [this](int x, int y, int z) -> int {
|
||||
return m_plan->roomIds[((size_t)y * m_plan->cellsZ + z) *
|
||||
m_plan->cellsX + x];
|
||||
};
|
||||
|
||||
int sx = -1, sz = -1;
|
||||
for (int z = 0; z < m_plan->cellsZ && sx < 0; ++z)
|
||||
for (int x = 0; x < m_plan->cellsX; ++x) {
|
||||
uint64_t c = m_plan->at(x, 0, z);
|
||||
if ((c & PlanFlags::Floor) &&
|
||||
(c & (PlanFlags::DoorXNeg | PlanFlags::DoorXPos |
|
||||
PlanFlags::DoorZPos | PlanFlags::DoorZNeg))) {
|
||||
sx = x;
|
||||
sz = z;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sx < 0)
|
||||
return;
|
||||
|
||||
const size_t plane = (size_t)m_plan->cellsX * m_plan->cellsZ;
|
||||
std::vector<char> seen(plane, 0);
|
||||
std::vector<std::pair<int, int> > stack;
|
||||
stack.push_back({ sx, sz });
|
||||
seen[(size_t)sz * m_plan->cellsX + sx] = 1;
|
||||
while (!stack.empty()) {
|
||||
int x = stack.back().first;
|
||||
int z = stack.back().second;
|
||||
stack.pop_back();
|
||||
uint64_t c = m_plan->at(x, 0, z);
|
||||
const int roomId = roomIdAt(x, 0, z);
|
||||
for (int d = 0; d < 4; ++d) {
|
||||
int nx = x + dxs[d];
|
||||
int nz = z + dzs[d];
|
||||
if (nx < 0 || nx >= m_plan->cellsX || nz < 0 ||
|
||||
nz >= m_plan->cellsZ)
|
||||
continue;
|
||||
uint64_t nc = m_plan->at(nx, 0, nz);
|
||||
if (!(nc & PlanFlags::Floor))
|
||||
continue;
|
||||
size_t ni = (size_t)nz * m_plan->cellsX + nx;
|
||||
if (seen[ni])
|
||||
continue;
|
||||
if (roomIdAt(nx, 0, nz) != roomId &&
|
||||
!(c & intPassBit[d]) && !(nc & intPassBit[d ^ 1]))
|
||||
continue; /* sealed wall */
|
||||
seen[ni] = 1;
|
||||
stack.push_back({ nx, nz });
|
||||
}
|
||||
}
|
||||
|
||||
for (const auto &kv : m_rooms[0]) {
|
||||
const RoomRect &rr = kv.second;
|
||||
bool reached = false;
|
||||
for (int z = rr.z0; z <= rr.z1 && !reached; ++z)
|
||||
for (int x = rr.x0; x <= rr.x1; ++x)
|
||||
if ((m_plan->at(x, 0, z) & PlanFlags::Floor) &&
|
||||
seen[(size_t)z * m_plan->cellsX + x]) {
|
||||
reached = true;
|
||||
break;
|
||||
}
|
||||
if (!reached)
|
||||
warn("room '" + kv.first +
|
||||
"' is unreachable from the entry door");
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* Entry point */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
bool LayoutEval::evaluate(const std::string &sourceOrName, BuildingPlan &out,
|
||||
std::string *error)
|
||||
std::string *error,
|
||||
std::vector<std::string> *warnings)
|
||||
{
|
||||
/* Fresh plan every call: a failed evaluation may have partially
|
||||
* filled the output (a shell defined before a later form
|
||||
@@ -1212,10 +1677,13 @@ bool LayoutEval::evaluate(const std::string &sourceOrName, BuildingPlan &out,
|
||||
m_plan = &out;
|
||||
m_floor = 0;
|
||||
m_inFloor = false;
|
||||
m_emptyShell = false;
|
||||
m_cellSize = LAYOUT_CELL_SIZE;
|
||||
m_cellHeight = LAYOUT_CELL_HEIGHT;
|
||||
m_error.clear();
|
||||
m_warnings = warnings;
|
||||
m_rooms.clear();
|
||||
m_roomNameIds.clear();
|
||||
|
||||
/* Predefined bindings. */
|
||||
std::map<std::string, Value> base;
|
||||
@@ -1228,6 +1696,9 @@ bool LayoutEval::evaluate(const std::string &sourceOrName, BuildingPlan &out,
|
||||
base["min-floors"] = Value::number(m_b.minFloors);
|
||||
base["max-floors"] = Value::number(m_b.maxFloors);
|
||||
base["roof-type"] = Value::number(m_b.roofType);
|
||||
base["density"] = Value::number(m_b.density);
|
||||
base["density-roll"] = Value::number(m_b.densityRoll);
|
||||
base["density-pass"] = Value::booleanV(m_b.densityPass);
|
||||
m_scopes.clear();
|
||||
m_scopes.push_back(std::move(base));
|
||||
|
||||
@@ -1266,6 +1737,10 @@ bool LayoutEval::evaluate(const std::string &sourceOrName, BuildingPlan &out,
|
||||
*error = "layout produced no shell";
|
||||
return false;
|
||||
}
|
||||
if (m_emptyShell) {
|
||||
finalizeUnionWalls();
|
||||
warnUnreachableRooms();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -31,11 +31,24 @@
|
||||
* min-fp max-fp district footprint range, meters
|
||||
* min-floors max-floors district floor range
|
||||
* roof-type edge-side roof override (-1 = none)
|
||||
* density density-roll density-pass — the district density, the
|
||||
* lot's own roll in [0,1) and whether the roll
|
||||
* passed; the roll never drops the lot (low
|
||||
* rolls clamp default layouts to a modest
|
||||
* variant), layouts read these for conditional
|
||||
* features
|
||||
*
|
||||
* Special forms: let, if, cond, dotimes, for-range.
|
||||
* Builtins: + - * / min max clamp abs sqrt, < <= > >= = !=,
|
||||
* and or not, rand rand-range rand-int.
|
||||
* Verbs: shell floor cut door windows room idoor roof parking place.
|
||||
* Verbs: shell floor cut door windows room idoor connect roof parking
|
||||
* place cell-size.
|
||||
*
|
||||
* Rooms-first footprints (phase 4.6): (shell w d [:floors n] :empty)
|
||||
* declares only the bounding rect — rooms claim the cells, the floor
|
||||
* footprint is the union of the room rects, exterior walls are the
|
||||
* union boundary and interior walls the shared edges, so L/U/T and
|
||||
* courtyard shapes need no `cut`. (connect a b) is an alias of idoor.
|
||||
*
|
||||
* Example:
|
||||
* (shell (rand-range min-fp max-fp) (rand-range min-fp max-fp)
|
||||
@@ -63,6 +76,13 @@ struct LayoutBindings {
|
||||
int maxFloors = 1;
|
||||
/** Edge-side roof override (-1 = none, builder default). */
|
||||
int roofType = -1;
|
||||
/** District density outcome for this lot. Density never drops a
|
||||
* lot — the roll flows into the layout as the density-pass /
|
||||
* density-roll / density bindings so layouts can scale features
|
||||
* (extra floors, garages, fences, ...) on it. */
|
||||
float density = 1.0f;
|
||||
float densityRoll = 0.0f;
|
||||
bool densityPass = true;
|
||||
};
|
||||
|
||||
class LayoutLibrary
|
||||
@@ -123,9 +143,13 @@ public:
|
||||
* inline source when it starts with '(' (after whitespace),
|
||||
* otherwise a library/built-in layout name. Returns false and
|
||||
* fills *error on failure; *out is then left invalid.
|
||||
* Non-fatal authoring problems (unreachable rooms, a centered
|
||||
* entry door moved to the nearest floor cell of a non-rect
|
||||
* footprint) are appended to *warnings when given.
|
||||
*/
|
||||
bool evaluate(const std::string &sourceOrName, BuildingPlan &out,
|
||||
std::string *error);
|
||||
std::string *error,
|
||||
std::vector<std::string> *warnings = nullptr);
|
||||
|
||||
private:
|
||||
struct Value {
|
||||
@@ -176,6 +200,10 @@ private:
|
||||
int z0 = 0;
|
||||
int x1 = 0;
|
||||
int z1 = 0;
|
||||
/* (room name (rect ...) :windows none) — suppress exterior
|
||||
* windows on this room's boundary cells (bathrooms, storage
|
||||
* rooms). */
|
||||
bool noWindows = false;
|
||||
};
|
||||
|
||||
bool eval(const SExpr &e, Value &out);
|
||||
@@ -208,9 +236,21 @@ private:
|
||||
bool verbPlace(const SExpr &form);
|
||||
|
||||
int sideIndex(const std::string &side) const;
|
||||
/* True when the cell belongs to a room declared with
|
||||
* :windows none (scans the current floor's room rects, so it
|
||||
* works for both filled-shell and rooms-first layouts; rooms
|
||||
* must be declared BEFORE the windows verb). */
|
||||
bool roomBlocksWindows(int x, int z) const;
|
||||
bool placeOpening(int x, int z, uint64_t clearMask, uint64_t setBits,
|
||||
const SExpr &at);
|
||||
void windowsOnSide(int side, float spacing);
|
||||
void windowsUnionBoundary(float spacing);
|
||||
/* Rooms-first (:empty shell) finalize: derive exterior walls on
|
||||
* the room-union boundary and interior walls between different
|
||||
* rooms, then warn about rooms unreachable from the entry door. */
|
||||
void finalizeUnionWalls();
|
||||
void warnUnreachableRooms();
|
||||
void warn(const std::string &msg);
|
||||
|
||||
LayoutBindings m_b;
|
||||
std::mt19937 *m_rng;
|
||||
@@ -219,14 +259,22 @@ private:
|
||||
BuildingPlan *m_plan;
|
||||
int m_floor; /* current (floor n ...) context */
|
||||
bool m_inFloor = false; /* inside a (floor n ...) form */
|
||||
/* True after (shell ... :empty): the shell declares only the
|
||||
* bounding rect; rooms claim the cells and the finalize pass
|
||||
* derives the walls from the room union. */
|
||||
bool m_emptyShell = false;
|
||||
/* Grid metrics in meters (the cell-size verb; copied into the
|
||||
* plan by verbShell). */
|
||||
float m_cellSize = LAYOUT_CELL_SIZE;
|
||||
float m_cellHeight = LAYOUT_CELL_HEIGHT;
|
||||
std::string m_error;
|
||||
std::vector<std::string> *m_warnings = nullptr;
|
||||
std::vector<std::map<std::string, Value> > m_scopes;
|
||||
/* rooms per floor: floor -> (name -> rect) */
|
||||
std::map<int, std::map<std::string, RoomRect> > m_rooms;
|
||||
/* roomIds index for a room name of the current evaluation:
|
||||
* name -> index into BuildingPlan::roomNames. */
|
||||
std::map<std::string, int> m_roomNameIds;
|
||||
};
|
||||
|
||||
} /* namespace LayoutLib */
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <iostream>
|
||||
#include <filesystem>
|
||||
#include <cstdlib>
|
||||
#include "EditorApp.hpp"
|
||||
#include "ProjectConfig.hpp"
|
||||
#include "systems/SceneSerializer.hpp"
|
||||
@@ -33,9 +34,25 @@ struct GenerateTownFrameListener : public Ogre::FrameListener {
|
||||
TownGen::TownParams params;
|
||||
int frames = 0;
|
||||
int result = -1;
|
||||
/* Debug hook (--regen-lot-params=<edge>,<side>): after generation,
|
||||
* exercise TownGenSystem::regenerateByLotParams like the editor
|
||||
* button does, then let the grids rebuild for a few frames. */
|
||||
int regenEdge = -1;
|
||||
int regenSide = -1;
|
||||
int regenWait = 0;
|
||||
bool useAreas = false;
|
||||
/* TOWNGEN_REPEAT_GENERATE=1: run the generation a second time a
|
||||
* few frames after the first — the headless equivalent of
|
||||
* pressing "Generate All Areas" twice (the settlement material
|
||||
* is really created, torn down and recreated between runs). */
|
||||
bool repeat = false;
|
||||
bool repeated = false;
|
||||
GenerateTownFrameListener(Ogre::Root *r,
|
||||
const TownGen::TownParams &p)
|
||||
: root(r), params(p) {}
|
||||
: root(r), params(p)
|
||||
{
|
||||
repeat = std::getenv("TOWNGEN_REPEAT_GENERATE") != nullptr;
|
||||
}
|
||||
bool frameRenderingQueued(const Ogre::FrameEvent &) override
|
||||
{
|
||||
if (++frames < 5)
|
||||
@@ -54,13 +71,63 @@ struct GenerateTownFrameListener : public Ogre::FrameListener {
|
||||
ts ? ts->getTownGenSystem() : nullptr;
|
||||
if (!townGen)
|
||||
return true; /* terrain not active yet */
|
||||
std::string error;
|
||||
int count = townGen->generate(params, &error);
|
||||
std::cout << "generate-town: " << count << " buildings"
|
||||
<< (error.empty() ? "" : " (" + error + ")")
|
||||
<< std::endl;
|
||||
result = 0;
|
||||
root->queueEndRendering();
|
||||
if (result == -1) {
|
||||
std::string error;
|
||||
int count = useAreas ? townGen->generateAreas(&error) :
|
||||
townGen->generate(params, &error);
|
||||
std::cout << "generate-town: " << count << " buildings"
|
||||
<< (error.empty() ? "" : " (" + error + ")")
|
||||
<< std::endl;
|
||||
for (const std::string &w : townGen->getLastWarnings())
|
||||
std::cout << "generate-town warning: " << w
|
||||
<< std::endl;
|
||||
result = -2; /* generated; maybe regen next */
|
||||
return true;
|
||||
}
|
||||
if (result == -2) {
|
||||
if (regenEdge < 0) {
|
||||
if (repeat && !repeated) {
|
||||
regenWait = 0;
|
||||
result = -4;
|
||||
return true;
|
||||
}
|
||||
result = 0;
|
||||
root->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
if (++regenWait < 10)
|
||||
return true; /* let the grids build */
|
||||
std::string error;
|
||||
int n = townGen->regenerateByLotParams(regenEdge,
|
||||
regenSide, &error);
|
||||
std::cout << "regen-lot-params: rebuilt " << n
|
||||
<< (error.empty() ? "" : " (" + error + ")")
|
||||
<< std::endl;
|
||||
for (const std::string &w : townGen->getLastWarnings())
|
||||
std::cout << "regen-lot-params warning: " << w
|
||||
<< std::endl;
|
||||
result = -3;
|
||||
return true;
|
||||
}
|
||||
if (result == -3) {
|
||||
if (++regenWait < 25)
|
||||
return true; /* let the rebuilt grids build */
|
||||
if (repeat && !repeated) {
|
||||
regenWait = 0;
|
||||
result = -4;
|
||||
return true;
|
||||
}
|
||||
result = 0;
|
||||
root->queueEndRendering();
|
||||
return true;
|
||||
}
|
||||
if (result == -4) {
|
||||
if (++regenWait < 10)
|
||||
return true; /* let the grids/materials settle */
|
||||
repeated = true;
|
||||
result = -1; /* generate again */
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@@ -111,6 +178,9 @@ int main(int argc, char *argv[])
|
||||
int terrainTestIterations = 0;
|
||||
bool generateTown = false;
|
||||
TownGen::TownParams genTownParams;
|
||||
int genTownRegenEdge = -1;
|
||||
int genTownRegenSide = 0;
|
||||
bool genTownUseAreas = false;
|
||||
Ogre::String sceneFile;
|
||||
Ogre::String projectDir;
|
||||
for (int i = 1; i < argc; i++) {
|
||||
@@ -151,6 +221,22 @@ int main(int argc, char *argv[])
|
||||
else
|
||||
genTownParams.district =
|
||||
TownGen::District::Village;
|
||||
} else if (arg == "--generate-town-areas") {
|
||||
/* Mirrors the "Generate All Areas" button. */
|
||||
generateTown = true;
|
||||
genTownUseAreas = true;
|
||||
} else if (arg.find("--regen-lot-params=") == 0) {
|
||||
/* --regen-lot-params=<edge>,<side>: after
|
||||
* --generate-town, regenerate the buildings of
|
||||
* that edge side (editor Regenerate button). */
|
||||
Ogre::String val = arg.substr(19);
|
||||
size_t comma = val.find(',');
|
||||
genTownRegenEdge = Ogre::StringConverter::parseInt(
|
||||
val.substr(0, comma), -1);
|
||||
if (comma != Ogre::String::npos)
|
||||
genTownRegenSide =
|
||||
Ogre::StringConverter::parseInt(
|
||||
val.substr(comma + 1), 0);
|
||||
} else if (arg.find("--run-terrain-tests=") == 0) {
|
||||
Ogre::String val = arg.substr(20);
|
||||
terrainTestIterations = Ogre::StringConverter::parseInt(
|
||||
@@ -261,6 +347,9 @@ int main(int argc, char *argv[])
|
||||
app.getRoot()->addFrameListener(&exitListener);
|
||||
GenerateTownFrameListener genTownListener(app.getRoot(),
|
||||
genTownParams);
|
||||
genTownListener.regenEdge = genTownRegenEdge;
|
||||
genTownListener.regenSide = genTownRegenSide;
|
||||
genTownListener.useAreas = genTownUseAreas;
|
||||
if (generateTown)
|
||||
app.getRoot()->addFrameListener(&genTownListener);
|
||||
app.getRoot()->startRendering();
|
||||
|
||||
@@ -200,6 +200,13 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
std::mt19937 rng(params.seed);
|
||||
std::uniform_real_distribution<float> unit(0.0f, 1.0f);
|
||||
std::map<std::string, int> streetNumbers;
|
||||
/* Phase 4.6: every lot draws from its own RNG stream seeded
|
||||
* pageSeed ^ hash(lot centroid) — editing one lot's params or
|
||||
* layout can never shift another lot's draws, so single-building
|
||||
* regeneration is exact (street NUMBERING still depends on how
|
||||
* many lots of the same street built before it). pageSeed is the
|
||||
* town seed, re-derived per page when paged (see below). */
|
||||
uint32_t pageSeed = params.seed;
|
||||
|
||||
/* Phase 4.5 item 5: page scoping for streamed building
|
||||
* generation. With a mapper the lots are processed grouped by
|
||||
@@ -258,7 +265,7 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
if (n < 3)
|
||||
continue;
|
||||
|
||||
/* Page boundary: reseed the stream and restart street
|
||||
/* Page boundary: re-derive the page seed and restart street
|
||||
* numbering (see the paging note above). Placed after
|
||||
* the degenerate-lot skip so skipped lots never disturb
|
||||
* the stream. */
|
||||
@@ -269,7 +276,7 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
pageStarted = true;
|
||||
uint32_t h = (uint32_t)(curPageX * 73856093L) ^
|
||||
(uint32_t)(curPageY * 19349663L);
|
||||
rng.seed(params.seed ^ h);
|
||||
pageSeed = params.seed ^ h;
|
||||
streetNumbers.clear();
|
||||
}
|
||||
|
||||
@@ -359,7 +366,10 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
* be reversed); the side the centroid lies on picks
|
||||
* lotLeft/lotRight. */
|
||||
const RoadLotParams *lotParams = nullptr;
|
||||
for (const RoadEdge &e : graph.edges) {
|
||||
int lotParamsEdge = -1;
|
||||
int lotParamsSide = -1;
|
||||
for (size_t ei = 0; ei < graph.edges.size(); ++ei) {
|
||||
const RoadEdge &e = graph.edges[ei];
|
||||
const RoadNode *na = graph.findNodeById(e.nodeA);
|
||||
const RoadNode *nb = graph.findNodeById(e.nodeB);
|
||||
if (!na || !nb)
|
||||
@@ -380,6 +390,8 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
bool lotOnLeft =
|
||||
gleft.dotProduct(lot.centroid - edgeMid) > 0.0f;
|
||||
lotParams = lotOnLeft ? &e.lotLeft : &e.lotRight;
|
||||
lotParamsEdge = (int)ei;
|
||||
lotParamsSide = lotOnLeft ? 0 : 1;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -490,16 +502,40 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Phase 4.6: per-lot RNG stream — reseed from the
|
||||
* page seed and a stable lot+strip hash (centroid in
|
||||
* cm, vertex count, strip index) before the first
|
||||
* draw. Lots no longer share a stream, so a
|
||||
* layout/params edit on one lot can never shift
|
||||
* another lot's building. */
|
||||
{
|
||||
uint32_t hx = (uint32_t)(int32_t)std::llround(
|
||||
lot.centroid.x * 100.0f);
|
||||
uint32_t hz = (uint32_t)(int32_t)std::llround(
|
||||
lot.centroid.z * 100.0f);
|
||||
uint32_t lotH = hx * 73856093u ^ hz * 19349663u ^
|
||||
(uint32_t)n * 83492791u ^
|
||||
(uint32_t)si * 2654435761u;
|
||||
rng.seed(pageSeed ^ lotH);
|
||||
}
|
||||
|
||||
/* Seeded draws: density first (here), then the layout
|
||||
* evaluation draws width/depth/floors through
|
||||
* rand-range/ rand-int — the default layouts keep
|
||||
* the legacy width, depth, floors draw order, so an
|
||||
* all-default unsplit town is bit-identical to the
|
||||
* pre-DSL generator. */
|
||||
if (unit(rng) > density) {
|
||||
result.skippedDensity++;
|
||||
continue;
|
||||
}
|
||||
* pre-DSL generator when density == 1. The roll
|
||||
* NEVER drops the lot (a lost roll used to skip it
|
||||
* silently, which read as "generation ignored my
|
||||
* layout") — instead the roll flows into the
|
||||
* layout bindings (density / density-roll /
|
||||
* density-pass) and a lost roll clamps a DEFAULT
|
||||
* layout to a modest variant (small footprint,
|
||||
* minimum floors). Explicit edge-side layouts are
|
||||
* build intent and react to density-pass
|
||||
* themselves. */
|
||||
float densityRoll = unit(rng);
|
||||
const bool densityPass = densityRoll <= density;
|
||||
|
||||
/* Phase 4.5 item 2: evaluate the layout DSL into a
|
||||
* BuildingPlan (edge-side layout string — inline
|
||||
@@ -515,6 +551,18 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
lb.minFloors = ldp.minFloors;
|
||||
lb.maxFloors = ldp.maxFloors;
|
||||
lb.roofType = roofType;
|
||||
lb.density = density;
|
||||
lb.densityRoll = densityRoll;
|
||||
lb.densityPass = densityPass;
|
||||
if (!densityPass && layout.empty()) {
|
||||
/* Modest default variant: small end of the
|
||||
* footprint range, minimum floors. */
|
||||
lb.maxFp = ldp.minFootprint +
|
||||
0.35f * (ldp.maxFootprint -
|
||||
ldp.minFootprint);
|
||||
lb.maxFloors = ldp.minFloors;
|
||||
result.densityModest++;
|
||||
}
|
||||
const char *defLayout =
|
||||
defaultLayoutForDistrict(params.district);
|
||||
const std::string layoutSrc =
|
||||
@@ -522,19 +570,22 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
LayoutLib::BuildingPlan plan;
|
||||
std::string layoutErr;
|
||||
{
|
||||
std::vector<std::string> layoutWarns;
|
||||
LayoutLib::LayoutEval eval(lb, &rng, layouts);
|
||||
if (!eval.evaluate(layoutSrc, plan,
|
||||
&layoutErr)) {
|
||||
&layoutErr, &layoutWarns)) {
|
||||
result.warnings.push_back(
|
||||
"lot " + std::to_string(li) +
|
||||
" strip " + std::to_string(si) +
|
||||
" layout '" + layoutSrc +
|
||||
"': " + layoutErr +
|
||||
" (district default used)");
|
||||
layoutWarns.clear();
|
||||
LayoutLib::LayoutEval fallback(
|
||||
lb, &rng, layouts);
|
||||
if (!fallback.evaluate(defLayout, plan,
|
||||
&layoutErr)) {
|
||||
&layoutErr,
|
||||
&layoutWarns)) {
|
||||
result.warnings.push_back(
|
||||
"lot " +
|
||||
std::to_string(li) +
|
||||
@@ -545,6 +596,11 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const std::string &w : layoutWarns)
|
||||
result.warnings.push_back(
|
||||
"lot " + std::to_string(li) +
|
||||
" strip " + std::to_string(si) +
|
||||
": " + w);
|
||||
}
|
||||
float width = plan.cellsX * plan.cellSize;
|
||||
float depth = plan.cellsZ * plan.cellSize;
|
||||
@@ -587,6 +643,8 @@ TownResult generateTown(const RoadGraph &graph, const TownParams ¶ms,
|
||||
|
||||
TownBuilding b;
|
||||
b.lotIndex = (int)li;
|
||||
b.paramsEdge = lotParamsEdge;
|
||||
b.paramsSide = lotParamsSide;
|
||||
b.center = center2d;
|
||||
b.center.y = floorY;
|
||||
b.yaw = std::atan2(-v.x, -v.z);
|
||||
|
||||
@@ -108,6 +108,12 @@ struct TownBuilding {
|
||||
int doorCellX = -1;
|
||||
/** "<street> <n>" or "Lot <lotIndex+1>". */
|
||||
std::string address;
|
||||
/** Frontage edge (index into RoadGraph::edges) and side
|
||||
* (0 = lotLeft, 1 = lotRight) whose lot params this building
|
||||
* took, -1 when unmatched; lets the editor regenerate exactly
|
||||
* the buildings affected by an edge-side params edit. */
|
||||
int paramsEdge = -1;
|
||||
int paramsSide = -1;
|
||||
};
|
||||
|
||||
/** Named curb point in front of a building's door (pickup/dropoff,
|
||||
@@ -121,8 +127,11 @@ struct TownPoi {
|
||||
struct TownResult {
|
||||
std::vector<TownBuilding> buildings;
|
||||
std::vector<TownPoi> pois;
|
||||
/** Suitable-but-empty lots (density roll). */
|
||||
int skippedDensity = 0;
|
||||
/** Lots whose density roll failed and whose DEFAULT layout was
|
||||
* clamped to the modest variant (explicit layouts react to the
|
||||
* density-pass binding themselves). The roll never drops a
|
||||
* lot. */
|
||||
int densityModest = 0;
|
||||
/** Lots too small for the district footprint. */
|
||||
int skippedTooSmall = 0;
|
||||
/** Lots below TownParams::waterLevel. */
|
||||
|
||||
@@ -247,7 +247,24 @@ void ProceduralMeshSystem::destroyMesh(TriangleBufferComponent& tb)
|
||||
void ProceduralMeshSystem::convertToMesh(flecs::entity entity, TriangleBufferComponent& tb)
|
||||
{
|
||||
if (!tb.buffer) return;
|
||||
|
||||
|
||||
/* If a procedural material is referenced but not created yet (it
|
||||
* is built by ProceduralMaterialSystem on a later update), defer
|
||||
* the conversion: binding now would bake in the default white
|
||||
* material and nothing would rebind it afterwards (white
|
||||
* foundation blocks right after a full town generate; single-
|
||||
* building regen reuses the already-created material, so it was
|
||||
* fine). */
|
||||
if (tb.materialEntity.is_alive() &&
|
||||
tb.materialEntity.has<ProceduralMaterialComponent>()) {
|
||||
const auto& material =
|
||||
tb.materialEntity.get<ProceduralMaterialComponent>();
|
||||
if (!material.created) {
|
||||
tb.dirty = true; /* retry on the next update */
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply UV mapping if texture and rect are specified
|
||||
if (tb.textureEntity.is_alive() && tb.textureEntity.has<ProceduralTextureComponent>()) {
|
||||
auto& texture = tb.textureEntity.get_mut<ProceduralTextureComponent>();
|
||||
|
||||
@@ -3013,10 +3013,12 @@ void TerrainSystem::activate(TerrainComponent &tc, TransformComponent &xform,
|
||||
m_roadSystem->setTerrainEntity(m_world.entity(m_terrainEntityId));
|
||||
m_roadSystem->setVisualAidsVisible(m_roadEditMode);
|
||||
|
||||
/* Town generation builds on the road graph; it holds no OGRE
|
||||
* objects of its own, so no teardown is needed on deactivate. */
|
||||
/* Town generation builds on the road graph. It needs the physics
|
||||
* wrapper for destroyEntityDeep() (synchronous teardown of
|
||||
* building rigid bodies on regen/clear). */
|
||||
m_townGenSystem = std::make_unique<TownGenSystem>(m_world,
|
||||
m_sceneMgr);
|
||||
m_sceneMgr,
|
||||
m_physics);
|
||||
|
||||
if (m_physics)
|
||||
m_physics->setBodyDrawFilter(&mBodyDrawFilter);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "../components/EntityName.hpp"
|
||||
#include "../components/EditorMarker.hpp"
|
||||
#include "../components/Primitive.hpp"
|
||||
#include "../components/Renderable.hpp"
|
||||
#include "../components/TriangleBuffer.hpp"
|
||||
#include "../components/RigidBody.hpp"
|
||||
#include "../components/PhysicsCollider.hpp"
|
||||
@@ -16,8 +17,11 @@
|
||||
#include "../components/ProceduralTexture.hpp"
|
||||
#include "../components/WaterPhysics.hpp"
|
||||
#include "../components/RoadGraph.hpp"
|
||||
#include "../physics/physics.h"
|
||||
#include "../roadlib/RoadNetworkGen.hpp"
|
||||
|
||||
#include <OgreRTShaderSystem.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cfloat>
|
||||
#include <array>
|
||||
@@ -96,13 +100,108 @@ planFloorRuns(const LayoutLib::BuildingPlan &plan, int floor)
|
||||
return runs;
|
||||
}
|
||||
|
||||
TownGenSystem::TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr)
|
||||
TownGenSystem::TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
JoltPhysicsWrapper *physics)
|
||||
: m_world(world)
|
||||
, m_sceneMgr(sceneMgr)
|
||||
, m_physics(physics)
|
||||
{
|
||||
m_terrainSystem = TerrainSystem::getInstance();
|
||||
}
|
||||
|
||||
void TownGenSystem::destroyEntityDeep(flecs::entity entity)
|
||||
{
|
||||
if (!entity.is_alive())
|
||||
return;
|
||||
|
||||
/* Children first (snapshot: teardown mutates the child list). */
|
||||
std::vector<flecs::entity> children;
|
||||
entity.children([&](flecs::entity c) { children.push_back(c); });
|
||||
for (flecs::entity c : children)
|
||||
destroyEntityDeep(c);
|
||||
|
||||
if (entity.has<RigidBodyComponent>()) {
|
||||
auto &rb = entity.get_mut<RigidBodyComponent>();
|
||||
if (m_physics && rb.bodyCreated && !rb.bodyID.IsInvalid()) {
|
||||
m_physics->removeBody(rb.bodyID);
|
||||
m_physics->destroyBody(rb.bodyID);
|
||||
rb.bodyCreated = false;
|
||||
rb.bodyID = JPH::BodyID();
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.has<TriangleBufferComponent>()) {
|
||||
auto &tb = entity.get_mut<TriangleBufferComponent>();
|
||||
if (tb.ogreEntity) {
|
||||
m_sceneMgr->destroyEntity(tb.ogreEntity);
|
||||
tb.ogreEntity = nullptr;
|
||||
}
|
||||
if (!tb.meshName.empty()) {
|
||||
try {
|
||||
Ogre::MeshManager::getSingleton().remove(
|
||||
tb.meshName);
|
||||
} catch (...) {
|
||||
}
|
||||
tb.meshName.clear();
|
||||
}
|
||||
}
|
||||
|
||||
if (entity.has<RenderableComponent>()) {
|
||||
auto &rc = entity.get_mut<RenderableComponent>();
|
||||
if (rc.entity) {
|
||||
m_sceneMgr->destroyEntity(rc.entity);
|
||||
rc.entity = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Settlement material/texture children of a town container (no
|
||||
* system sweeps these; Ogre shared pointers make the remove
|
||||
* order between the two harmless). */
|
||||
if (entity.has<ProceduralMaterialComponent>()) {
|
||||
auto &pm = entity.get_mut<ProceduralMaterialComponent>();
|
||||
if (pm.ogreMaterial) {
|
||||
try {
|
||||
/* RTSS keeps stale cache entries keyed by
|
||||
* material name unless the techniques are
|
||||
* removed first. */
|
||||
Ogre::RTShader::ShaderGenerator *sg = Ogre::
|
||||
RTShader::ShaderGenerator::
|
||||
getSingletonPtr();
|
||||
if (sg)
|
||||
sg->removeAllShaderBasedTechniques(
|
||||
*pm.ogreMaterial);
|
||||
Ogre::MaterialManager::getSingleton().remove(
|
||||
pm.ogreMaterial);
|
||||
} catch (...) {
|
||||
}
|
||||
pm.ogreMaterial.reset();
|
||||
}
|
||||
}
|
||||
if (entity.has<ProceduralTextureComponent>()) {
|
||||
auto &pt = entity.get_mut<ProceduralTextureComponent>();
|
||||
if (!pt.textureName.empty()) {
|
||||
try {
|
||||
Ogre::TextureManager::getSingleton().remove(
|
||||
pt.textureName);
|
||||
} catch (...) {
|
||||
}
|
||||
pt.textureName.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/* Scene node last: destroying it only detaches attached objects,
|
||||
* and child nodes die with their parent. */
|
||||
if (entity.has<TransformComponent>()) {
|
||||
auto &t = entity.get_mut<TransformComponent>();
|
||||
if (t.node) {
|
||||
m_sceneMgr->destroySceneNode(t.node);
|
||||
t.node = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
entity.destruct();
|
||||
}
|
||||
|
||||
int TownGenSystem::countTowns() const
|
||||
{
|
||||
int count = 0;
|
||||
@@ -125,7 +224,9 @@ int TownGenSystem::clear()
|
||||
towns.push_back(e);
|
||||
});
|
||||
for (flecs::entity e : towns)
|
||||
e.destruct();
|
||||
destroyEntityDeep(e);
|
||||
m_containerParams.clear();
|
||||
m_containerArea.clear();
|
||||
return (int)towns.size();
|
||||
}
|
||||
|
||||
@@ -255,6 +356,7 @@ int TownGenSystem::generate(const TownGen::TownParams ¶ms,
|
||||
/* Replace any previously generated town. */
|
||||
clear();
|
||||
|
||||
m_lastWarnings = result.warnings;
|
||||
for (const std::string &w : result.warnings)
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TownGenSystem: " + w);
|
||||
@@ -271,6 +373,7 @@ int TownGenSystem::generate(const TownGen::TownParams ¶ms,
|
||||
std::string("TownGen ") + dp.name + " seed " +
|
||||
std::to_string(p.seed);
|
||||
createTownContainer(containerName, result, dp, p.seed);
|
||||
m_containerParams[containerName] = params;
|
||||
|
||||
return (int)result.buildings.size();
|
||||
}
|
||||
@@ -304,6 +407,7 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
|
||||
/* Replace any previously generated town(s). */
|
||||
clear();
|
||||
m_lastWarnings.clear();
|
||||
|
||||
int total = 0;
|
||||
for (size_t ai = 0; ai < graph->townAreas.size(); ++ai) {
|
||||
@@ -319,17 +423,20 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
TownGen::generateTown(*graph, p, sampler, &layouts,
|
||||
&graph->townAreas, (int)ai,
|
||||
scopePtr);
|
||||
for (const std::string &w : result.warnings)
|
||||
for (const std::string &w : result.warnings) {
|
||||
m_lastWarnings.push_back(area.name + ": " + w);
|
||||
Ogre::LogManager::getSingleton().logMessage(
|
||||
"TownGenSystem[" + area.name + "]: " + w);
|
||||
}
|
||||
|
||||
if (result.buildings.empty())
|
||||
continue;
|
||||
|
||||
const TownGen::DistrictParams &dp =
|
||||
TownGen::districtParams(p.district);
|
||||
createTownContainer("TownGen " + area.name, result, dp,
|
||||
p.seed);
|
||||
std::string containerName = "TownGen " + area.name;
|
||||
createTownContainer(containerName, result, dp, p.seed);
|
||||
m_containerArea[containerName] = (int)ai;
|
||||
total += (int)result.buildings.size();
|
||||
}
|
||||
|
||||
@@ -341,6 +448,172 @@ int TownGenSystem::generateAreas(std::string *error)
|
||||
return total;
|
||||
}
|
||||
|
||||
int TownGenSystem::regenerateBuilding(const std::string &address,
|
||||
const std::string &genMode,
|
||||
std::string *error)
|
||||
{
|
||||
return regenerateMatching(
|
||||
[&address](const TownGen::TownBuilding &b) {
|
||||
return b.address == address;
|
||||
},
|
||||
genMode,
|
||||
"no generated building '" + address +
|
||||
"' (the lot params may have changed); "
|
||||
"regenerate the whole town",
|
||||
error);
|
||||
}
|
||||
|
||||
int TownGenSystem::regenerateByLotParams(int edgeIndex, int side,
|
||||
std::string *error)
|
||||
{
|
||||
return regenerateMatching(
|
||||
[edgeIndex, side](const TownGen::TownBuilding &b) {
|
||||
return b.paramsEdge == edgeIndex &&
|
||||
b.paramsSide == side;
|
||||
},
|
||||
"exteriorOnly",
|
||||
"no generated buildings use the params of edge " +
|
||||
std::to_string(edgeIndex) +
|
||||
(side == 0 ? " left" : " right") +
|
||||
" (generate the town first)",
|
||||
error);
|
||||
}
|
||||
|
||||
int TownGenSystem::regenerateMatching(
|
||||
const std::function<bool(const TownGen::TownBuilding &)> &match,
|
||||
const std::string &genMode, const std::string ¬Found,
|
||||
std::string *error)
|
||||
{
|
||||
TerrainSystem *ts = m_terrainSystem ?
|
||||
m_terrainSystem :
|
||||
TerrainSystem::getInstance();
|
||||
const RoadGraph *graph = nullptr;
|
||||
float waterY = -FLT_MAX;
|
||||
RoadNetworkGen::HeightSampler sampler;
|
||||
if (!setupTownRun(ts, &graph, &waterY, &sampler, m_world, error))
|
||||
return 0;
|
||||
|
||||
/* Candidate containers with their generation parameters: the
|
||||
* area's params re-read from the graph (edited lot params live
|
||||
* there), or the recorded free-form generate() params. Streamed
|
||||
* or stale containers have neither and are skipped. */
|
||||
struct ContainerRun {
|
||||
flecs::entity entity;
|
||||
std::string name;
|
||||
TownGen::TownParams params;
|
||||
const std::vector<RoadTownArea> *areas;
|
||||
int areaIndex;
|
||||
};
|
||||
std::vector<ContainerRun> runs;
|
||||
m_world.query<EntityNameComponent, TownComponent>().each(
|
||||
[&](flecs::entity c, EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name.rfind("TownGen ", 0) != 0)
|
||||
return;
|
||||
TownGen::TownParams p;
|
||||
const std::vector<RoadTownArea> *areas = nullptr;
|
||||
int areaIndex = -1;
|
||||
auto ait = m_containerArea.find(name.name);
|
||||
auto pit = m_containerParams.find(name.name);
|
||||
if (ait != m_containerArea.end() && ait->second >= 0 &&
|
||||
ait->second < (int)graph->townAreas.size()) {
|
||||
const RoadTownArea &area =
|
||||
graph->townAreas[ait->second];
|
||||
p.district =
|
||||
(TownGen::District)(int)area.district;
|
||||
p.seed = area.seed;
|
||||
p.densityScale = area.densityScale;
|
||||
areas = &graph->townAreas;
|
||||
areaIndex = ait->second;
|
||||
} else if (pit != m_containerParams.end()) {
|
||||
p = pit->second;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
p.waterLevel = waterY;
|
||||
runs.push_back({ c, name.name, p, areas, areaIndex });
|
||||
});
|
||||
if (runs.empty()) {
|
||||
if (error)
|
||||
*error = "no generated town with known parameters; "
|
||||
"generate the town first";
|
||||
return 0;
|
||||
}
|
||||
|
||||
LayoutLib::LayoutLibrary layouts = loadLayoutLibrary();
|
||||
|
||||
TownGen::PageScope scope;
|
||||
const TownGen::PageScope *scopePtr = nullptr;
|
||||
if (ts->getStreamingActive() && ts->getTerrainGroup()) {
|
||||
scope.mapper = makePageMapper(ts);
|
||||
scopePtr = &scope;
|
||||
}
|
||||
|
||||
m_lastWarnings.clear();
|
||||
int rebuilt = 0;
|
||||
for (const ContainerRun &run : runs) {
|
||||
TownGen::TownResult result = TownGen::generateTown(
|
||||
*graph, run.params, sampler, &layouts, run.areas,
|
||||
run.areaIndex, scopePtr);
|
||||
for (const std::string &w : result.warnings)
|
||||
m_lastWarnings.push_back(run.name + ": " + w);
|
||||
|
||||
for (size_t i = 0; i < result.buildings.size(); ++i) {
|
||||
const TownGen::TownBuilding &tb = result.buildings[i];
|
||||
if (!match(tb))
|
||||
continue;
|
||||
|
||||
/* The entity to replace: the container child named
|
||||
* by the address (per-lot RNG streams keep the
|
||||
* address stable across single-lot edits; street
|
||||
* numbering shifts are the documented exception). */
|
||||
flecs::entity oldBuilding;
|
||||
run.entity.children([&](flecs::entity child) {
|
||||
if (oldBuilding.is_alive() ||
|
||||
!child.has<CellGridComponent>() ||
|
||||
!child.has<EntityNameComponent>())
|
||||
return;
|
||||
if (child.get<EntityNameComponent>().name ==
|
||||
tb.address)
|
||||
oldBuilding = child;
|
||||
});
|
||||
if (!oldBuilding.is_alive())
|
||||
continue;
|
||||
|
||||
std::vector<const TownGen::TownPoi *> pois;
|
||||
for (const TownGen::TownPoi &poi : result.pois)
|
||||
if (poi.building == (int)i)
|
||||
pois.push_back(&poi);
|
||||
|
||||
flecs::entity material;
|
||||
if (run.entity.has<TownComponent>())
|
||||
material = run.entity.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 = "container '" + run.name +
|
||||
"' lost its settlement material";
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
destroyEntityDeep(oldBuilding);
|
||||
createBuilding(run.entity, material, texture, tb, pois,
|
||||
genMode);
|
||||
++rebuilt;
|
||||
}
|
||||
}
|
||||
|
||||
if (rebuilt == 0 && error)
|
||||
*error = notFound;
|
||||
return rebuilt;
|
||||
}
|
||||
|
||||
flecs::entity
|
||||
TownGenSystem::createTownContainer(const std::string &containerName,
|
||||
const TownGen::TownResult &result,
|
||||
@@ -376,7 +649,8 @@ TownGenSystem::createTownContainer(const std::string &containerName,
|
||||
EntityNameComponent(containerName + " material"));
|
||||
material.add<EditorMarkerComponent>();
|
||||
ProceduralMaterialComponent mat;
|
||||
mat.materialName = containerName + " material";
|
||||
/* Unique resource name per creation — see the texture above. */
|
||||
mat.materialName = containerName + " material " + mat.materialId;
|
||||
mat.diffuseTextureId =
|
||||
texture.get<ProceduralTextureComponent>().textureId;
|
||||
mat.diffuseTextureEntity = texture;
|
||||
@@ -444,7 +718,7 @@ void TownGenSystem::update()
|
||||
if (loaded.find(it->first) == loaded.end()) {
|
||||
for (flecs::entity c : it->second.containers)
|
||||
if (c.is_alive())
|
||||
c.destruct();
|
||||
destroyEntityDeep(c);
|
||||
it = m_streamedPages.erase(it);
|
||||
} else {
|
||||
++it;
|
||||
@@ -533,7 +807,7 @@ void TownGenSystem::clearStreamedPages()
|
||||
for (auto &kv : m_streamedPages)
|
||||
for (flecs::entity c : kv.second.containers)
|
||||
if (c.is_alive())
|
||||
c.destruct();
|
||||
destroyEntityDeep(c);
|
||||
m_streamedPages.clear();
|
||||
}
|
||||
|
||||
@@ -549,7 +823,14 @@ TownGenSystem::createSettlementResources(flecs::entity container,
|
||||
texture.add<EditorMarkerComponent>();
|
||||
|
||||
ProceduralTextureComponent tex;
|
||||
tex.textureName = "towngen_tex_" + std::to_string(seed);
|
||||
/* Unique resource name per creation (the persistent id is
|
||||
* auto-generated): Ogre resource bookkeeping is name-keyed while
|
||||
* resource lifetime is refcounted, so reusing a deterministic
|
||||
* name across town regenerations can leave stale entries
|
||||
* pointing at a destroyed resource (the RTSS material entries
|
||||
* turned that into a crash on the next same-name lookup). */
|
||||
tex.textureName =
|
||||
"towngen_tex_" + std::to_string(seed) + "_" + tex.textureId;
|
||||
|
||||
/* Whole atlas tinted with the district wall color; specific
|
||||
* rects get their own colors. */
|
||||
@@ -576,7 +857,8 @@ TownGenSystem::createSettlementResources(flecs::entity container,
|
||||
}
|
||||
|
||||
void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
CellGridComponent &grid)
|
||||
CellGridComponent &grid,
|
||||
const std::string &genMode)
|
||||
{
|
||||
/* Phase 4.5 item 2: the building shape comes from the
|
||||
* evaluated layout plan (TownBuilding::plan); the plan flags
|
||||
@@ -589,7 +871,7 @@ void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
grid.depth = plan.cellsZ;
|
||||
grid.cellSize = plan.cellSize;
|
||||
grid.cellHeight = plan.cellHeight;
|
||||
grid.generationMode = "exteriorOnly";
|
||||
grid.generationMode = genMode;
|
||||
|
||||
/* Exterior shell only. The entrance gets a real door leaf so the
|
||||
* doorway reads as a door, but every external door is locked by
|
||||
@@ -643,7 +925,8 @@ void TownGenSystem::fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
void TownGenSystem::createBuilding(flecs::entity container,
|
||||
flecs::entity material, flecs::entity texture,
|
||||
const TownGen::TownBuilding &b,
|
||||
const std::vector<const TownGen::TownPoi *> &pois)
|
||||
const std::vector<const TownGen::TownPoi *> &pois,
|
||||
const std::string &genMode)
|
||||
{
|
||||
TerrainSystem *ts = m_terrainSystem ?
|
||||
m_terrainSystem :
|
||||
@@ -655,7 +938,7 @@ void TownGenSystem::createBuilding(flecs::entity container,
|
||||
building.add<EditorMarkerComponent>();
|
||||
|
||||
CellGridComponent grid;
|
||||
fillBuildingCells(b, grid);
|
||||
fillBuildingCells(b, grid, genMode);
|
||||
|
||||
/* cellToWorld() puts cell (x, y, z) at x*cellSize, so the shell
|
||||
* spans [0, width*cellSize] from the entity node — shift the
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "../components/CellGrid.hpp"
|
||||
|
||||
class TerrainSystem;
|
||||
class JoltPhysicsWrapper;
|
||||
|
||||
/**
|
||||
* Town generation system (demo-taxi phase 4).
|
||||
@@ -42,7 +43,8 @@ class TerrainSystem;
|
||||
class TownGenSystem
|
||||
{
|
||||
public:
|
||||
TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr);
|
||||
TownGenSystem(flecs::world &world, Ogre::SceneManager *sceneMgr,
|
||||
JoltPhysicsWrapper *physics);
|
||||
|
||||
/** Generate a settlement on the active terrain's road graph.
|
||||
* Returns the number of buildings placed (0 on failure, @p error
|
||||
@@ -74,6 +76,58 @@ public:
|
||||
/** Live "TownGen " container entities. */
|
||||
int countTowns() const;
|
||||
|
||||
/** Phase 4.6: regenerate ONE building of a generated town
|
||||
* (matched by address, the building's entity name). Re-runs the
|
||||
* pure town generation with the same parameters (area params
|
||||
* re-read from the graph, so edited lot params apply), finds the
|
||||
* lot's TownBuilding and rebuilds only that building entity.
|
||||
* @p genMode overrides the CellGrid generation mode for this
|
||||
* build only ("exteriorOnly" default, "interiorOnly" or "full" —
|
||||
* an authoring preview; the next full regen resets to
|
||||
* exterior). Returns 1 on success, 0 with @p error set.
|
||||
* Per-lot RNG streams make the rebuild exact: editing one lot's
|
||||
* params can never shift another lot's building (only street
|
||||
* NUMBERING still depends on the build outcomes of earlier lots
|
||||
* of the same street). */
|
||||
int regenerateBuilding(const std::string &address,
|
||||
const std::string &genMode, std::string *error);
|
||||
|
||||
/** Phase 4.6: regenerate every generated building whose lot
|
||||
* params came from road edge @p edgeIndex (index into
|
||||
* RoadGraph::edges) side @p side (0 = lotLeft, 1 = lotRight) —
|
||||
* the "apply my lot params edit" button next to the edge-side DSL
|
||||
* editor, no building selection needed. Buildings are matched
|
||||
* through TownBuilding::paramsEdge/paramsSide recorded at
|
||||
* generation time. Returns the number of rebuilt buildings. */
|
||||
int regenerateByLotParams(int edgeIndex, int side, std::string *error);
|
||||
|
||||
/** 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
|
||||
* go to the Ogre log. */
|
||||
const std::vector<std::string> &getLastWarnings() const
|
||||
{
|
||||
return m_lastWarnings;
|
||||
}
|
||||
|
||||
/** Layout library loader: built-ins + town_layouts.txt from the
|
||||
* current working directory when present (phase 4.5 item 2).
|
||||
* Public for the editor's live DSL validation. */
|
||||
static LayoutLib::LayoutLibrary loadLayoutLibrary();
|
||||
|
||||
private:
|
||||
/** Shared regen core of regenerateBuilding()/
|
||||
* regenerateByLotParams(): for every "TownGen " container with
|
||||
* known generation parameters, re-run the pure generation and
|
||||
* rebuild the container's building entities whose fresh
|
||||
* TownBuilding satisfies @p match (replaced in place by address).
|
||||
* Returns the number of rebuilt buildings; 0 sets @p error to
|
||||
* @p notFound. */
|
||||
int regenerateMatching(
|
||||
const std::function<bool(const TownGen::TownBuilding &)> &match,
|
||||
const std::string &genMode, const std::string ¬Found,
|
||||
std::string *error);
|
||||
|
||||
private:
|
||||
/** Streamed-page bookkeeping for update(): nextArea walks the
|
||||
* graph's town areas one per frame; containers holds the created
|
||||
@@ -99,7 +153,8 @@ private:
|
||||
void clearStreamedPages();
|
||||
void createBuilding(flecs::entity container, flecs::entity material,
|
||||
flecs::entity texture, const TownGen::TownBuilding &b,
|
||||
const std::vector<const TownGen::TownPoi *> &pois);
|
||||
const std::vector<const TownGen::TownPoi *> &pois,
|
||||
const std::string &genMode = "exteriorOnly");
|
||||
void createFoundation(flecs::entity building,
|
||||
const TownGen::TownBuilding &b,
|
||||
const CellGridComponent &grid,
|
||||
@@ -108,15 +163,39 @@ private:
|
||||
const CellGridComponent &grid,
|
||||
flecs::entity material, flecs::entity texture);
|
||||
static void fillBuildingCells(const TownGen::TownBuilding &b,
|
||||
CellGridComponent &grid);
|
||||
/** Layout library loader: built-ins + town_layouts.txt from the
|
||||
* current working directory when present (phase 4.5 item 2). */
|
||||
static LayoutLib::LayoutLibrary loadLayoutLibrary();
|
||||
CellGridComponent &grid,
|
||||
const std::string &genMode);
|
||||
|
||||
/** Full synchronous teardown of a generated entity and its
|
||||
* children (regen/clear used to bare-destruct, leaking physics
|
||||
* bodies, TriangleBuffer meshes and scene nodes — the "remains
|
||||
* of previous states" after a single-building regen). Children
|
||||
* recurse first, then physics body, TriangleBuffer Ogre
|
||||
* entity+mesh, renderable, and the scene node LAST (Ogre
|
||||
* destroySceneNode detaches but does not destroy attached
|
||||
* objects). CellGrid shell meshes/frames/doors are owned by
|
||||
* CellGridSystem::m_entityMeshes and are destroyed by its
|
||||
* next-frame cleanupDestroyedEntities() sweep — dead door
|
||||
* children are skipped there, so the synchronous child teardown
|
||||
* here never double-destroys. */
|
||||
void destroyEntityDeep(flecs::entity entity);
|
||||
|
||||
flecs::world &m_world;
|
||||
Ogre::SceneManager *m_sceneMgr;
|
||||
JoltPhysicsWrapper *m_physics;
|
||||
TerrainSystem *m_terrainSystem = nullptr;
|
||||
|
||||
/* Per-container generation parameters, recorded by generate()
|
||||
* (container name -> params) and generateAreas() (container name
|
||||
* -> town area index, params re-read from the graph at regen) for
|
||||
* regenerateBuilding(). */
|
||||
std::map<std::string, TownGen::TownParams> m_containerParams;
|
||||
std::map<std::string, int> m_containerArea;
|
||||
|
||||
/** Warnings of the last generate/regenerate run (see
|
||||
* getLastWarnings()). */
|
||||
std::vector<std::string> m_lastWarnings;
|
||||
|
||||
/* Streaming driver state (item 5). */
|
||||
std::map<uint64_t, StreamedTownPage> m_streamedPages;
|
||||
flecs::entity_t m_streamTerrainId = 0;
|
||||
|
||||
@@ -3620,8 +3620,8 @@ static bool runTownGenProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* Village preset: single-storey, density leaves some lots empty
|
||||
* but stays deterministic. */
|
||||
/* Village preset: single-storey, deterministic; lost density
|
||||
* rolls clamp to the modest variant instead of skipping. */
|
||||
{
|
||||
RoadGraph graph;
|
||||
buildTown(graph);
|
||||
@@ -3649,6 +3649,40 @@ static bool runTownGenProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4.6 follow-up: the density roll NEVER drops a lot — a
|
||||
* lost roll clamps a DEFAULT layout to the modest variant
|
||||
* (counted in densityModest) and flows into the layout bindings
|
||||
* (density / density-roll / density-pass) so explicit layouts
|
||||
* can react themselves. Village seed 3 loses some rolls; with a
|
||||
* layout on every edge side nothing is clamped. */
|
||||
{
|
||||
RoadGraph graph;
|
||||
buildTown(graph);
|
||||
TownGen::TownParams vill;
|
||||
vill.district = TownGen::District::Village;
|
||||
vill.seed = 3;
|
||||
TownGen::TownResult res =
|
||||
TownGen::generateTown(graph, vill, flat);
|
||||
if (res.buildings.size() != 12 || res.densityModest == 0) {
|
||||
printf(" *** density modest: %zu buildings %d "
|
||||
"modest, want 12/>0 ***\n",
|
||||
res.buildings.size(), res.densityModest);
|
||||
ok = false;
|
||||
}
|
||||
for (RoadEdge &e : graph.edges) {
|
||||
e.lotLeft.layout = "village-house";
|
||||
e.lotRight.layout = "village-house";
|
||||
}
|
||||
TownGen::TownResult resB =
|
||||
TownGen::generateTown(graph, vill, flat);
|
||||
if (resB.buildings.size() != 12 || resB.densityModest != 0) {
|
||||
printf(" *** density modest: %zu buildings %d "
|
||||
"modest with explicit layouts, want 12/0 ***\n",
|
||||
resB.buildings.size(), resB.densityModest);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4.5: per-edge-side lot params. A single 100 m edge grows
|
||||
* one fallback square lot per side; the edge runs +X, so graph
|
||||
* "left" (UNIT_Y x dir) is -Z. */
|
||||
@@ -4092,6 +4126,254 @@ static bool runTownGenProbe()
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* --- Phase 4.6: rooms-first (:empty) layouts --- */
|
||||
|
||||
/* L footprint from two rooms (no cut): 12x8 m -> 6x4 cells;
|
||||
* living x[0,2] full depth, kitchen x[3,5] z[0,1]; the
|
||||
* union boundary gets exterior walls, the shared edge an
|
||||
* interior wall, and connect() an interior door. */
|
||||
{
|
||||
BuildingPlan pL;
|
||||
std::vector<std::string> warnsL;
|
||||
LayoutEval evL(lb, &rngDsl, nullptr);
|
||||
if (!evL.evaluate("(shell 12 8 :empty)"
|
||||
"(floor 0 (room living (rect 0 0 0.5 1))"
|
||||
" (room kitchen (rect 0.5 0 1 0.5))"
|
||||
" (connect living kitchen)"
|
||||
" (door front -2))",
|
||||
pL, &derr, &warnsL)) {
|
||||
printf(" *** dsl rooms-L: eval failed '%s' ***\n",
|
||||
derr.c_str());
|
||||
ok = false;
|
||||
} else {
|
||||
uint64_t livFront = pL.at(0, 0, 3);
|
||||
uint64_t notch = pL.at(5, 0, 3);
|
||||
uint64_t kitchen = pL.at(5, 0, 1);
|
||||
uint64_t lCorner = pL.at(2, 0, 3);
|
||||
uint64_t kBack = pL.at(3, 0, 1);
|
||||
uint64_t doorSide = pL.at(2, 0, 0);
|
||||
uint64_t doorOther = pL.at(3, 0, 0);
|
||||
uint64_t wallA = pL.at(2, 0, 1);
|
||||
uint64_t wallB = pL.at(3, 0, 1);
|
||||
bool idsOk =
|
||||
pL.roomIds.size() == 24 &&
|
||||
pL.roomNames.size() == 2 &&
|
||||
pL.roomIds[3 * 6 + 0] >= 0 &&
|
||||
pL.roomNames[pL.roomIds[3 * 6 + 0]] ==
|
||||
"living" &&
|
||||
pL.roomIds[1 * 6 + 5] >= 0 &&
|
||||
pL.roomNames[pL.roomIds[1 * 6 + 5]] ==
|
||||
"kitchen" &&
|
||||
pL.roomIds[3 * 6 + 5] == -1;
|
||||
if (!(livFront & PlanFlags::Floor) ||
|
||||
notch != 0 ||
|
||||
!(kitchen & PlanFlags::Floor) ||
|
||||
!(lCorner & PlanFlags::WallXPos) ||
|
||||
!(lCorner & PlanFlags::DoorZPos) ||
|
||||
(lCorner & PlanFlags::WallZPos) ||
|
||||
!(kBack & PlanFlags::WallZPos) ||
|
||||
!(doorSide & PlanFlags::IntDoorXPos) ||
|
||||
!(doorOther & PlanFlags::IntDoorXNeg) ||
|
||||
(doorSide & PlanFlags::IntWallXPos) ||
|
||||
!(wallA & PlanFlags::IntWallXPos) ||
|
||||
!(wallB & PlanFlags::IntWallXNeg) ||
|
||||
(wallA & PlanFlags::WallXPos) ||
|
||||
!idsOk || !warnsL.empty()) {
|
||||
printf(" *** dsl rooms-L: flags wrong "
|
||||
"(notch %llx corner %llx door "
|
||||
"%llx/%llx wall %llx/%llx ids %d "
|
||||
"warns %zu) ***\n",
|
||||
(unsigned long long)notch,
|
||||
(unsigned long long)lCorner,
|
||||
(unsigned long long)doorSide,
|
||||
(unsigned long long)doorOther,
|
||||
(unsigned long long)wallA,
|
||||
(unsigned long long)wallB,
|
||||
(int)idsOk, warnsL.size());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Snapping consistency at an odd cell count: 10x10 m ->
|
||||
* 5x5 cells; rooms split at fraction 0.5 must tile with
|
||||
* no overlap and no gap (half-open [x0, x1) snapping;
|
||||
* lround(0.5*5)=3, so a=[0,2], b=[3,4]). */
|
||||
{
|
||||
BuildingPlan pOdd;
|
||||
LayoutEval evOdd(lb, &rngDsl, nullptr);
|
||||
if (!evOdd.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 0.5 1))"
|
||||
" (room b (rect 0.5 0 1 1))"
|
||||
" (connect a b))",
|
||||
pOdd, &derr)) {
|
||||
printf(" *** dsl rooms-odd: eval failed "
|
||||
"'%s' ***\n", derr.c_str());
|
||||
ok = false;
|
||||
} else {
|
||||
bool tileOk = true;
|
||||
for (int z = 0; z < 5 && tileOk; ++z)
|
||||
for (int x = 0; x < 5; ++x)
|
||||
if (pOdd.roomIds
|
||||
[(size_t)z * 5 + x] < 0)
|
||||
tileOk = false;
|
||||
uint64_t doorCell = pOdd.at(2, 0, 2);
|
||||
uint64_t doorOther = pOdd.at(3, 0, 2);
|
||||
if (!tileOk ||
|
||||
!(doorCell & PlanFlags::IntDoorXPos) ||
|
||||
!(doorOther & PlanFlags::IntDoorXNeg)) {
|
||||
printf(" *** dsl rooms-odd: tiling %d "
|
||||
"door %llx/%llx ***\n",
|
||||
(int)tileOk,
|
||||
(unsigned long long)doorCell,
|
||||
(unsigned long long)doorOther);
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Validation: overlapping rooms, out-of-shell fractions,
|
||||
* zero-cell rooms and unconnected rooms are hard errors. */
|
||||
{
|
||||
BuildingPlan pBad2;
|
||||
LayoutEval evBad2(lb, &rngDsl, nullptr);
|
||||
if (evBad2.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 0.7 1))"
|
||||
" (room b (rect 0.5 0 1 1)))",
|
||||
pBad2, &derr) ||
|
||||
evBad2.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 1.2 1)))",
|
||||
pBad2, &derr) ||
|
||||
evBad2.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 0.05 1)))",
|
||||
pBad2, &derr) ||
|
||||
evBad2.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 0.4 0.4))"
|
||||
" (room b (rect 0.6 0.6 1 1))"
|
||||
" (connect a b))",
|
||||
pBad2, &derr)) {
|
||||
printf(" *** dsl rooms-errors: bad sources "
|
||||
"accepted ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Warning: a room with no connection path to the entry
|
||||
* door is unreachable. */
|
||||
{
|
||||
BuildingPlan pUnr;
|
||||
std::vector<std::string> warnsU;
|
||||
LayoutEval evUnr(lb, &rngDsl, nullptr);
|
||||
if (!evUnr.evaluate("(shell 10 10 :empty)"
|
||||
"(floor 0 (room a (rect 0 0 0.4 1))"
|
||||
" (room b (rect 0.6 0 1 1))"
|
||||
" (door front -4))",
|
||||
pUnr, &derr, &warnsU) ||
|
||||
warnsU.size() != 1 ||
|
||||
warnsU[0].find("'b'") == std::string::npos ||
|
||||
warnsU[0].find("unreachable") ==
|
||||
std::string::npos) {
|
||||
printf(" *** dsl rooms-unreachable: err '%s' "
|
||||
"warns %zu ***\n", derr.c_str(),
|
||||
warnsU.size());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Door center fallback on a non-rect union: the front
|
||||
* center cell of the U is the courtyard; the door moves
|
||||
* to the nearest floor cell with a warning. */
|
||||
{
|
||||
BuildingPlan pU;
|
||||
std::vector<std::string> warnsUd;
|
||||
LayoutEval evU(lb, &rngDsl, nullptr);
|
||||
if (!evU.evaluate("(shell 12 12 :empty)"
|
||||
"(floor 0 (room hall (rect 0 0 1 0.5))"
|
||||
" (room bed-a (rect 0 0.5 0.34 1))"
|
||||
" (room bed-b (rect 0.66 0.5 1 1))"
|
||||
" (connect hall bed-a)"
|
||||
" (connect hall bed-b)"
|
||||
" (door front center))",
|
||||
pU, &derr, &warnsUd) ||
|
||||
warnsUd.size() != 1 ||
|
||||
!(pU.at(4, 0, 5) & PlanFlags::DoorZPos)) {
|
||||
printf(" *** dsl rooms-door-fallback: err "
|
||||
"'%s' warns %zu door %llx ***\n",
|
||||
derr.c_str(), warnsUd.size(),
|
||||
(unsigned long long)pU.at(4, 0, 5));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Union-boundary windows: a courtyard-facing wall gets a
|
||||
* window from (windows all). And roof :level puts the
|
||||
* lower wing's roof at cell row 1. */
|
||||
{
|
||||
BuildingPlan pW;
|
||||
LayoutEval evW(lb, &rngDsl, nullptr);
|
||||
if (!evW.evaluate("(shell 12 12 :empty)"
|
||||
"(floor 0 (room hall (rect 0 0 1 0.5))"
|
||||
" (room bed-a (rect 0 0.5 0.34 1))"
|
||||
" (room bed-b (rect 0.66 0.5 1 1))"
|
||||
" (connect hall bed-a)"
|
||||
" (connect hall bed-b)"
|
||||
" (door front -4)"
|
||||
" (windows all))"
|
||||
"(roof flat 0 6 12 12 :level 1)",
|
||||
pW, &derr) ||
|
||||
!(pW.at(1, 0, 3) & PlanFlags::WindowXPos) ||
|
||||
pW.roofs.size() != 1 || pW.roofs[0].posY != 1) {
|
||||
printf(" *** dsl rooms-windows/roof-level: err "
|
||||
"'%s' win %llx roofs %zu ***\n",
|
||||
derr.c_str(),
|
||||
(unsigned long long)pW.at(1, 0, 3),
|
||||
pW.roofs.size());
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Per-room :windows none (rooms-first AND legacy): the
|
||||
* flagged room's boundary cells get no windows while the
|
||||
* other rooms keep theirs. 12x8 m -> 6x4 cells; bath
|
||||
* x[3,5] z[2,3] (right edge + front row). */
|
||||
for (bool emptyShell : { true, false }) {
|
||||
BuildingPlan pN;
|
||||
LayoutEval evN(lb, &rngDsl, nullptr);
|
||||
std::string src =
|
||||
emptyShell ? "(shell 12 8 :empty)" :
|
||||
"(shell 12 8)";
|
||||
src += "(floor 0 (room living (rect 0 0 0.5 1))"
|
||||
" (room kitchen (rect 0.5 0 1 0.5))"
|
||||
" (room bath (rect 0.5 0.5 1 1) "
|
||||
" :windows none)"
|
||||
" (connect living kitchen)"
|
||||
" (connect kitchen bath)"
|
||||
" (door front -4)"
|
||||
" (windows all))";
|
||||
bool evOk = evN.evaluate(src, pN, &derr);
|
||||
bool bathBlocked =
|
||||
!(pN.at(5, 0, 2) & PlanFlags::WindowXPos) &&
|
||||
!(pN.at(5, 0, 3) & PlanFlags::WindowXPos) &&
|
||||
!(pN.at(5, 0, 3) & PlanFlags::WindowZPos);
|
||||
/* (5,0,1): kitchen right edge, non-corner (the
|
||||
* legacy perimeter walk skips corners). */
|
||||
bool othersHave =
|
||||
(pN.at(5, 0, 1) & PlanFlags::WindowXPos) &&
|
||||
(pN.at(0, 0, 1) & PlanFlags::WindowXNeg);
|
||||
if (!evOk || !bathBlocked || !othersHave) {
|
||||
printf(" *** dsl :windows none (%s): err "
|
||||
"'%s' bath %llx/%llx other %llx/%llx "
|
||||
"***\n",
|
||||
emptyShell ? "empty" : "legacy",
|
||||
derr.c_str(),
|
||||
(unsigned long long)pN.at(5, 0, 2),
|
||||
(unsigned long long)pN.at(5, 0, 3),
|
||||
(unsigned long long)pN.at(5, 0, 1),
|
||||
(unsigned long long)pN.at(0, 0, 1));
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Integration: an inline edge-side layout drives the
|
||||
* building (10x8 m shell, 3 floors, cone roof). */
|
||||
{
|
||||
@@ -4487,6 +4769,71 @@ static bool runTownGenProbe()
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4.6: per-lot RNG streams — editing one edge side's lot
|
||||
* params must not shift any building of another edge side (the
|
||||
* old shared-stream behaviour changed every downstream lot). */
|
||||
{
|
||||
RoadGraph graph;
|
||||
buildTown(graph);
|
||||
TownGen::TownResult before =
|
||||
TownGen::generateTown(graph, cc, flat);
|
||||
|
||||
/* Every building records its params source edge/side. */
|
||||
for (const TownGen::TownBuilding &b : before.buildings)
|
||||
if (b.paramsEdge < 0 || b.paramsSide < 0 ||
|
||||
b.paramsSide > 1) {
|
||||
printf(" *** per-lot seeds: '%s' paramsEdge %d "
|
||||
"paramsSide %d ***\n", b.address.c_str(),
|
||||
b.paramsEdge, b.paramsSide);
|
||||
ok = false;
|
||||
}
|
||||
|
||||
/* Replace the params source of the first building with an
|
||||
* inline layout of a different draw count. */
|
||||
const TownGen::TownBuilding &target = before.buildings[0];
|
||||
RoadLotParams &lp = target.paramsSide == 0 ?
|
||||
graph.edges[target.paramsEdge].lotLeft :
|
||||
graph.edges[target.paramsEdge].lotRight;
|
||||
lp.layout = "(shell 14 12 :floors 2) (door front center) "
|
||||
"(windows all) (roof gable)";
|
||||
TownGen::TownResult after =
|
||||
TownGen::generateTown(graph, cc, flat);
|
||||
if (after.buildings.size() != before.buildings.size()) {
|
||||
printf(" *** per-lot seeds: count %zu -> %zu ***\n",
|
||||
before.buildings.size(), after.buildings.size());
|
||||
ok = false;
|
||||
} else {
|
||||
int changed = 0;
|
||||
for (size_t i = 0; i < before.buildings.size(); ++i) {
|
||||
const TownGen::TownBuilding &a =
|
||||
before.buildings[i];
|
||||
const TownGen::TownBuilding &b =
|
||||
after.buildings[i];
|
||||
bool sameEdge =
|
||||
a.paramsEdge == target.paramsEdge &&
|
||||
a.paramsSide == target.paramsSide;
|
||||
bool identical = a.center == b.center &&
|
||||
a.yaw == b.yaw &&
|
||||
a.width == b.width &&
|
||||
a.depth == b.depth &&
|
||||
a.floors == b.floors;
|
||||
if (!sameEdge && !identical) {
|
||||
printf(" *** per-lot seeds: untouched "
|
||||
"lot '%s' changed ***\n",
|
||||
a.address.c_str());
|
||||
ok = false;
|
||||
}
|
||||
if (sameEdge && !identical)
|
||||
++changed;
|
||||
}
|
||||
if (changed == 0) {
|
||||
printf(" *** per-lot seeds: the edited side "
|
||||
"did not change ***\n");
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf(" %s\n", ok ? "passed" : "FAILURES above");
|
||||
return ok;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -290,7 +291,7 @@ public:
|
||||
"directly on the terrain in the 3D view.");
|
||||
}
|
||||
|
||||
renderRoadSection(tc, ts);
|
||||
renderRoadSection(entity, tc, ts);
|
||||
|
||||
ImGui::PopID();
|
||||
}
|
||||
@@ -702,26 +703,44 @@ private:
|
||||
" min-fp max-fp district footprint range (m)\n"
|
||||
" min-floors max-floors district floor range\n"
|
||||
" roof-type edge-side roof override "
|
||||
"(-1 = none)");
|
||||
"(-1 = none)\n"
|
||||
" density density-roll density-pass the "
|
||||
"district density, the lot's own roll and "
|
||||
"whether it passed — use for conditional "
|
||||
"features (a lost roll never drops the lot; "
|
||||
"default layouts just build a modest variant)");
|
||||
ImGui::Spacing();
|
||||
ImGui::TextUnformatted("Verbs:");
|
||||
ImGui::TextWrapped(
|
||||
" (cell-size xz-m [height-m]) grid metrics, "
|
||||
"before shell (default 2 / 4)\n"
|
||||
" (shell w d [:floors n]) exterior walls, "
|
||||
"meters\n"
|
||||
" (shell w d [:floors n] [:empty]) exterior "
|
||||
"walls, meters; :empty declares only the bounding "
|
||||
"rect — rooms then claim the cells and the "
|
||||
"footprint is the union of the rooms (L/U/T/"
|
||||
"courtyard shapes without cut; declare rooms "
|
||||
"before door/windows)\n"
|
||||
" (cut x0 z0 x1 z1) remove cells "
|
||||
"(L/U shapes), shell-frame meters; all floors, or "
|
||||
"one inside (floor n ...)\n"
|
||||
" (floor n body...) per-floor context\n"
|
||||
" (door front|back|left|right center|<offset-m>)\n"
|
||||
" (windows all|front|back|left|right [:spacing m])\n"
|
||||
" (room name (rect x0 z0 x1 z1)) fractions of the "
|
||||
"shell\n"
|
||||
" (idoor roomA roomB) door in the shared "
|
||||
"wall\n"
|
||||
" (door front|back|left|right center|<offset-m>)"
|
||||
" center on a non-rect footprint moves to the "
|
||||
"nearest floor cell (warning)\n"
|
||||
" (windows all|front|back|left|right [:spacing m])"
|
||||
" 'all' covers the whole footprint boundary of a "
|
||||
"rooms-first shell (incl. courtyard walls)\n"
|
||||
" (room name (rect x0 z0 x1 z1) [:windows "
|
||||
"none]) fractions of the shell; :windows none "
|
||||
"suppresses exterior windows on the room's "
|
||||
"boundary cells (bathroom/storage); declare "
|
||||
"rooms before the windows verb\n"
|
||||
" (connect roomA roomB) door in the "
|
||||
"shared wall (alias: idoor)\n"
|
||||
" (roof flat|normal|normal2|gable|cone|cylinder "
|
||||
"[x0 z0 x1 z1] [:height m])\n"
|
||||
"[x0 z0 x1 z1] [:height m] [:level n]) :level = "
|
||||
"cell row (default above the top floor; 1 roofs a "
|
||||
"lower wing under a smaller upper floor)\n"
|
||||
" (parking x z w d) or (parking (rect-front-left "
|
||||
"w d)) or (rect-front-right w d)\n"
|
||||
" (place \"<prefab>\" (at x z) [:yaw deg]) "
|
||||
@@ -751,7 +770,227 @@ private:
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static void renderRoadSection(TerrainComponent &tc, TerrainSystem *ts)
|
||||
/* Phase 4.6: live layout-DSL validation for the lot-params
|
||||
* editors. Evaluates the source (library name or inline) against
|
||||
* representative bindings with a scratch RNG — a syntax/semantic
|
||||
* check, not the lot's real draw, so rand draws shown here are not
|
||||
* the lot's own. */
|
||||
struct LayoutCheckResult {
|
||||
bool ok = false;
|
||||
std::string message; /* error text or OK summary */
|
||||
std::vector<std::string> warnings;
|
||||
};
|
||||
|
||||
static void checkLayoutSource(const std::string &src,
|
||||
LayoutCheckResult &out)
|
||||
{
|
||||
out = LayoutCheckResult();
|
||||
LayoutLib::LayoutBindings lb;
|
||||
lb.lotW = 40.0f;
|
||||
lb.lotD = 40.0f;
|
||||
lb.availW = 35.0f;
|
||||
lb.availD = 35.0f;
|
||||
lb.minFp = 8.0f;
|
||||
lb.maxFp = 14.0f;
|
||||
lb.minFloors = 1;
|
||||
lb.maxFloors = 3;
|
||||
static LayoutLib::LayoutLibrary s_checkLib =
|
||||
TownGenSystem::loadLayoutLibrary();
|
||||
std::mt19937 scratch(0);
|
||||
LayoutLib::BuildingPlan plan;
|
||||
std::string err;
|
||||
LayoutLib::LayoutEval eval(lb, &scratch, &s_checkLib);
|
||||
if (!eval.evaluate(src, plan, &err, &out.warnings)) {
|
||||
out.message = "Error: " + err;
|
||||
return;
|
||||
}
|
||||
out.ok = true;
|
||||
char buf[160];
|
||||
snprintf(buf, sizeof(buf), "OK: %dx%d cells, %d floor(s)",
|
||||
plan.cellsX, plan.cellsZ, plan.floors);
|
||||
out.message = buf;
|
||||
}
|
||||
|
||||
static void layoutCheckText(const ImVec4 &col, const std::string &text)
|
||||
{
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, col);
|
||||
ImGui::TextWrapped("%s", text.c_str());
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
/* ImGui::InputTextMultiline on a std::string (imgui_stdlib is
|
||||
* not linked here): the CallbackResize pattern from
|
||||
* imgui_demo.cpp. Editing the string DIRECTLY keeps the inline
|
||||
* Lot Params field and the pop-out editor in sync — both bind
|
||||
* to RoadLotParams::layout, no stale buffers to copy. */
|
||||
struct LayoutStrResize {
|
||||
static int cb(ImGuiInputTextCallbackData *data)
|
||||
{
|
||||
if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) {
|
||||
std::string *s = (std::string *)data->UserData;
|
||||
IM_ASSERT(data->Buf == s->c_str());
|
||||
s->resize(data->BufTextLen);
|
||||
data->Buf = (char *)s->c_str();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
static bool layoutInputMultiline(const char *label, std::string &str,
|
||||
const ImVec2 &size)
|
||||
{
|
||||
return ImGui::InputTextMultiline(
|
||||
label, (char *)str.c_str(), str.capacity() + 1, size,
|
||||
ImGuiInputTextFlags_CallbackResize, LayoutStrResize::cb,
|
||||
&str);
|
||||
}
|
||||
|
||||
/* Renders the cached check of src under a DSL editor field,
|
||||
* re-evaluating only when the source changed. slot picks one of
|
||||
* the cache lines (0/1 = inline left/right, 2 = pop-out). */
|
||||
static void renderLayoutCheck(const std::string &src, int slot)
|
||||
{
|
||||
static std::string s_checked[3];
|
||||
static LayoutCheckResult s_result[3];
|
||||
slot = std::max(0, std::min(slot, 2));
|
||||
if (src.empty()) {
|
||||
s_checked[slot].clear();
|
||||
ImGui::TextDisabled("empty = district default layout");
|
||||
return;
|
||||
}
|
||||
if (src != s_checked[slot]) {
|
||||
s_checked[slot] = src;
|
||||
checkLayoutSource(src, s_result[slot]);
|
||||
}
|
||||
const LayoutCheckResult &r = s_result[slot];
|
||||
if (!r.ok)
|
||||
layoutCheckText(ImVec4(1.0f, 0.35f, 0.35f, 1.0f),
|
||||
r.message);
|
||||
else if (!r.warnings.empty())
|
||||
layoutCheckText(ImVec4(1.0f, 0.8f, 0.3f, 1.0f),
|
||||
r.message);
|
||||
else
|
||||
layoutCheckText(ImVec4(0.45f, 0.9f, 0.45f, 1.0f),
|
||||
r.message);
|
||||
for (const std::string &w : r.warnings)
|
||||
layoutCheckText(ImVec4(1.0f, 0.8f, 0.3f, 1.0f),
|
||||
"warning: " + w);
|
||||
}
|
||||
|
||||
/* Phase 4.6: pop-out DSL editor state — a floating window with a
|
||||
* large text area editing the same edge-side layout string as the
|
||||
* inline Lot Params field. Bound to the edge by its (nodeA,
|
||||
* nodeB) pair (edge INDICES shift as edges are added/removed).
|
||||
* No text buffer of its own: it edits RoadLotParams::layout
|
||||
* directly, so it can never desync from the inline field. */
|
||||
struct LayoutPopState {
|
||||
bool open = false;
|
||||
int nodeA = -1;
|
||||
int nodeB = -1;
|
||||
int side = 0;
|
||||
std::string result;
|
||||
};
|
||||
|
||||
static LayoutPopState &layoutPopState()
|
||||
{
|
||||
static LayoutPopState s;
|
||||
return s;
|
||||
}
|
||||
|
||||
static void openLayoutPopout(const RoadEdge &e, int side)
|
||||
{
|
||||
LayoutPopState &st = layoutPopState();
|
||||
st.open = true;
|
||||
st.nodeA = e.nodeA;
|
||||
st.nodeB = e.nodeB;
|
||||
st.side = side;
|
||||
st.result.clear();
|
||||
}
|
||||
|
||||
static void renderLayoutPopout(RoadGraph &rg, TerrainSystem *ts)
|
||||
{
|
||||
LayoutPopState &st = layoutPopState();
|
||||
if (!st.open)
|
||||
return;
|
||||
|
||||
/* Rebind every frame: edge indices shift with edits. */
|
||||
RoadEdge *edge = nullptr;
|
||||
for (RoadEdge &e : rg.edges)
|
||||
if (e.nodeA == st.nodeA && e.nodeB == st.nodeB) {
|
||||
edge = &e;
|
||||
break;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowSize(ImVec2(640.0f, 600.0f),
|
||||
ImGuiCond_FirstUseEver);
|
||||
bool open = st.open;
|
||||
if (!ImGui::Begin("Layout DSL Editor", &open)) {
|
||||
ImGui::End();
|
||||
st.open = open;
|
||||
return;
|
||||
}
|
||||
st.open = open;
|
||||
if (!edge) {
|
||||
ImGui::TextWrapped(
|
||||
"The bound edge (%d -> %d) no longer exists.",
|
||||
st.nodeA, st.nodeB);
|
||||
if (ImGui::Button("Close"))
|
||||
st.open = false;
|
||||
ImGui::End();
|
||||
return;
|
||||
}
|
||||
RoadLotParams &lp =
|
||||
st.side == 0 ? edge->lotLeft : edge->lotRight;
|
||||
ImGui::Text("Edge %d -> %d, %s side", st.nodeA, st.nodeB,
|
||||
st.side == 0 ? "left" : "right");
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("DSL Help"))
|
||||
getLayoutDslHelpOpen() = !getLayoutDslHelpOpen();
|
||||
if (lp.layout.empty()) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Insert Template")) {
|
||||
lp.layout = LayoutLib::LayoutLibrary::
|
||||
defaultTemplateSource();
|
||||
rg.bumpVersion();
|
||||
}
|
||||
}
|
||||
if (layoutInputMultiline("##layoutsrc", lp.layout,
|
||||
ImVec2(-1.0f, -110.0f)))
|
||||
rg.bumpVersion();
|
||||
renderLayoutCheck(lp.layout, 2);
|
||||
|
||||
/* Rebuild the buildings whose lots take their params from
|
||||
* this edge side (per-lot seeds leave every other lot
|
||||
* untouched). */
|
||||
TownGenSystem *tgs = ts ? ts->getTownGenSystem() : nullptr;
|
||||
if (tgs) {
|
||||
if (ImGui::Button("Regenerate Buildings")) {
|
||||
std::string err;
|
||||
int eidx = (int)(edge - rg.edges.data());
|
||||
int n = tgs->regenerateByLotParams(eidx, st.side,
|
||||
&err);
|
||||
char buf[256];
|
||||
if (n > 0)
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Rebuilt %d building(s).", n);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%s",
|
||||
err.c_str());
|
||||
st.result = buf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Rebuild the generated buildings whose "
|
||||
"lots take their params from this edge "
|
||||
"side; every other lot is untouched.");
|
||||
if (!st.result.empty())
|
||||
ImGui::TextWrapped("%s", st.result.c_str());
|
||||
}
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
static void renderRoadSection(flecs::entity entity, TerrainComponent &tc,
|
||||
TerrainSystem *ts)
|
||||
{
|
||||
RoadSystem *rs = ts ? ts->getRoadSystem() : nullptr;
|
||||
if (!rs) {
|
||||
@@ -1324,6 +1563,105 @@ private:
|
||||
"district/seed/density; only lots whose "
|
||||
"centroid it owns). Replaces all "
|
||||
"generated towns.");
|
||||
|
||||
ImGui::Separator();
|
||||
/* Phase 4.6: rebuild one building from the current
|
||||
* graph/lot params without a whole-town regen, for
|
||||
* layout tweaking. The generation-mode choice is an
|
||||
* authoring preview only — generated towns are never
|
||||
* serialized and a full regen resets to exterior. */
|
||||
ImGui::TextUnformatted("Per-Building Regeneration");
|
||||
std::vector<std::string> regenList;
|
||||
entity.world()
|
||||
.query<EntityNameComponent, TownComponent>()
|
||||
.each([&](flecs::entity c,
|
||||
EntityNameComponent &name,
|
||||
TownComponent &) {
|
||||
if (name.name.rfind("TownGen ", 0) != 0)
|
||||
return;
|
||||
c.children([&](flecs::entity child) {
|
||||
if (child.has<CellGridComponent>() &&
|
||||
child.has<EntityNameComponent>())
|
||||
regenList.push_back(
|
||||
child.get<EntityNameComponent>()
|
||||
.name);
|
||||
});
|
||||
});
|
||||
if (regenList.empty()) {
|
||||
ImGui::TextDisabled(
|
||||
"No generated buildings.");
|
||||
} else {
|
||||
static int s_regenSel = -1;
|
||||
static int s_regenMode = 0;
|
||||
const char *regenModes[] = {
|
||||
"Exterior (default)",
|
||||
"Interior (preview)",
|
||||
"Both (preview)"
|
||||
};
|
||||
ImGui::Combo("Mode##regen", &s_regenMode,
|
||||
regenModes, 3);
|
||||
if (s_regenSel >= (int)regenList.size())
|
||||
s_regenSel = -1;
|
||||
const char *preview =
|
||||
s_regenSel >= 0 ?
|
||||
regenList[s_regenSel].c_str() :
|
||||
"(select building)";
|
||||
if (ImGui::BeginCombo("Building##regen",
|
||||
preview)) {
|
||||
for (int i = 0;
|
||||
i < (int)regenList.size(); ++i) {
|
||||
if (ImGui::Selectable(
|
||||
regenList[i].c_str(),
|
||||
s_regenSel == i))
|
||||
s_regenSel = i;
|
||||
}
|
||||
ImGui::EndCombo();
|
||||
}
|
||||
if (ImGui::Button("Regenerate Building") &&
|
||||
s_regenSel >= 0) {
|
||||
static const char *modes[] = {
|
||||
"exteriorOnly", "interiorOnly",
|
||||
"full"
|
||||
};
|
||||
std::string err;
|
||||
int okr = tgs->regenerateBuilding(
|
||||
regenList[s_regenSel],
|
||||
modes[s_regenMode], &err);
|
||||
s_townResult =
|
||||
okr ? "Regenerated " +
|
||||
regenList[s_regenSel] +
|
||||
"." :
|
||||
"Regeneration failed: " +
|
||||
err;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Re-evaluate this lot's layout and "
|
||||
"rebuild only this building (edited "
|
||||
"lot params apply immediately).\n"
|
||||
"Interior/Both are preview modes; the "
|
||||
"next full regen resets to exterior.\n"
|
||||
"Per-lot RNG seeds keep every other "
|
||||
"lot untouched; only street numbering "
|
||||
"can shift when a layout edit changes "
|
||||
"whether a lot builds at all.");
|
||||
}
|
||||
/* Phase 4.6: generation warnings (layout errors that
|
||||
* fell back to the district default, unreachable
|
||||
* rooms, ...) of the last generate/regenerate run. */
|
||||
const std::vector<std::string> &townWarns =
|
||||
tgs->getLastWarnings();
|
||||
if (!townWarns.empty()) {
|
||||
ImGui::TextColored(
|
||||
ImVec4(1.0f, 0.8f, 0.3f, 1.0f),
|
||||
"%zu warning(s) from the last run:",
|
||||
townWarns.size());
|
||||
ImGui::BeginChild("townwarns",
|
||||
ImVec2(0.0f, 120.0f), true);
|
||||
for (const std::string &w : townWarns)
|
||||
ImGui::TextWrapped("%s", w.c_str());
|
||||
ImGui::EndChild();
|
||||
}
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
@@ -2140,51 +2478,46 @@ private:
|
||||
* (village-house, town-rowhouse,
|
||||
* city-block, city-tower or a
|
||||
* town_layouts.txt entry) or inline
|
||||
* source starting with '('. */
|
||||
static char s_layoutBuf[2][2048];
|
||||
static int s_layoutEdge[2] = { -1,
|
||||
-1 };
|
||||
if (s_layoutEdge[side] != idx) {
|
||||
snprintf(s_layoutBuf[side],
|
||||
sizeof(s_layoutBuf
|
||||
[side]),
|
||||
"%s",
|
||||
lp.layout.c_str());
|
||||
s_layoutEdge[side] = idx;
|
||||
}
|
||||
if (ImGui::InputTextMultiline(
|
||||
"Layout",
|
||||
s_layoutBuf[side],
|
||||
sizeof(s_layoutBuf
|
||||
[side]),
|
||||
ImVec2(-1.0f, 60.0f))) {
|
||||
lp.layout =
|
||||
s_layoutBuf[side];
|
||||
* source starting with '('. Edits
|
||||
* lp.layout DIRECTLY so the pop-out
|
||||
* editor stays in sync (single
|
||||
* source of truth). */
|
||||
if (layoutInputMultiline(
|
||||
"Layout", lp.layout,
|
||||
ImVec2(-1.0f, 60.0f)))
|
||||
edgeChanged = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Layout DSL name or inline "
|
||||
"source; empty = district "
|
||||
"default");
|
||||
"default. The density roll "
|
||||
"never drops a lot: a lost "
|
||||
"roll clamps the DEFAULT "
|
||||
"layout to a modest variant "
|
||||
"(small footprint, minimum "
|
||||
"floors); explicit layouts "
|
||||
"read density/density-roll/"
|
||||
"density-pass");
|
||||
/* Live validation (phase 4.6):
|
||||
* syntax/semantic check against
|
||||
* representative bindings, so a
|
||||
* broken layout is flagged before
|
||||
* any regeneration. */
|
||||
renderLayoutCheck(lp.layout,
|
||||
side);
|
||||
/* Starting aid: fill an empty field with
|
||||
* the default template; the help window
|
||||
* carries the reference and worked
|
||||
* examples. */
|
||||
bool layoutEmpty =
|
||||
s_layoutBuf[side][0] == '\0';
|
||||
lp.layout.empty();
|
||||
if (!layoutEmpty)
|
||||
ImGui::BeginDisabled();
|
||||
if (ImGui::SmallButton(
|
||||
"Insert Template")) {
|
||||
snprintf(
|
||||
s_layoutBuf[side],
|
||||
sizeof(s_layoutBuf
|
||||
[side]),
|
||||
"%s",
|
||||
LayoutLib::LayoutLibrary::defaultTemplateSource());
|
||||
lp.layout =
|
||||
s_layoutBuf[side];
|
||||
lp.layout = LayoutLib::
|
||||
LayoutLibrary::
|
||||
defaultTemplateSource();
|
||||
edgeChanged = true;
|
||||
}
|
||||
if (!layoutEmpty)
|
||||
@@ -2199,6 +2532,71 @@ private:
|
||||
if (ImGui::SmallButton("DSL Help"))
|
||||
getLayoutDslHelpOpen() =
|
||||
!getLayoutDslHelpOpen();
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton("Pop Out"))
|
||||
openLayoutPopout(e, side);
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Edit this layout in a "
|
||||
"larger floating window");
|
||||
/* Phase 4.6: rebuild only the
|
||||
* buildings whose lots take their
|
||||
* params from this edge side —
|
||||
* per-lot RNG seeds keep every
|
||||
* other lot untouched. */
|
||||
static std::string
|
||||
s_regenEdgeResult[2];
|
||||
static int s_regenEdgeIdx[2] = {
|
||||
-1, -1
|
||||
};
|
||||
if (s_regenEdgeIdx[side] != idx) {
|
||||
s_regenEdgeResult[side]
|
||||
.clear();
|
||||
s_regenEdgeIdx[side] = idx;
|
||||
}
|
||||
TownGenSystem *tgsEdge =
|
||||
ts ? ts->getTownGenSystem() :
|
||||
nullptr;
|
||||
if (tgsEdge) {
|
||||
if (ImGui::SmallButton(
|
||||
"Regenerate")) {
|
||||
std::string
|
||||
err;
|
||||
int n = tgsEdge->regenerateByLotParams(
|
||||
idx, side,
|
||||
&err);
|
||||
char buf[256];
|
||||
if (n > 0)
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"Rebuilt %d building(s).",
|
||||
n);
|
||||
else
|
||||
snprintf(
|
||||
buf,
|
||||
sizeof(buf),
|
||||
"%s",
|
||||
err.c_str());
|
||||
s_regenEdgeResult
|
||||
[side] =
|
||||
buf;
|
||||
}
|
||||
if (ImGui::IsItemHovered())
|
||||
ImGui::SetTooltip(
|
||||
"Rebuild the generated "
|
||||
"buildings using this "
|
||||
"edge side's lot params "
|
||||
"(every other lot is "
|
||||
"untouched)");
|
||||
if (!s_regenEdgeResult[side]
|
||||
.empty())
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
s_regenEdgeResult
|
||||
[side]
|
||||
.c_str());
|
||||
}
|
||||
ImGui::TreePop();
|
||||
}
|
||||
ImGui::PopID();
|
||||
@@ -2361,6 +2759,10 @@ private:
|
||||
rg.bumpVersion();
|
||||
}
|
||||
}
|
||||
|
||||
/* Phase 4.6: the pop-out layout editor renders whether or
|
||||
* not an edge is selected (bound by node ids). */
|
||||
renderLayoutPopout(rg, ts);
|
||||
}
|
||||
|
||||
static bool
|
||||
|
||||
Reference in New Issue
Block a user