240 lines
23 KiB
Markdown
240 lines
23 KiB
Markdown
# Worklog
|
|
|
|
One line per item, newest at the top of each section. What was done, what worked, what did not. Keep it terse;
|
|
the reasoning lives in the specs, this is the memory.
|
|
|
|
## Done
|
|
|
|
- 2026-09-17 - Off the ladder, the coast, part two: the uplift field stops being multiplied by the continent
|
|
mask (D-52), so a range that reaches the water rises at range rates right up to the waterline instead of
|
|
being tapered to nothing across the shore. Surf cut 23.3 → 38.9 Mm3 on seed 7 and 22.4 → 30.4 on seed 9342;
|
|
high ground now meets the water in the detail crop where it previously had a uniform low fringe in front of
|
|
it. New `TestBorderIsAlwaysOcean` in `internal/check`, because the margin that keeps land off the map border
|
|
was relying on that same multiplication as a side effect: the border is still all ocean on three seeds, so
|
|
there is no frozen rim. **Did not work, and it was mine:** the "mean cliff" statistic measured the drop from
|
|
a cell to its seaward neighbour and called it a cliff. That is a gradient - one cell of a 10 m grid at the
|
|
angle of repose is 7 m - so it could not exceed 7 whatever the coast did, and it read 2 m on a plain coast
|
|
and 3 m on a cliffed one. It is why yesterday's brief claimed there were no sea cliffs anywhere; the
|
|
replacement, backshore height at one to two surf reaches inland, shows the *old* build already at 88 m P90.
|
|
A cliff is how far you fall, not how steep the first cell is. **Also measured:** the slope-area fit is too
|
|
noisy at five or six bins to judge one change on one seed - the same code gives -0.480 on seed 7 and -0.698
|
|
on seed 9342 - and 14 to 15 % of every coastline is drawn by the map margin rather than by the noise, which
|
|
is a straight line parallel to the map edge and is pre-existing.
|
|
- 2026-09-17 - Off the ladder, the coast. `internal/coast`, running after the fluvial solve, on the terrain
|
|
the solve produced. An exact signed distance transform with a feature index (Felzenszwalb, two 1-D passes)
|
|
is the coordinate everything is written in; on it sit a continental shelf whose width is read off the relief
|
|
behind each stretch of shore, a surf that planes the land to a shore platform within a reach set by how open
|
|
the water is - the cliff is the step where the reach ends - and a sediment budget that carries what the surf
|
|
cut a drift length along the shore and lays it in sheltered shallow water, with river mouths delivering
|
|
their own load. `uplift.Result.Bathymetry` is gone: the coast pass owns the sea floor, and ocean cells stay
|
|
at sea level for the whole solve. New: `map_exposure.png`, `map_coast.png`, a coast block in `meta.json`
|
|
and in the summary, `--no-coast` and six other overrides, nine tests, and the determinism test now covers
|
|
the pass. The measured finding was the coastline *outline*: at five octaves its finest feature is 450 m,
|
|
which is a blob, and the fetch called the median stretch of coast fully open. Swept, seed 7 at 1400:
|
|
shoreline 64/81/96/114 km and median exposure 1.00/0.98/0.84/0.51 at outline gain 0.50/0.58/0.62/0.66. D-51
|
|
sets 8 octaves at 0.62. Whole pass 83 ms. **Did not work:** a percentile stretch for exposure (degenerate on
|
|
an even coast, and a global statistic two tiles would disagree about); casting fetch in every direction
|
|
rather than seaward only (it counts the land behind the shore as shelter, so a straight coast scored more
|
|
sheltered than a bay); a cut fraction read as a fraction of the height above the platform (15 % of a 120 m
|
|
headland is 18 m, which is not a platform); `dep = blur(supply) * want / blur(want)`, which looks like a
|
|
normalised convolution and lost 68 % of the sediment budget - the supply blurred onto land and deep water
|
|
where nothing wants it was silently dropped, and the conserving order is to divide by the blurred want
|
|
*first*, then blur; and one kernel for both jobs - the sediment needs zero padding to stay symmetric and a
|
|
carried value needs edge clamping, and using the mass-preserving one on the shelf width shrank every shelf
|
|
near the border to nothing.
|
|
- 2026-09-17 - Off the ladder, the plains problem. The lowlands read as shrunken mountains because they were
|
|
being uplifted at mountain rates: `S = U/(K*A^m)` applied down to one cell puts a 0.25 mm/yr divide at 28
|
|
degrees and the 0.9 mm/yr swell past the 35 degree repose clamp, so the clamp was the surface of the
|
|
continent (81 % of land in the >0.5 mm/yr class, the plain class 1 % and all sea cliff). Built
|
|
`stats.UpliftBuckets` first, to measure per uplift class rather than map-wide - that is what made it
|
|
visible. Then D-49 (intraplate 0.25 to 0.03, swell 0.9 to 0.08, relief lo 50 to 15 m) and D-50
|
|
(hash-jittered flood epsilon and D8 tie-breaks, for the flat ground the fix exposed). Plains are now 42 %
|
|
of land at 0.8 degrees and 9 m relief; slope-area R2 0.06 to 0.46. Then Roering nonlinear hillslope
|
|
diffusion in `internal/fluvial/hillslope.go`, running *after* the repose clamp each step so the clamp's D8
|
|
pyramid facets get rounded off: mountains 31.1 to 28.5 degrees and 44 % to 33 % pinned at talus, at 8 %
|
|
more runtime. Four tests on it. A run now also writes uplift, erodibility, slope, relief, flow and basin
|
|
maps. **Did not work:** `critical_area_m2` 1e4, re-measured after the uplift fix and still failing (plains
|
|
back to 7 degrees, 79 % of mountains at the clamp) - it needs a hillslope transport law to pair with;
|
|
clamping once at the end of the run instead of in the loop (facets returned). **Still open:** half the land
|
|
is still in the mountain uplift class; fault traces and the range grain are still visibly straight lines;
|
|
MFD accumulation unbuilt.
|
|
- 2026-09-17 — Off the ladder, build-order steps 3 and 4 of [`Terrain.md`](Terrain.md): the Go core exists and
|
|
the stream-power solver works. `Tools/Terrain/` (module `salty/terrain`, `Scripts/build-terrain.sh`, binary
|
|
and `RawContent/World/Preview/` gitignored): `field` (the one array type, vertex-convention resampling,
|
|
Catmull-Rom upsample, greyscale PNG through `image/png`, hillshaded thumbnails, and `field.Rows` as the only
|
|
place goroutines are made), `manifest` (World.json over Go defaults, the same height contract as
|
|
`world_manifest.py`, and a validator that refuses a resolution the importer would turn into thousands of
|
|
components), `noise`, `uplift` (the continent, now producing an uplift *rate* rather than terrain), `fluvial`
|
|
(priority-flood, D8 receivers, stack, accumulation, implicit Braun & Willett update, sub-stepped diffusion),
|
|
`stats` and two integration tests. **The proof: the analytic check `K*A^m*S^n/U` measures 1.0000 over 822
|
|
channel cells, and on the continent the slope-area exponent is -0.498 against -0.500 at R2 0.996 with
|
|
drainage density 1.33 /km.** Determinism is asserted across five values of GOMAXPROCS, not assumed. Nothing
|
|
in the editor was touched (it was open), so steps 1 and 2 are still first and still open; the numpy pipeline
|
|
still builds `L_World` and is unchanged. Three bugs, all of which produced plausible-looking terrain and
|
|
none of which could have been seen by looking: slope-area measured with the topographic gradient instead of
|
|
the channel gradient read -0.78 where the truth was -0.50; cells at a local minimum were skipped entirely
|
|
including their uplift, which froze exactly the basins that differential uplift was forming; and the fix for
|
|
that then uplifted the outlets, because a map border is an outlet but is not ocean, so base level rose 2 m a
|
|
step and the steady-state ratio read 0.03. The last one was invisible in real runs, where the border *is*
|
|
ocean, and only the test caught it.
|
|
- 2026-09-17 — Off the ladder, no code: an incoming procedural terrain specification (tectonics, faults,
|
|
lithology, stream-power erosion, a Go core, an editor bridge into a Landscape edit layer) reconciled against
|
|
the built pipeline in [`Terrain.md`](Terrain.md), and the Go core taken as D-47. What the reconciliation
|
|
found: the spec's 7113 canvas is a trap, because 7112 divides by 127 and not by 255, so the importer's own
|
|
rule gives 3136 components rather than the spec's 784 — the 4033 mistake again (D-45); 7141 at 200 cm gives
|
|
28x28 components of 255 quads with the same 14.28 km side the manifest already has. The spec's Z scale 400
|
|
is exactly a 2048 m elevation span, so its canvas is expressible in the manifest's own metres contract, at
|
|
the cost of dropping the crests from 2800 m to 1536 m (Q2). The spec's uplift/K parameter ranges and its
|
|
elevation budget are not jointly consistent at their corners: steady state puts slope at U/(K*A^m), and the
|
|
aggressive corner overshoots 1536 m badly, so U/K is the one relief knob and the clip fraction is the check.
|
|
The spec has no coast at all and leaves base level open, where this world has one and a sea plane (Q1).
|
|
Lithology (plan view) and strata (vertical) are orthogonal and both kept. The spec's §8 DEM escape hatch is
|
|
already built and better specified than the spec's version. Deferred with triggers: the preset gallery
|
|
widget, `UTerrainPreset`, river splines, build-zone volumes, any GPU port. The five open questions were put
|
|
to the user the same day and all five went the recommended way (D-48): the coast stays and sea level is the
|
|
base level, the ceiling drops to 1536 m, the canvas goes to 7141 at 200 cm, and a full run holds the
|
|
five-minute bar. Go 1.25 turned out to be installed already, on 16 cores, so the toolchain cost was zero.
|
|
Nothing is built; the ladder is untouched.
|
|
- 2026-09-17 — Off the ladder, third pass: geological simulation in the generator (D-46). `heightmap_erosion.py`,
|
|
numpy only: particle hydraulic erosion in vectorised batches, a coarse pass on the 4x-downsampled map for the
|
|
valleys and a fine pass at full resolution for the gullies; mass-conserving thermal weathering at a 35° angle
|
|
of repose; strata hardness scaling the erosion; flow, wear, deposition and curvature maps written as PNGs and
|
|
used by the layer rules. The uplift gained Worley cellular crest lines through the same domain warp. The
|
|
manifest's `erosion` block drives it; `"enabled": false` for a real DEM. Two things the smoke test caught:
|
|
droplets stepping together in a batch and crowding one cell run away to infinity without a per-step cap, and
|
|
thermal weathering that sheds half the mean excess converges far slower than half the largest excess.
|
|
- 2026-09-16 — Off the ladder, second pass: the world's terrain is now a manifest, `RawContent/World/World.json`
|
|
(D-44). `world_manifest.py` derives the landscape's Z scale and Z offset from an elevation range in metres so
|
|
world Z 0 is sea level; `heightmap_io.py` reads 16-bit PNG and raw r16 with numpy alone and resamples;
|
|
`heightmap_noise.py` builds the continent in metres (retuned: 80 % of the land under 15°, ranges on a fifth
|
|
of the map); `generate_heightmap.py` takes either the noise or a real heightmap named in the manifest and
|
|
derives the three Rocky Meadows layers from the height by slope and altitude, so a real heightmap needs no
|
|
weightmaps. `create_world.py` dresses `L_World` with the pack's kit as `dump_level.py` (new, dumps any level's
|
|
actors to JSON) read it from `Rocky_Meadows_01`: sun with the cloud-shadow light function, skybox dome, sky
|
|
light, height fog, fixed exposure and saturation; plus `World_Sea_Proto`, a plane with the engine's water
|
|
material. Resolution moved from 4033 to 4081 so the engine's importer picks 16x16 components of 255 quads
|
|
instead of 64x64 of 63 (D-45): the import takes two minutes instead of forty.
|
|
- 2026-09-16 — Off the ladder, at the user's request: `L_World`, a world-partitioned ~200 km² landscape from a
|
|
seeded heightmap. `Scripts/Authoring/generate_heightmap.py` (numpy, in `Scripts/Authoring/.pylib` via
|
|
`bootstrap-pylib.sh`) writes 16-bit height and 8-bit weight PNGs to `RawContent/World/Heightmaps/`;
|
|
`create_world.py` rebuilds the level from them through `ULandscapeAuthoringLibrary` in the new `SaltyEditor`
|
|
module, because the engine exposes no landscape creation to Python. Elite_RockyMeadows' landscape material and
|
|
three layer infos. Swap the terrain by replacing the PNGs and rerunning. Nothing in gameplay references it.
|
|
- 2026-09-16 — Step 2: `ITelemetrySink` with null, log and JSON Lines sinks; `FTelemetryEvent`, the envelope and
|
|
`Telemetry::ToJsonLine`; `TelemetryEvents` with the four session names; `UTelemetrySubsystem`; `ASaltyGameMode`
|
|
minting the session id into `ASaltyGameState`; `bs.TelemetryTest`; the git hash in the build string (D-42);
|
|
four Core tests. Proved standalone, and with a headless `-server` plus `-game` client sharing one session id.
|
|
- 2026-09-16 — The editor's MCP: the engine's experimental plugin only listens when `bAutoStartServer` is set, so
|
|
`.mcp.json` refused until `DefaultEditorPerProjectUserSettings.ini` set it (D-43). Enabled the Editor,
|
|
AutomationTest, GameplayTags, ConfigSettings and LiveCoding toolsets; before that the only toolset was skills.
|
|
Reconnect with `/mcp` after the editor is up. Live Coding must be closed before a shell build.
|
|
- 2026-09-16 — Step 1: `Salty.uproject` (UE 5.8) at the root from the Third Person template, renamed from
|
|
`ProjectSomething` with class redirects; `SaltyCore` and `Salty` modules; `SaltyServer.Target.cs` (unbuildable on
|
|
the launcher, OD-04); `USaltyAssetManager` calling `InitGlobalData`; `Config/Tags/` with 23 root namespaces;
|
|
Gameplay Abilities, CommonUI and Python enabled; `Scripts/run-tests.sh`, `build.sh`, `Authoring/create_gym.py`;
|
|
Git LFS attributes; the placeholder test. Template variants kept as reference (D-41). Four Fab packs in `Content/`,
|
|
none used yet (D-39).
|
|
- 2026-09-15 — `Spec/Stats.md`: one stat block on every body, every outside influence an effect, counters on the
|
|
receiver (D-37, D-38). Replaced the movement component's own speed-multiplier map, which was the first strand
|
|
of the spaghetti this exists to prevent; enemies and kits now carry `FStatBlockDefaults`; step 5 builds it.
|
|
- 2026-09-15 — The documentation set written for Unreal from the two earlier projects' docs: `Design.md`,
|
|
`Steps.md` (fifteen rungs, the first six in full), `Ideas.md`, `Decisions.md` (D-01 to D-36, OD-01 to OD-08),
|
|
and the eight specs under `Spec/`. No code, no project yet; step 1 is next and waits on OD-01 and OD-02.
|
|
|
|
## Worked
|
|
|
|
- (nothing built yet)
|
|
|
|
## Did not work
|
|
|
|
- 2026-09-17 — `"fill_every": 50` in the terrain spec, written to protect the five-minute budget, silently
|
|
destroyed the stream-power solve. Uplift reaches 5 mm/yr, which at dt 1500 is 7.5 m a step, so fifty steps
|
|
is up to 375 m of differential uplift between floods: basins close and sit unrouted while everything above
|
|
them stops eroding. Measured at 512² over 3000 steps the fitted exponent goes -0.500 (R2 0.992) at every
|
|
step, -0.277 at every fifth, -0.121 at every tenth, noise beyond. Even every fifth step is broken, and the
|
|
saving was 93 s against 38 s. The budget came back from the step count instead: 1000 steps, not the spec's
|
|
5000, because at this K the trunk response time is about 45 000 yr and the exponent stops moving after 500.
|
|
- 2026-09-17 — The heap in the priority-flood was the wrong data structure. The flood pops in non-decreasing
|
|
elevation and never pushes below the current front, which is exactly when a monotone bucket queue is valid;
|
|
swapping the binary heap for one took 1.6x off the whole solve at identical output. 22 comparisons and as
|
|
many cache misses per operation, on two thirds of the runtime.
|
|
- 2026-09-17 — Relief from the stream-power solve is strongly resolution-dependent: the same seed and uplift
|
|
field give 1020 m of land relief at 512² and 2605 m at 1786², because finer grids resolve smaller drainage
|
|
areas near the divides and S goes as A^(-m/n). Tuning U/K on a `--size` preview will therefore overshoot the
|
|
elevation ceiling at full resolution. Judge shape on the preview; confirm the elevation budget at the real
|
|
geology grid, where the clip warning is the check.
|
|
|
|
- 2026-09-16 — The first `L_World` rebuild of the evening died at twelve minutes with no error: it was run under a
|
|
tool with a ten-minute timeout, which killed the commandlet mid-import and left an empty level. Long
|
|
commandlets are launched detached (`Start-Process`) and watched through their log.
|
|
- 2026-09-16 — A rebuild while the editor had ever loaded `L_World` ended in a nameless `/Temp/Untitled` world
|
|
whose save went nowhere: deleting and recreating the level resaves `L_World_HLODLayer_Instanced` and
|
|
`_Merged`, and the open editor keeps those two files locked. `create_world.py` now loads and empties an
|
|
existing level instead of recreating it, checks the world's package name before building, and sweeps
|
|
leftover proxy packages after the save. Check the editor's current level (`SceneTools.get_current_level`
|
|
over MCP) before a rebuild; never rebuild the level the editor has open.
|
|
- 2026-09-16 — Epic's recommended 4033 resolution gives the engine's importer 64x64 components of 63 quads, because
|
|
4032 does not divide by 127 or 255 and the importer prefers one section per component: 4096 components, over
|
|
8000 textures to build, forty minutes. 4081 divides by 255: 256 components, two minutes.
|
|
- 2026-09-16 — The noise stacked eight octaves at gain 0.5, so every octave was as steep as the last and a third of
|
|
the land stood above 50°. Lower gains, no octave finer than about 50 m, a percentile-thresholded range mask
|
|
and a 38° thermal pass brought it to 80 % under 15°; measured with a slope histogram, not by eye.
|
|
- 2026-09-17 — Cellular (Worley) crest lines at 30 % of the mountain height turned the ranges into a honeycomb of
|
|
polygon walls with flat floors in the hillshade. Kept at 12 % through a stronger warp; the ridged noise and the
|
|
erosion make the divides.
|
|
- 2026-09-17 — The first eroded map had the meadows brushed with rills: land under 15° fell from 64 % to 38 %.
|
|
A stage-by-stage slope histogram (uplift, coarse pass, fine pass, thermal) put most of it on the coarse pass:
|
|
the slope gate sat at 7°, the median lowland slope, and one droplet could cut 7 m per step at 14 m cells.
|
|
Gate at 14°, cut capped at a fifth of a cell per step. Attribute by measurement before turning knobs.
|
|
- 2026-09-17 — Spreading droplet deposits through a 3x3 brush built mounds: a pit's rim rises faster than its
|
|
floor, the pit never fills, and every droplet draining into it adds its load to the rim. Cuts go through the
|
|
brush (no one-cell rills), deposits land on the droplet's own cell, and a droplet's load is capped, since
|
|
capacity scales with the drop per step and a cliff hands a droplet a hundred cell-heights.
|
|
- 2026-09-16 — The first `L_World` with the manifest pipeline showed rock everywhere: Elite_RockyMeadows' layer
|
|
names mislead. Its `Base_Layer` function samples the rock textures, `Layer_02` the grass, `Layer_03` the high
|
|
rock, and the meadow weightmap had gone to `Base_Layer`. Read a pack's layer functions before mapping layers.
|
|
- 2026-09-16 — The retuned noise overshot into flat land with small hills; the user wanted aggressive ranges.
|
|
Ranges now come from an elongated, warped, percentile-thresholded band (about two fifths of the land with
|
|
foothills), ridged noise sampled through the same warp, crests to about 2600 m.
|
|
- 2026-09-16 — `AActor.set_is_spatially_loaded` does not exist in Python; the property is set with
|
|
`set_editor_property("is_spatially_loaded", False)`.
|
|
- 2026-09-16 — Positional `unreal.Rotator(a, b, c)` is (roll, pitch, yaw), not (pitch, yaw, roll). Both authoring
|
|
scripts pitched the sun 25 to 30 degrees upward, every map rendered as night, and the viewport's "cached lighting
|
|
is going to be clipped" warning was the auto exposure at EV -8.5 saying so. Keyword arguments from now on.
|
|
- 2026-09-16 — A landscape imported from a commandlet had collision but no visible surface: edit layers are always
|
|
on in 5.8, the render heightmaps come from a GPU merge, and a commandlet reports it can never render unless
|
|
launched with `-AllowCommandletRendering`. `ULandscapeAuthoringLibrary` now forces the full layer update and
|
|
`create_world.py` documents the flag.
|
|
- 2026-09-16 — The asset library cannot delete a folder of world-partition streaming proxies (their packages do not
|
|
load on their own); `create_world.py` removes the folder on disk instead.
|
|
|
|
## Open
|
|
|
|
- **World, next time (2026-09-17, the user's verdict: "still somewhat rough").** Three items, none started:
|
|
1. *Sculptable terrain.* Today `create_world.py` empties the level and re-imports the heightmap into a fresh
|
|
landscape, so any hand sculpting dies with the next rerun. Wanted: the generated height applied to the
|
|
level as its base, with sculpting on top that survives a regeneration. The engine's shape for this is edit
|
|
layers: import the PNG into a named `Generated` layer and leave a `Sculpt` layer above it for hand work, then
|
|
make a rerun re-import into `Generated` only (`ALandscape` edit-layer API, `FLandscapeEditDataInterface`, or
|
|
`ULandscapeEditorObject::ImportHeightmap` per layer) instead of destroying the actor. Needs a C++ change in
|
|
`ULandscapeAuthoringLibrary` (a `ReimportHeightmapIntoLayer` beside `CreateLandscapeFromHeightmap`), so a
|
|
rebuild of `SaltyEditor` with the editor closed; the weightmaps follow the same path. Check first whether
|
|
the proxies' edit-layer data survives `ChangeGridSize`.
|
|
2. *Not visible in the editor.* The rebuilt terrain shows in PIE but the user reports it not visible in the
|
|
editor viewport. Most likely cause, to verify: the level is world-partitioned and the 256 landscape proxies
|
|
are spatially loaded, so the editor loads none of them until a region is loaded in the World Partition
|
|
window; only the always-loaded dressing shows. Candidates: World Settings → World Partition → turn
|
|
"Enable Streaming" off for now (everything loads, editor and runtime; 256 components is fine for a
|
|
prototype), or save loaded regions with the level, or have `create_world.py` load the regions after the
|
|
build. Also confirm the editor reloads `L_World` from disk after a rebuild rather than a stale in-memory copy.
|
|
3. *No greenery on the floor.* The meadow layer is a flat grass texture; nothing grows. No pack in `Content/`
|
|
has a grass mesh (Elite_RockyMeadows ships textures only, HouseForge's foliage folder holds one mushroom
|
|
and a cover-plant material). Wanted: landscape grass on the meadow layer (`ULandscapeGrassType` fed from a
|
|
`LandscapeGrassOutput` node, which means a child or copy of `M_Landscape_Main` since the pack's material has
|
|
none), driven by the erosion's deposit and flow maps once the material samples them, plus a foliage pass for
|
|
trees and bushes from a pack still to be chosen. Distance culling matters at 200 km².
|
|
- Step 1's last proof is a person's: Play In Editor in `L_Gym`, two players, "Run Dedicated Server" on, two pawns
|
|
in the gym. Until it is ticked the step is `◐`.
|
|
- The default camera mode (OD-03) is deliberately undecided until step 8.
|
|
- The numbers in every spec are the earlier projects' guesses in centimetres and seconds. None has been played.
|