Compare commits

...
12 Commits
Author SHA1 Message Date
JosephFerano c31dcca280 AGENTS: blocks (kept regions, registries, keys a section reads) 2026-10-20 16:28:44 +07:00
JosephFerano bfa3e1ce65 Moon bloom in GUI reference pixels: HiDPI does the 1x work and its glow looks like 1x 2026-10-20 16:28:23 +07:00
JosephFerano 59f26dc5af Blocks: kept blocks always look for their clip stencil again (a deleted node answers until the frame ends) 2026-10-20 16:19:24 +07:00
JosephFerano bb6b34c1b6 Moon generation only native (mmterrain): Lua generator and its fallback gone, no generation timing; moon-check compares mmterrain with terrain.js 2026-10-20 16:19:24 +07:00
JosephFerano 302bb2b764 Merge branch 'c-perf-moon' into c-perf 2026-10-20 15:46:14 +07:00
JosephFerano 90722d1c04 WIP: incremental rebuilds (blocks), reveal flight defers rebuilds 2026-10-20 12:39:57 +07:00
JosephFerano 22dca72f39 make check runs moon-check (terrain.lua and mmterrain against terrain.js); AGENTS: the mmterrain extension 2026-10-20 12:36:55 +07:00
JosephFerano 27f415d4af Moon meshes on the web: native work as a share of the frame time, the buffer filled over the next frames, grid 128 2026-10-20 12:36:55 +07:00
JosephFerano b8f2422433 mmterrain: fill a range of the strip; directions stepped too 2026-10-20 12:36:55 +07:00
JosephFerano 62ebb27d25 Moon meshes through mmterrain (a thread on desktop, 3 ms a frame on the web); a bought moon's mesh starts with the buy-moon response 2026-10-20 12:22:53 +07:00
JosephFerano 979470f306 mmterrain: the moon generator in C (native extension), checked against terrain.js and terrain.lua 2026-10-20 12:22:53 +07:00
JosephFerano 18f31b322a Moon generation: checkpoints in crater placement too, so no step of it runs unchunked 2026-10-20 12:22:53 +07:00
38 changed files with 2291 additions and 672 deletions

No files matched your search

+52 -16
View File
@@ -36,8 +36,17 @@ rules in Lua. Countdowns may tick locally as display only.
`screens/game/game.gui_script` only forwards the lifecycle to `screens/game/core/main.lua` `screens/game/game.gui_script` only forwards the lifecycle to `screens/game/core/main.lua`
(and on a hot reload re-requires it, see Fast iteration). The screen is rebuilt from the (and on a hot reload re-requires it, see Fast iteration). The screen is rebuilt from the
state: every new state (a response, a prediction) or a window/UI-scale change sets state: every new state (a response, a prediction) or a window/UI-scale change sets
`g.dirty`, and the next update drops the Druid instance and every layout node and builds `g.dirty`, and the next update builds again what changed. The regions (banner, header,
them again. `g` is the gui script's `self`. moon list, main panel, aside) and costly parts inside them (the market's ticker, chart and
sparklines; Store, Equipment and Advisors cards through `card.grid`'s `key`/`sig`) are
blocks (`modules/blocks.lua`): one whose signature (a serialization of everything it
shows: its state keys, the view, busy buttons, ready counts on our clock) is unchanged
keeps its nodes and Druid components. A full rebuild (drops the Druid instance and every
layout node) happens on the first state, a size change, the narrow layout and
`g.dirty_all` (theme, reduced motion, mute, scroll offsets set by code, hot reload). What
a region shows from the state is listed in `core/main.lua` (`SECTION_KEYS`, `ASIDE_KEYS`,
the header's and moon list's sigs): a section that reads a new state key must add it
there, or it won't update. `g` is the gui script's `self`.
Core (`screens/game/core/`): Core (`screens/game/core/`):
- `main.lua`: init (layers, session, helpers, autoplay), update (rebuild when dirty, - `main.lua`: init (layers, session, helpers, autoplay), update (rebuild when dirty,
@@ -107,23 +116,31 @@ table, past moon, first-moon pitch; well helpers `supply_bar`/`supply_max`/`tool
- optional `M.aside(g, root, w, h, H)` (section): fills the aside instead of the moon - optional `M.aside(g, root, w, h, H)` (section): fills the aside instead of the moon
viewport, return true when it did; or `M.aside_below(g, root, pad, cy, inner, limit, H)` viewport, return true when it did; or `M.aside_below(g, root, pad, cy, inner, limit, H)`
for content under the viewport. for content under the viewport.
- optional `M.update(g, dt)`: every frame. optional `M.teardown(g)`: before every rebuild - optional `M.update(g, dt)`: every frame. optional `M.teardown(g)`: before its region is
and when the screen goes away (cancel timers, drop references to nodes). built again and when the screen goes away (cancel timers, drop references to nodes).
- What a module gets on `g`: `state`, `view`, `size` ({W, H, ui scale}), `druid` (made - What a module gets on `g`: `state`, `view`, `size` ({W, H, ui scale}), `druid` (made
again every build), `fx`, `toasts`, `reveal`, `dialog`, `tween`, `fields` (text field again by a full rebuild), `fx`, `toasts`, `reveal`, `dialog`, `tween`, `fields` (text field
state), `scroll_pos` (scroll offsets by key), `pointers` (mouse-move handlers, reset state), `scroll_pos` (scroll offsets by key), `pointers` (mouse-move handlers, reset
every build), `frames` (cards for the main panel's staggered entrance), `moon_nodes`, every build), `frames` (cards for the main panel's staggered entrance), `moon_nodes`,
`well_nodes`, `tab_nodes` (for effects), `busy`, `inflight`. `well_nodes`, `tab_nodes` (for effects), `busy`, `inflight`.
- What must survive a rebuild lives on `g` (offsets, fields, eased values) or on a layer - What must survive a rebuild lives on `g` (offsets, fields, eased values) or on a layer
outside `g.layout` (toasts, reveal, dialog, effects). Never keep a layout node across outside `g.layout` (toasts, reveal, dialog, effects). Never keep a layout node across
builds. builds outside its block.
- Per-build registries (test ids and focus, tooltips, tickers, `g.inputs`,
`g.scroll_areas`, `g.pointers`, fx anchors, autoplay) are reset by every build; a kept
block makes its registrations again from a journal: a new registry must note them
(`blocks.note(fn, ...)`, or `blocks.push`/`blocks.put` for a list or map on g). Code
that builds outside a build (a list making rows as it scrolls, a field's on_change)
runs through `blocks.bound(fn)` so what it makes belongs to its block. A kept block
moved under another parent finds its buttons' clip stencil again.
- A new section: a module in `screens/game/`, an entry in `M.SECTIONS` - A new section: a module in `screens/game/`, an entry in `M.SECTIONS`
(`core/main.lua`), and a tab name in `header.lua` `SECTIONS` if it has one. (`core/main.lua`), and a tab name in `header.lua` `SECTIONS` if it has one.
Entrances (enter.js): the main panel enters when the section or moon changes, the aside Entrances (enter.js): the main panel enters when the section or moon changes, the aside
when the well or drill changes, both on the first state and after the reveal closes when the well or drill changes, both on the first state and after the reveal closes
(`g.enter_all`); cards registered in `g.frames` (components/card) follow, (`g.enter_all`); cards registered in `g.frames` (components/card) follow,
staggered. Not on other rebuilds. staggered. Not on other rebuilds. While the reveal card flies into the moon list (Start
mining) a new state waits for it to land.
## Components (`components/`) ## Components (`components/`)
Build new UI from these instead of coordinate math. Each file's header documents its API. Build new UI from these instead of coordinate math. Each file's header documents its API.
@@ -241,6 +258,19 @@ the markup colors). Fields refuse `<` and `&`. Measuring (`ui.text_width`,
(the engine's GLFW window to its monitor and back; Defold has no runtime fullscreen). (the engine's GLFW window to its monitor and back; Defold has no runtime fullscreen).
Changing it rebuilds the engine on the build server. On the web it builds as stubs; the Changing it rebuilds the engine on the build server. On the web it builds as stubs; the
browser's Fullscreen API is used instead (`modules/screen.lua`). browser's Fullscreen API is used instead (`modules/screen.lua`).
- `mmterrain/`: native extension, the moon generator in C (`src/terrain.c`, a port of
terrain.js's generation, same doubles in the same order; there is no Lua generator).
User rule: C only (`.c`, Lua C API); C++ only where the dmSDK forces it,
as a C-style shim (free functions and plain structs only: no classes, methods,
templates, STL, references, own namespaces, exceptions or RTTI; `src/buffer_shim.cpp`:
the buffer API). The extender compiles `.c`
files as C++ ("treating 'c' input as 'c++'"), so the C must also be valid C++ and
what links across files is `extern "C"`; the registration (`DM_DECLARE_EXTENSION` is
C++) is written out in `mmterrain.c`. Lua: `mmterrain.new(terrain.native_params(p))`,
`start` (a thread of its own where there are threads: desktop), `step(gen, secs)` (the
web: no threads), `done`, `vertices`, `fill(gen, buf[, from, to])` (the triangle strip
straight into the mesh buffer's streams), `result` (the flat arrays), `free` (header of
`mmterrain.c`). `moon.script` makes every moon mesh with it.
- `web/index.html`: the web build's page (`[html5] htmlfile`; see Web build). - `web/index.html`: the web build's page (`[html5] htmlfile`; see Web build).
- `screens/game/`: the game screen (above). Also `market.lua` (`ticker.lua` (pauses on hover; items not in the Tab order: `test.register(..., no_focus)`), `sale.lua` (the sell power-down, kept - `screens/game/`: the game screen (above). Also `market.lua` (`ticker.lua` (pauses on hover; items not in the Tab order: `test.register(..., no_focus)`), `sale.lua` (the sell power-down, kept
by time on g and rebound every build; fx.lua asks `g.on_sale` for the coins' origin), by time on g and rebound every build; fx.lua asks `g.on_sale` for the coins' origin),
@@ -288,9 +318,12 @@ file),
other two lack: `₥`, `★`, `◆`, `▲`, `▼`, `●`, `›`, `≈`, Greek); distance field, built other two lack: `₥`, `★`, `◆`, `▲`, `▼`, `●`, `›`, `≈`, Greek); distance field, built
at 40px and scaled. OFL 1.1. `assets/ui.atlas`: white, frame, outline, sphere, glow, at 40px and scaled. OFL 1.1. `assets/ui.atlas`: white, frame, outline, sphere, glow,
fade, ring, spark, speaker, sun and moon icons; tinted per node. fade, ring, spark, speaker, sun and moon icons; tinted per node.
- The moon viewport: `modules/moon/terrain.lua` ports the server's `terrain.js` - The moon viewport: `modules/moon/terrain.lua` ports the settings side of the server's
(`scripts/moon-check.mjs` compares them), `modules/moon/moon.lua` is the API `terrain.js` (defaults, resolve, traits, colors, the cube-sphere and its strip), and
(`moon.show(id, node)`, `hide`, `mystery`, `reveal`, `skip_reveal`, `revealing`), `mmterrain/` the generation, in C (`scripts/moon-check.mjs` compares them with
terrain.js, many ids and settings; in `make check`), `modules/moon/moon.lua` is the API
(`moon.show(id, node)`, `hide`, `mystery`, `reveal`, `skip_reveal`, `revealing`,
`prefetch`: reveal.lua starts a bought moon's mesh as the buy-moon answer arrives),
`moon/moon.collection` (`moon.script`: meshes, spin, drag, zoom, shroud, reveal), `moon/moon.collection` (`moon.script`: meshes, spin, drag, zoom, shroud, reveal),
`moon/materials` (port of scene.js). `render/mm.render_script` draws the moon into an `moon/materials` (port of scene.js). `render/mm.render_script` draws the moon into an
HDR target with bloom; the viewport node shows it through the `moon` gui material. HDR target with bloom; the viewport node shows it through the `moon` gui material.
@@ -317,9 +350,10 @@ fetches) to the server, so the game talks to its own origin; the server stays un
server, which must allow CORS); the player id and settings are saved with sys.save server, which must allow CORS); the player id and settings are saved with sys.save
(IndexedDB), X-MM-User sent as on desktop (the cookie works too, same origin). (IndexedDB), X-MM-User sent as on desktop (the cookie works too, same origin).
Fullscreen (F11, display dialog) through the browser's Fullscreen API, not saved or Fullscreen (F11, display dialog) through the browser's Fullscreen API, not saved or
restored (the browser allows it only after a click or key). Moon generation is plain restored (the browser allows it only after a click or key). Moon generation is the
Lua there (no LuaJIT): grid capped at 96 (`WEB_RESOLUTION`) and 8 ms a frame (keeps mmterrain extension in wasm (no threads there): stepped with a share of each frame
60 fps; a checkpoint every few ms of work in terrain.lua), about 4 s per moon. Moon targets fall back to 8 bits without EXT_color_buffer_float. (30% of the frame time, 2–12 ms: ~2 ms at 144 Hz), the buffer filled over the next
frames, grid capped at 128 (`WEB_RESOLUTION` in moon.script). Moon targets fall back to 8 bits without EXT_color_buffer_float.
Variable frame rate there (`sys.set_update_frequency(0)` in main.script): the browser's Variable frame rate there (`sys.set_update_frequency(0)` in main.script): the browser's
requestAnimationFrame paces at the display's rate; `display.update_frequency = 60` requestAnimationFrame paces at the display's rate; `display.update_frequency = 60`
(desktop's timer pacing) made the engine skip callbacks: 60 fps in a 2/3 cadence on (desktop's timer pacing) made the engine skip callbacks: 60 fps in a 2/3 cadence on
@@ -337,7 +371,7 @@ fetches) to the server, so the game talks to its own origin; the server stays un
## Commands ## Commands
- `make build`: `bob build bundle` (debug) into `dist/x86_64-linux/MoonMiners/` (`build/` is - `make build`: `bob build bundle` (debug) into `dist/x86_64-linux/MoonMiners/` (`build/` is
bob's output; `build/x86_64-linux/dmengine` is the engine with our extension). bob's output; `build/x86_64-linux/dmengine` is the engine with our extensions).
- `make run ARGS="--config=mm.api_base=http://localhost:3001"`: build and start. - `make run ARGS="--config=mm.api_base=http://localhost:3001"`: build and start.
- `make web`, `make serve-web`: the browser build (Web build). - `make web`, `make serve-web`: the browser build (Web build).
- `make shot`: run under `xvfb-run` at 1280x800, save `dist/shot.png`, quit. `SHOT=`, - `make shot`: run under `xvfb-run` at 1280x800, save `dist/shot.png`, quit. `SHOT=`,
@@ -461,8 +495,10 @@ Fails on: Lua syntax errors (every `.lua`, `.script`, `.gui_script`, `.render_sc
luajit); stale debug flags (`[mm]` keys or values in `game.project` other than the shipped luajit); stale debug flags (`[mm]` keys or values in `game.project` other than the shipped
defaults, `DEBUG`/`XXX`/`HACK` markers in sources and shaders); characters the fonts lack defaults, `DEBUG`/`XXX`/`HACK` markers in sources and shaders); characters the fonts lack
(non-ASCII in Lua string literals: in the sym font or in the text fonts; server texts of (non-ASCII in Lua string literals: in the sym font or in the text fonts; server texts of
a fixture player in every section: symbols in sym, the rest in the text fonts); and a fixture player in every section: symbols in sym, the rest in the text fonts); the moon
engine warnings in a `sections` scenario log (font batches, characters, text entries, generator (`node scripts/moon-check.mjs [resolution] [seeds...]`:
`mmterrain/src/terrain.c`, built for the host with `cc` and loaded into luajit through the
FFI, and terrain.lua's settings side, against terrain.js; needs node, luajit, cc); and engine warnings in a `sections` scenario log (font batches, characters, text entries,
nodes, animations, stencil, invalid markup tags, script errors). The scenario runs nodes, animations, stencil, invalid markup tags, script errors). The scenario runs
against `MM_API` (default http://localhost:3000) when it answers; otherwise the newest against `MM_API` (default http://localhost:3000) when it answers; otherwise the newest
`dist/drive/*/game.log` is checked. `scripts/check.py --static` skips it. `dist/drive/*/game.log` is checked. `scripts/check.py --static` skips it.
+50 -9
View File
@@ -31,6 +31,7 @@ local images = require("modules.images")
local anim = require("modules.anim") local anim = require("modules.anim")
local button = require("components.button") local button = require("components.button")
local panel = require("components.panel") local panel = require("components.panel")
local blocks = require("modules.blocks")
local S = theme.size local S = theme.size
@@ -209,8 +210,11 @@ function M:set_height(h)
end end
-- A grid of cards from y; returns the y under it. make(c, item) fills one card. -- A grid of cards from y; returns the y under it. make(c, item) fills one card.
function M.grid(g, parent, x, y, inner, items, make) -- opts.key(item): keep the card (modules/blocks) while opts.sig(item) (what make shows
-- besides the item; default the item alone) and its place stay the same.
function M.grid(g, parent, x, y, inner, items, make, opts)
local cols, cw = M.columns(inner) local cols, cw = M.columns(inner)
local keyed = opts and opts.key
local i = 1 local i = 1
while i <= #items do while i <= #items do
local row = {} local row = {}
@@ -219,18 +223,55 @@ function M.grid(g, parent, x, y, inner, items, make)
if not item then if not item then
break break
end end
local c = M.new(g, parent, x + (col - 1) * (cw + S.gap), y, cw) local r = { item = item, x = x + (col - 1) * (cw + S.gap) }
make(c, item) if keyed then
c:refresh() r.key = opts.key(item)
row[#row + 1] = c r.sig = blocks.sig(r.x, y, cw, item, opts.sig and opts.sig(item) or false)
r.old = blocks.peek(g, r.key, r.sig) -- (kept, if its row's height stays the same)
end
if r.old then
r.natural = r.old.card.natural
else
if r.key then
r.blk = blocks.open(g, r.key, r.sig, parent)
end
r.c = M.new(g, r.blk and r.blk.root or parent, r.x, y, cw)
make(r.c, item)
r.natural = r.c:refresh()
if r.blk then
r.blk.card = r.c
blocks.close(g, r.blk)
end
end
row[#row + 1] = r
i = i + 1 i = i + 1
end end
local rh = 0 local rh = 0
for _, c in ipairs(row) do for _, r in ipairs(row) do
rh = math.max(rh, c.natural) rh = math.max(rh, r.natural)
end end
for _, c in ipairs(row) do for _, r in ipairs(row) do
c:set_height(rh) if r.old and r.old.card.h == rh then
r.blk = blocks.open(g, r.key, r.sig, parent) -- (kept)
r.c = r.blk.card
else
if r.old then
-- The row's height changed: made again at it.
blocks.drop(g, r.old)
r.blk = blocks.open(g, r.key, r.sig, parent)
r.c = M.new(g, r.blk.root, r.x, y, cw)
make(r.c, r.item)
r.c:refresh()
r.blk.card = r.c
elseif r.blk then
blocks.reopen(g, r.blk) -- (its hover and footer are the card's)
end
r.c:set_height(rh)
if r.blk then
blocks.close(g, r.blk)
end
end
local c = r.c
if g.frames then if g.frames then
g.frames[#g.frames + 1] = panel.frame(c.root, c.edge, cw, rh) -- staggered entrance g.frames[#g.frames + 1] = panel.frame(c.root, c.edge, cw, rh) -- staggered entrance
end end
+3 -3
View File
@@ -15,6 +15,7 @@
-- places it. A Druid component returned with it (a widget) is removed with the row. -- places it. A Druid component returned with it (a widget) is removed with the row.
local ui = require("modules.ui") local ui = require("modules.ui")
local blocks = require("modules.blocks")
local M = {} local M = {}
M.__index = M M.__index = M
@@ -24,7 +25,7 @@ local function vec(x, y)
end end
function M.new(g, parent, x, y, w, h, opts) function M.new(g, parent, x, y, w, h, opts)
local l = setmetatable({ g = g, parent = parent, x = x, y = y, w = w, h = h, key = opts.key, create = opts.create, local l = setmetatable({ g = g, parent = parent, x = x, y = y, w = w, h = h, key = opts.key, create = blocks.bound(opts.create), -- (rows made as it scrolls are its block's)
focus_id = opts.focus_id, row_h = opts.row_h, items = {} }, M) focus_id = opts.focus_id, row_h = opts.row_h, items = {} }, M)
g.scroll_pos = g.scroll_pos or {} g.scroll_pos = g.scroll_pos or {}
l.view = ui.rect(parent, x, y, w, h, "text") l.view = ui.rect(parent, x, y, w, h, "text")
@@ -49,8 +50,7 @@ function M.new(g, parent, x, y, w, h, opts)
g.scroll_pos[l.key] = pos.y g.scroll_pos[l.key] = pos.y
l:place_thumb(pos.y) l:place_thumb(pos.y)
end) end)
g.scroll_areas = g.scroll_areas or {} blocks.push(g, "scroll_areas", { view = l.view, content = l.content, key = l.key, max = 0, h = h, list = l, scroll = l.scroll })
g.scroll_areas[#g.scroll_areas + 1] = { view = l.view, content = l.content, key = l.key, max = 0, h = h, list = l, scroll = l.scroll }
return l return l
end end
+3 -3
View File
@@ -5,7 +5,7 @@
-- ... build into area.content (top-left based, like any parent) ... -- ... build into area.content (top-left based, like any parent) ...
-- area:finish(content_height) -- area:finish(content_height)
-- --
-- Screens rebuild their layout on every new state, so the offset is kept per key in -- A section built again makes its scroll area again, so the offset is kept per key in
-- self.scroll_pos and restored on the next build. Buttons inside take the view as their -- self.scroll_pos and restored on the next build. Buttons inside take the view as their
-- click zone (Druid does that for nodes under a stencil), so hidden parts can't be -- click zone (Druid does that for nodes under a stencil), so hidden parts can't be
-- clicked. Mouse wheel and drag scroll; a thin bar on the right shows where you are; its -- clicked. Mouse wheel and drag scroll; a thin bar on the right shows where you are; its
@@ -13,6 +13,7 @@
-- g.scroll_areas (the test hook's scroll_to brings an element into view). -- g.scroll_areas (the test hook's scroll_to brings an element into view).
local ui = require("modules.ui") local ui = require("modules.ui")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -75,8 +76,7 @@ function Area:finish(content_h)
gui.animate(thumb, "size.x", on and BAR_W + 2 or BAR_W, gui.EASING_OUTQUAD, 0.12) gui.animate(thumb, "size.x", on and BAR_W + 2 or BAR_W, gui.EASING_OUTQUAD, 0.12)
end) end)
local s = self_.druid:new_scroll(self.view, self.content) local s = self_.druid:new_scroll(self.view, self.content)
self_.scroll_areas = self_.scroll_areas or {} blocks.push(self_, "scroll_areas", { view = self.view, content = self.content, key = self.key, max = max, h = h, scroll = s })
self_.scroll_areas[#self_.scroll_areas + 1] = { view = self.view, content = self.content, key = self.key, max = max, h = h, scroll = s }
s:set_horizontal_scroll(false) s:set_horizontal_scroll(false)
s:set_extra_stretch_size(0) s:set_extra_stretch_size(0)
+1
View File
@@ -0,0 +1 @@
name: "MMTerrain"
+37
View File
@@ -0,0 +1,37 @@
// The two calls mmterrain.c needs from the engine's C++-only buffer API, as C functions:
// the float32 x3 streams of the buffer at a Lua stack index, and marking it changed.
#include <dmsdk/sdk.h>
static float* stream(dmBuffer::HBuffer buf, const char* name, uint32_t* count, uint32_t* stride)
{
void* data = 0;
uint32_t components = 0;
if (dmBuffer::GetStream(buf, dmHashString64(name), &data, count, &components, stride) != dmBuffer::RESULT_OK)
return 0;
if (components != 3)
return 0;
dmBuffer::ValueType type;
if (dmBuffer::GetStreamType(buf, dmHashString64(name), &type, &components) != dmBuffer::RESULT_OK
|| type != dmBuffer::VALUE_TYPE_FLOAT32)
return 0;
return (float*)data;
}
extern "C" int mm_buffer_streams(lua_State* L, int index, float** pos, float** nor, float** col, unsigned* count, unsigned* stride)
{
dmBuffer::HBuffer buf = dmScript::CheckBufferUnpack(L, index);
uint32_t c[3], s[3];
*pos = stream(buf, "position", &c[0], &s[0]);
*nor = stream(buf, "normal", &c[1], &s[1]);
*col = stream(buf, "color", &c[2], &s[2]);
if (!*pos || !*nor || !*col || s[0] != s[1] || s[0] != s[2])
return 0;
*count = c[0];
*stride = s[0];
return 1;
}
extern "C" void mm_buffer_touch(lua_State* L, int index)
{
dmBuffer::UpdateContentVersion(dmScript::CheckBufferUnpack(L, index));
}
+201
View File
@@ -0,0 +1,201 @@
/* mmterrain: the moon generator (terrain.c) for Lua. Plain C; the engine's buffer API is
* C++ only, so buffer_shim.cpp reaches a buffer's streams for mmterrain.fill.
*
* local gen = mmterrain.new(terrain.native_params(p)) -- p: terrain.resolve's result
* mmterrain.start(gen) -> true: it runs on a thread of its own (desktop);
* false: step it (web)
* mmterrain.step(gen, secs) -> true when done (works about secs; 0: to the end)
* mmterrain.done(gen) -> true when done (threaded or stepped)
* mmterrain.vertices(gen) -> the triangle strip's length (terrain.strip)
* mmterrain.fill(gen, buf [, from, to]) fills buf's position, normal and color
* streams (float32 x3, at least vertices(gen) long) with the
* strip: its vertices from..to (1-based, inclusive; default
* all)
* mmterrain.result(gen) -> { n, count, positions, normals, colors, min_h, max_h }
* mmterrain.free(gen) frees it now (else the garbage collector does)
* Don't call anything but done/free on a started generator until done is true.
*/
#include <dmsdk/extension/extension.h>
#include <dmsdk/dlib/align.h>
#include "terrain.h"
#define META "mmterrain.gen"
/* The extender compiles .c files in C++ mode (clang++, "treating 'c' input as 'c++'"), so
* this file and terrain.c are valid as both; what links across files is extern "C". */
#ifdef __cplusplus
extern "C" {
#endif
/* buffer_shim.cpp: a buffer's three float32 x3 streams, or 0. */
int mm_buffer_streams(lua_State* L, int index, float** pos, float** nor, float** col, unsigned* count, unsigned* stride);
void mm_buffer_touch(lua_State* L, int index);
void MMTerrain(void);
#ifdef __cplusplus
}
#endif
static mt_gen** check_slot(lua_State* L)
{
return (mt_gen**)luaL_checkudata(L, 1, META);
}
static mt_gen* check_gen(lua_State* L)
{
mt_gen** slot = check_slot(L);
if (!*slot)
luaL_error(L, "mmterrain: generator already freed");
return *slot;
}
static mt_gen* check_done(lua_State* L)
{
mt_gen* g = check_gen(L);
if (!mt_done(g))
luaL_error(L, "mmterrain: generation not done");
return g;
}
static int New(lua_State* L)
{
double a[MT_PARAM_COUNT];
mt_params p;
mt_gen** slot;
int i;
luaL_checktype(L, 1, LUA_TTABLE);
for (i = 0; i < MT_PARAM_COUNT; ++i) {
lua_rawgeti(L, 1, i + 1);
a[i] = luaL_checknumber(L, -1);
lua_pop(L, 1);
}
mt_params_from_array(a, MT_PARAM_COUNT, &p);
slot = (mt_gen**)lua_newuserdata(L, sizeof(mt_gen*));
*slot = 0;
luaL_getmetatable(L, META);
lua_setmetatable(L, -2);
*slot = mt_new(&p);
if (!*slot)
return luaL_error(L, "mmterrain: out of memory");
return 1;
}
static int Start(lua_State* L)
{
lua_pushboolean(L, mt_start(check_gen(L)));
return 1;
}
static int Step(lua_State* L)
{
mt_gen* g = check_gen(L);
lua_pushboolean(L, mt_done(g) || mt_step(g, luaL_optnumber(L, 2, 0)));
return 1;
}
static int Done(lua_State* L)
{
lua_pushboolean(L, mt_done(check_gen(L)));
return 1;
}
static int Vertices(lua_State* L)
{
lua_pushinteger(L, mt_strip_len(mt_n(check_gen(L))));
return 1;
}
static int Fill(lua_State* L)
{
mt_gen* g = check_done(L);
float *pos, *nor, *col;
unsigned count, stride;
if (!mm_buffer_streams(L, 2, &pos, &nor, &col, &count, &stride))
return luaL_error(L, "mmterrain.fill: needs position, normal and color streams (float32 x3)");
int len = mt_strip_len(mt_n(g));
int from = (int)luaL_optinteger(L, 3, 1) - 1, to = (int)luaL_optinteger(L, 4, len);
if (count < (unsigned)len)
return luaL_error(L, "mmterrain.fill: buffer too short");
mt_fill(g, pos, nor, col, (int)stride, from < 0 ? 0 : from, to > len ? len : to);
mm_buffer_touch(L, 2);
return 0;
}
static void push_array(lua_State* L, const double* a, int n)
{
int i;
lua_createtable(L, n, 0);
for (i = 0; i < n; ++i) {
lua_pushnumber(L, a[i]);
lua_rawseti(L, -2, i + 1);
}
}
static int Result(lua_State* L)
{
mt_gen* g = check_done(L);
int c = mt_count(g);
lua_createtable(L, 0, 7);
lua_pushinteger(L, mt_n(g));
lua_setfield(L, -2, "n");
lua_pushinteger(L, c);
lua_setfield(L, -2, "count");
push_array(L, mt_positions(g), c * 3);
lua_setfield(L, -2, "positions");
push_array(L, mt_normals(g), c * 3);
lua_setfield(L, -2, "normals");
push_array(L, mt_colors(g), c * 3);
lua_setfield(L, -2, "colors");
lua_pushnumber(L, mt_min_h(g));
lua_setfield(L, -2, "min_h");
lua_pushnumber(L, mt_max_h(g));
lua_setfield(L, -2, "max_h");
return 1;
}
static int Free(lua_State* L)
{
mt_gen** slot = check_slot(L);
mt_free(*slot); /* (joins a running thread) */
*slot = 0;
return 0;
}
static const luaL_Reg Module_methods[] = {
{ "new", New },
{ "start", Start },
{ "step", Step },
{ "done", Done },
{ "vertices", Vertices },
{ "fill", Fill },
{ "result", Result },
{ "free", Free },
{ 0, 0 },
};
static ExtensionResult Initialize(ExtensionParams* params)
{
lua_State* L = params->m_L;
int top = lua_gettop(L);
luaL_newmetatable(L, META);
lua_pushcfunction(L, Free);
lua_setfield(L, -2, "__gc");
lua_pop(L, 1);
luaL_register(L, "mmterrain", Module_methods);
lua_pop(L, 1);
(void)top;
return EXTENSION_RESULT_OK;
}
static ExtensionResult Finalize(ExtensionParams* params)
{
(void)params;
return EXTENSION_RESULT_OK;
}
/* DM_DECLARE_EXTENSION, written out for C (the macro's function is extern "C"). */
static uint8_t DM_ALIGNED(16) g_Desc[128]; /* ExtensionDescBufferSize (a const, not a C constant) */
void MMTerrain(void)
{
ExtensionRegister((void*)g_Desc, sizeof(g_Desc), "MMTerrain", 0, 0, Initialize, Finalize, 0, 0);
}
+830
View File
@@ -0,0 +1,830 @@
/* The moon generator (see terrain.h), a port of the server's web/static/js/terrain.js:
* keep the two in sync (scripts/moon-check.mjs compares them). */
#if !defined(_WIN32) && !defined(_POSIX_C_SOURCE)
#define _POSIX_C_SOURCE 199309L /* clock_gettime */
#endif
#include "terrain.h"
#include <math.h>
#if MT_THREADS
#include <pthread.h>
#endif
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#if defined(_WIN32)
#include <windows.h>
#else
#include <time.h>
#endif
/* The same rounding as the JS: no fused multiply-adds. */
#if defined(__clang__)
#pragma clang fp contract(off)
#elif defined(__GNUC__)
#pragma GCC optimize("fp-contract=off")
#endif
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
#define MAX_OCTAVES 10
#define MAX_CRATERS 2000
#define GRID 12
static double clock_secs(void)
{
#if defined(_WIN32)
LARGE_INTEGER f, c;
QueryPerformanceFrequency(&f);
QueryPerformanceCounter(&c);
return (double)c.QuadPart / (double)f.QuadPart;
#else
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + ts.tv_nsec * 1e-9;
#endif
}
static double dmin(double a, double b) { return a < b ? a : b; }
static double dmax(double a, double b) { return a > b ? a : b; }
static double lround_(double x) { return floor(x + 0.5); }
/* ------------------------------------------------------------ random */
/* mulberry32, uniform in [0, 1). */
typedef struct { uint32_t a; } mt_rng;
static mt_rng rng_new(double seed)
{
mt_rng r;
/* bit.tobit: the number modulo 2^32 */
r.a = (uint32_t)(int64_t)fmod(seed, 4294967296.0);
return r;
}
static double rng_next(mt_rng* r)
{
uint32_t t;
r->a += 0x6d2b79f5u;
t = (r->a ^ (r->a >> 15)) * (1u | r->a);
t = (t + (t ^ (t >> 7)) * (61u | t)) ^ t;
return (double)(t ^ (t >> 14)) / 4294967296.0;
}
/* ------------------------------------------------------------ noise */
static const double GRAD3[36] = {
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1,
};
static int PERM[512];
static double GX[256], GY[256], GZ[256];
static int perm_ready = 0;
static void perm_init(void)
{
int p[256], i;
mt_rng r;
if (perm_ready)
return;
for (i = 0; i < 256; ++i)
p[i] = i;
r = rng_new(1337);
for (i = 255; i >= 1; --i) {
int j = (int)floor(rng_next(&r) * (i + 1));
int t = p[i];
p[i] = p[j];
p[j] = t;
}
for (i = 0; i < 512; ++i)
PERM[i] = p[i & 255];
for (i = 0; i < 256; ++i) {
int g = (i % 12) * 3;
GX[i] = GRAD3[g];
GY[i] = GRAD3[g + 1];
GZ[i] = GRAD3[g + 2];
}
perm_ready = 1;
}
#define F3 (1.0 / 3.0)
#define G3 (1.0 / 6.0)
/* floor without a libm call (x86_64's baseline has no rounding instruction); the same
* value for |x| < 2^53. */
static double ffloor(double x)
{
double t = (double)(int64_t)x;
return t > x ? t - 1 : t;
}
/* terrain.lua simplex3 (branch-free, the same values as the reference's if/else chains,
* and faster here too: the corner order is unpredictable). */
static double simplex3(double x, double y, double z)
{
const int* P = PERM;
double s = (x + y + z) * F3;
double i = ffloor(x + s), j = ffloor(y + s), k = ffloor(z + s);
double t = (i + j + k) * G3;
double x0 = x - (i - t), y0 = y - (j - t), z0 = z - (k - t);
double a = x0 >= y0 ? 1 : 0; /* 1 + floor(x0 - y0) in the Lua: |x0 - y0| < 1 */
double b = y0 >= z0 ? 1 : 0;
double c = x0 >= z0 ? 1 : 0;
double i1 = a * (b + (1 - b) * c);
double j1 = (1 - a) * b;
double k1 = 1 - i1 - j1;
double i2 = a + (1 - a) * b * c;
double j2 = a * b + 1 - a;
double k2 = 2 - i2 - j2;
int ii = (int)((int64_t)i & 255), jj = (int)((int64_t)j & 255), kk = (int)((int64_t)k & 255);
int ii1 = (int)i1, jj1 = (int)j1, kk1 = (int)k1, ii2 = (int)i2, jj2 = (int)j2, kk2 = (int)k2;
double tt, n, x1, y1, z1, x2, y2, z2, x3, y3, z3;
int g;
tt = dmax(0, 0.6 - x0 * x0 - y0 * y0 - z0 * z0);
g = P[ii + P[jj + P[kk]]];
tt = tt * tt;
n = tt * tt * (GX[g] * x0 + GY[g] * y0 + GZ[g] * z0);
x1 = x0 - i1 + G3; y1 = y0 - j1 + G3; z1 = z0 - k1 + G3;
tt = dmax(0, 0.6 - x1 * x1 - y1 * y1 - z1 * z1);
g = P[ii + ii1 + P[jj + jj1 + P[kk + kk1]]];
tt = tt * tt;
n = n + tt * tt * (GX[g] * x1 + GY[g] * y1 + GZ[g] * z1);
x2 = x0 - i2 + 2 * G3; y2 = y0 - j2 + 2 * G3; z2 = z0 - k2 + 2 * G3;
tt = dmax(0, 0.6 - x2 * x2 - y2 * y2 - z2 * z2);
g = P[ii + ii2 + P[jj + jj2 + P[kk + kk2]]];
tt = tt * tt;
n = n + tt * tt * (GX[g] * x2 + GY[g] * y2 + GZ[g] * z2);
x3 = x0 - 1 + 3 * G3; y3 = y0 - 1 + 3 * G3; z3 = z0 - 1 + 3 * G3;
tt = dmax(0, 0.6 - x3 * x3 - y3 * y3 - z3 * z3);
g = P[ii + 1 + P[jj + 1 + P[kk + 1]]];
tt = tt * tt;
n = n + tt * tt * (GX[g] * x3 + GY[g] * y3 + GZ[g] * z3);
return 32 * n;
}
static double fbm(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity)
{
double sum = 0, amp = 1, norm = 0, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
for (o = 1; o <= top; o += 1) {
sum = sum + simplex3(x * f, y * f, z * f) * amp;
norm = norm + amp;
amp = amp * persistence;
f = f * lacunarity;
}
return sum / norm;
}
static double billow(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity)
{
double sum = 0, amp = 1, norm = 0, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
for (o = 1; o <= top; o += 1) {
sum = sum + fabs(simplex3(x * f, y * f, z * f)) * amp;
norm = norm + amp;
amp = amp * persistence;
f = f * lacunarity;
}
return sum / norm;
}
static double ridged(double x, double y, double z, double octaves, double frequency, double persistence, double lacunarity,
double sharpness, double gain)
{
double sum = 0, amp = 1, norm = 0, weight = 1, f = frequency, o, top = dmin(octaves, MAX_OCTAVES);
for (o = 1; o <= top; o += 1) {
double v = 1 - fabs(simplex3(x * f, y * f, z * f));
v = pow(v, sharpness) * weight;
weight = dmin(1, dmax(0, v * gain));
sum = sum + v * amp;
norm = norm + amp;
amp = amp * persistence;
f = f * lacunarity;
}
return sum / norm;
}
static double smooth_min(double a, double b, double k)
{
double h;
if (k <= 0)
return dmin(a, b);
h = dmin(1, dmax(0, (b - a + k) / (2 * k)));
return a * h + b * (1 - h) - k * h * (1 - h);
}
static double smooth_max(double a, double b, double k)
{
return -smooth_min(-a, -b, k);
}
static double smoothstep(double e0, double e1, double x)
{
double d = e1 - e0, t;
if (d == 0)
d = 1e-6;
t = dmin(1, dmax(0, (x - e0) / d));
return t * t * (3 - 2 * t);
}
/* ------------------------------------------------------------ colors */
int mt_params_from_array(const double* a, int n, mt_params* p)
{
int i = 0, k;
if (n < MT_PARAM_COUNT)
return 0;
p->resolution = a[i++];
p->technique = (int)a[i++];
p->shape_strength = a[i++]; p->shape_frequency = a[i++]; p->shape_octaves = a[i++];
p->shape_persistence = a[i++]; p->shape_lacunarity = a[i++]; p->shape_min = a[i++];
p->ridge_strength = a[i++]; p->ridge_frequency = a[i++]; p->ridge_octaves = a[i++];
p->ridge_persistence = a[i++]; p->ridge_lacunarity = a[i++]; p->ridge_sharpness = a[i++];
p->ridge_gain = a[i++]; p->ridge_min = a[i++];
p->warp_strength = a[i++]; p->warp_frequency = a[i++]; p->warp_octaves = a[i++];
p->crater_count = a[i++]; p->crater_min_radius = a[i++]; p->crater_max_radius = a[i++];
p->crater_size_bias = a[i++]; p->crater_depth = a[i++]; p->crater_rim_width = a[i++];
p->crater_rim_steepness = a[i++]; p->crater_floor = a[i++]; p->crater_smoothness = a[i++];
p->crater_ejecta = a[i++];
for (k = 0; k < 3; ++k) p->low[k] = a[i++];
for (k = 0; k < 3; ++k) p->mid[k] = a[i++];
for (k = 0; k < 3; ++k) p->high[k] = a[i++];
for (k = 0; k < 3; ++k) p->steep[k] = a[i++];
for (k = 0; k < 3; ++k) p->patch[k] = a[i++];
p->steep_threshold = a[i++]; p->steep_blend = a[i++]; p->patch_amount = a[i++];
p->patch_frequency = a[i++]; p->grain = a[i++];
for (k = 0; k < 3; ++k) p->offset[k] = a[i++];
p->crater_seed = a[i++];
return 1;
}
/* ------------------------------------------------------------ generator */
typedef struct crater {
double x, y, z, r, reach;
} crater;
enum { ST_DIRS, ST_CRATERS, ST_HEIGHTS, ST_MEAN, ST_POSITIONS, ST_NORMALS, ST_SEAMS, ST_NORMALIZE, ST_COLORS, ST_DONE };
struct mt_gen {
mt_params p;
#if MT_THREADS
pthread_t thread;
int threaded;
int finished; /* (set by the thread, read with atomics) */
#endif
int n, row, per, count;
int stage, i;
double* dirs;
int* keys;
double *heights, *ejecta, *positions, *normals, *colors;
crater* craters;
int n_craters;
int* cell_start; /* GRID^3 + 1 offsets into cell_list */
int* cell_list;
double x_max, mean, min_h, max_h;
};
static const double FACES[6][9] = {
{ 1, 0, 0, 0, 1, 0, 0, 0, 1 },
{ -1, 0, 0, 0, 0, 1, 0, 1, 0 },
{ 0, 1, 0, 0, 0, 1, 1, 0, 0 },
{ 0, -1, 0, 1, 0, 0, 0, 0, 1 },
{ 0, 0, 1, 1, 0, 0, 0, 1, 0 },
{ 0, 0, -1, 0, 1, 0, 1, 0, 0 },
};
mt_gen* mt_new(const mt_params* p)
{
mt_gen* g = (mt_gen*)calloc(1, sizeof(mt_gen));
size_t c3;
if (!g)
return 0;
perm_init();
g->p = *p;
g->n = (int)dmax(8, dmin(320, lround_(p->resolution)));
g->row = g->n + 1;
g->per = g->row * g->row;
g->count = 6 * g->per;
c3 = (size_t)g->count * 3;
g->dirs = (double*)malloc(c3 * sizeof(double));
g->keys = (int*)malloc((size_t)g->count * sizeof(int));
g->heights = (double*)malloc((size_t)g->count * sizeof(double));
g->ejecta = (double*)malloc((size_t)g->count * sizeof(double));
g->positions = (double*)malloc(c3 * sizeof(double));
g->normals = (double*)calloc(c3, sizeof(double));
g->colors = (double*)malloc(c3 * sizeof(double));
if (!g->dirs || !g->keys || !g->heights || !g->ejecta || !g->positions || !g->normals || !g->colors) {
mt_free(g);
return 0;
}
g->stage = ST_DIRS;
return g;
}
void mt_free(mt_gen* g)
{
if (!g)
return;
#if MT_THREADS
if (g->threaded)
pthread_join(g->thread, 0);
#endif
free(g->dirs);
free(g->keys);
free(g->heights);
free(g->ejecta);
free(g->positions);
free(g->normals);
free(g->colors);
free(g->craters);
free(g->cell_start);
free(g->cell_list);
free(g);
}
static void dir_at(mt_gen* g, int v)
{
int n = g->n, row = g->row, f = v / g->per, r = v % g->per, j = r / row, i = r % row;
const double* F = FACES[f];
double s = ((double)i / n) * 2 - 1;
double t = ((double)j / n) * 2 - 1;
double cx = F[0] + s * F[3] + t * F[6];
double cy = F[1] + s * F[4] + t * F[7];
double cz = F[2] + s * F[5] + t * F[8];
double x2 = cx * cx, y2 = cy * cy, z2 = cz * cz;
g->dirs[v * 3] = cx * sqrt(1 - y2 / 2 - z2 / 2 + (y2 * z2) / 3);
g->dirs[v * 3 + 1] = cy * sqrt(1 - x2 / 2 - z2 / 2 + (x2 * z2) / 3);
g->dirs[v * 3 + 2] = cz * sqrt(1 - x2 / 2 - y2 / 2 + (x2 * y2) / 3);
if (i == 0 || j == 0 || i == n || j == n) {
int kx = (int)lround_(((cx + 1) / 2) * n), ky = (int)lround_(((cy + 1) / 2) * n), kz = (int)lround_(((cz + 1) / 2) * n);
g->keys[v] = (kx * row + ky) * row + kz;
} else {
g->keys[v] = -1;
}
}
static int cell_at(double v)
{
double cell = 2.0 / GRID;
return (int)dmin(GRID - 1, dmax(0, floor((v + 1) / cell)));
}
/* terrain.lua make_craters: the list, then each grid cell's craters in list order. */
static int make_craters(mt_gen* g)
{
const mt_params* p = &g->p;
int field = p->technique == MT_CRATER_FIELD;
double min_r = field ? p->crater_min_radius * 0.8 : p->crater_min_radius;
double max_r = field ? p->crater_max_radius * 0.6 : p->crater_max_radius;
double scale = field ? 3 : 1;
int count = (int)lround_(dmin(p->crater_count, MAX_CRATERS) * scale), i, c, x, y, z, total;
mt_rng r = rng_new(p->crater_seed);
int cells = GRID * GRID * GRID;
int* fill;
if (count < 0)
count = 0;
g->x_max = dmax(1 + p->crater_rim_width, sqrt(1 + dmax(0, p->crater_smoothness)));
g->craters = (crater*)malloc((size_t)(count > 0 ? count : 1) * sizeof(crater));
g->cell_start = (int*)calloc((size_t)cells + 1, sizeof(int));
if (!g->craters || !g->cell_start)
return 0;
for (i = 0; i < count; ++i) {
double zz = rng_next(&r) * 2 - 1;
double a = rng_next(&r) * M_PI * 2;
double q = sqrt(1 - zz * zz);
double radius = min_r + (max_r - min_r) * pow(rng_next(&r), p->crater_size_bias);
crater* k = &g->craters[i];
k->x = q * cos(a);
k->y = q * sin(a);
k->z = zz;
k->r = radius;
k->reach = radius * g->x_max;
}
g->n_craters = count;
/* Two passes: counts per cell, then the lists. */
for (c = 0; c < count; ++c) {
crater* k = &g->craters[c];
for (x = cell_at(k->x - k->reach); x <= cell_at(k->x + k->reach); ++x)
for (y = cell_at(k->y - k->reach); y <= cell_at(k->y + k->reach); ++y)
for (z = cell_at(k->z - k->reach); z <= cell_at(k->z + k->reach); ++z)
g->cell_start[(x * GRID + y) * GRID + z + 1]++;
}
for (i = 0; i < cells; ++i)
g->cell_start[i + 1] += g->cell_start[i];
total = g->cell_start[cells];
g->cell_list = (int*)malloc((size_t)(total > 0 ? total : 1) * sizeof(int));
fill = (int*)calloc((size_t)cells, sizeof(int));
if (!g->cell_list || !fill) {
free(fill);
return 0;
}
for (c = 0; c < count; ++c) {
crater* k = &g->craters[c];
for (x = cell_at(k->x - k->reach); x <= cell_at(k->x + k->reach); ++x)
for (y = cell_at(k->y - k->reach); y <= cell_at(k->y + k->reach); ++y)
for (z = cell_at(k->z - k->reach); z <= cell_at(k->z + k->reach); ++z) {
int cell = (x * GRID + y) * GRID + z;
g->cell_list[g->cell_start[cell] + fill[cell]++] = c;
}
}
free(fill);
return 1;
}
static void height_at(mt_gen* g, int i)
{
const mt_params* p = &g->p;
int field = p->technique == MT_CRATER_FIELD;
double x = g->dirs[i * 3], y = g->dirs[i * 3 + 1], z = g->dirs[i * 3 + 2];
double px = x + p->offset[0], py = y + p->offset[1], pz = z + p->offset[2];
double value, h, crater_h = 0, glow = 0;
double shape_strength = field ? p->shape_strength * 0.4 : p->shape_strength;
double smooth = dmax(0, p->crater_smoothness);
double ejecta_reach = 1 + p->crater_rim_width * 2.5;
double x_max = g->x_max;
int cell, c, end;
switch (p->technique) {
case MT_RIDGED:
value = ridged(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity,
p->ridge_sharpness, p->ridge_gain);
break;
case MT_BILLOW:
value = billow(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity);
break;
case MT_WARPED: {
double wf = p->warp_frequency;
double wx = fbm(px + 5.2, py + 1.3, pz, p->warp_octaves, wf, 0.5, 2);
double wy = fbm(px, py + 9.7, pz + 2.8, p->warp_octaves, wf, 0.5, 2);
double wz = fbm(px + 3.1, py, pz + 7.4, p->warp_octaves, wf, 0.5, 2);
double s = p->warp_strength;
value = (fbm(px + wx * s, py + wy * s, pz + wz * s, p->shape_octaves, p->shape_frequency, p->shape_persistence,
p->shape_lacunarity) + 1) / 2;
break;
}
default:
value = (fbm(px, py, pz, p->shape_octaves, p->shape_frequency, p->shape_persistence, p->shape_lacunarity) + 1) / 2;
break;
}
h = dmax(0, value - p->shape_min) * shape_strength;
if (p->ridge_strength != 0) {
double r = ridged(px - 11.3, py + 4.1, pz - 7.9, p->ridge_octaves, p->ridge_frequency, p->ridge_persistence,
p->ridge_lacunarity, p->ridge_sharpness, p->ridge_gain);
h = h + dmax(0, r - p->ridge_min) * p->ridge_strength;
}
cell = (cell_at(x) * GRID + cell_at(y)) * GRID + cell_at(z);
end = g->cell_start[cell + 1];
for (c = g->cell_start[cell]; c < end; ++c) {
const crater* k = &g->craters[g->cell_list[c]];
double dx = x - k->x, dy = y - k->y, dz = z - k->z;
double d2 = dx * dx + dy * dy + dz * dz;
double reach = dmax(k->reach, k->r * ejecta_reach);
if (d2 <= reach * reach) {
double dist = sqrt(d2) / k->r;
if (dist < x_max) {
double cavity = dist * dist - 1;
double rim_x = dmin(dist - 1 - p->crater_rim_width, 0);
double rim = p->crater_rim_steepness * rim_x * rim_x;
double shape = smooth_max(cavity, p->crater_floor, smooth);
shape = smooth_min(shape, rim, smooth);
crater_h = crater_h + shape * k->r;
}
if (dist > 0.8 && dist < ejecta_reach)
glow = glow + 1 - smoothstep(1, ejecta_reach, dist);
}
}
g->heights[i] = h + crater_h * p->crater_depth;
g->ejecta[i] = dmin(1, glow);
}
static void tri(mt_gen* g, int a, int b, int c)
{
const double* P = g->positions;
double* N = g->normals;
double e1x, e1y, e1z, e2x, e2y, e2z, nx, ny, nz;
a *= 3;
b *= 3;
c *= 3;
e1x = P[b] - P[a]; e1y = P[b + 1] - P[a + 1]; e1z = P[b + 2] - P[a + 2];
e2x = P[c] - P[a]; e2y = P[c + 1] - P[a + 1]; e2z = P[c + 2] - P[a + 2];
nx = e1y * e2z - e1z * e2y;
ny = e1z * e2x - e1x * e2z;
nz = e1x * e2y - e1y * e2x;
N[a] += nx; N[a + 1] += ny; N[a + 2] += nz;
N[b] += nx; N[b + 1] += ny; N[b + 2] += nz;
N[c] += nx; N[c + 1] += ny; N[c + 2] += nz;
}
typedef struct seam {
int key, index;
} seam;
static int seam_cmp(const void* a, const void* b)
{
const seam* x = (const seam*)a;
const seam* y = (const seam*)b;
if (x->key != y->key)
return x->key < y->key ? -1 : 1;
return x->index < y->index ? -1 : (x->index > y->index);
}
/* Faces duplicate their edge vertices: each copy gets the sum (in index order, like the
* Lua's ipairs over its group). */
static int seams(mt_gen* g)
{
int count = g->count, i, m = 0, a, b, k;
seam* list = (seam*)malloc((size_t)count * sizeof(seam));
if (!list)
return 0;
for (i = 0; i < count; ++i)
if (g->keys[i] >= 0) {
list[m].key = g->keys[i];
list[m].index = i;
++m;
}
qsort(list, (size_t)m, sizeof(seam), seam_cmp);
for (a = 0; a < m; a = b) {
double sx = 0, sy = 0, sz = 0;
for (b = a; b < m && list[b].key == list[a].key; ++b) {
}
if (b - a < 2)
continue;
for (k = a; k < b; ++k) {
int o = list[k].index * 3;
sx = sx + g->normals[o];
sy = sy + g->normals[o + 1];
sz = sz + g->normals[o + 2];
}
for (k = a; k < b; ++k) {
int o = list[k].index * 3;
g->normals[o] = sx;
g->normals[o + 1] = sy;
g->normals[o + 2] = sz;
}
}
free(list);
return 1;
}
static void color_at(mt_gen* g, int i)
{
const mt_params* p = &g->p;
int o = i * 3;
double x = g->dirs[o], y = g->dirs[o + 1], z = g->dirs[o + 2];
double ox = p->offset[0], oy = p->offset[1], oz = p->offset[2];
double range = g->max_h - g->min_h;
double e, te, c1, c2, c3, gg, steepness, ws, gr;
const double *lo, *hi;
if (range == 0)
range = 1;
e = (g->heights[i] - g->min_h) / range;
if (e < 0.5) {
lo = p->low; hi = p->mid; te = e * 2;
} else {
lo = p->mid; hi = p->high; te = (e - 0.5) * 2;
}
c1 = lo[0] + (hi[0] - lo[0]) * te;
c2 = lo[1] + (hi[1] - lo[1]) * te;
c3 = lo[2] + (hi[2] - lo[2]) * te;
if (p->patch_amount > 0) {
double pn = fbm(x + ox + 31.7, y + oy - 12.2, z + oz + 5.5, 4, p->patch_frequency, 0.5, 2);
double w = smoothstep(-0.05, 0.4, pn) * p->patch_amount * (1 - e * 0.6);
c1 = c1 + (p->patch[0] - c1) * w;
c2 = c2 + (p->patch[1] - c2) * w;
c3 = c3 + (p->patch[2] - c3) * w;
}
gg = g->ejecta[i] * p->crater_ejecta;
c1 = c1 + (p->high[0] - c1) * gg;
c2 = c2 + (p->high[1] - c2) * gg;
c3 = c3 + (p->high[2] - c3) * gg;
steepness = 1 - (g->normals[o] * x + g->normals[o + 1] * y + g->normals[o + 2] * z);
ws = smoothstep(p->steep_threshold - p->steep_blend, p->steep_threshold + p->steep_blend, steepness);
c1 = c1 + (p->steep[0] - c1) * ws;
c2 = c2 + (p->steep[1] - c2) * ws;
c3 = c3 + (p->steep[2] - c3) * ws;
gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * p->grain;
g->colors[o] = c1 * gr;
g->colors[o + 1] = c2 * gr;
g->colors[o + 2] = c3 * gr;
}
int mt_step(mt_gen* g, double budget)
{
double deadline = clock_secs() + budget;
int count = g->count, every = 0;
#define DUE() (budget > 0 && (++every & 31) == 0 && clock_secs() > deadline)
while (g->stage != ST_DONE) {
switch (g->stage) {
case ST_DIRS:
while (g->i < count) {
dir_at(g, g->i++);
if (DUE())
goto out;
}
g->stage = ST_CRATERS;
break;
case ST_CRATERS:
if (!make_craters(g)) {
g->stage = ST_DONE; /* (out of memory: a flat moon) */
break;
}
g->stage = ST_HEIGHTS;
g->i = 0;
break;
case ST_HEIGHTS:
while (g->i < count) {
height_at(g, g->i++);
if (DUE())
goto out;
}
g->stage = ST_MEAN;
break;
case ST_MEAN: {
double mean = 0;
int i;
for (i = 0; i < count; ++i)
mean = mean + g->heights[i];
g->mean = mean / count;
g->min_h = HUGE_VAL;
g->max_h = -HUGE_VAL;
g->stage = ST_POSITIONS;
g->i = 0;
break;
}
case ST_POSITIONS: {
int i;
for (i = 0; i < count; ++i) {
double hh = g->heights[i] - g->mean, r;
g->heights[i] = hh;
r = 1 + hh;
g->positions[i * 3] = g->dirs[i * 3] * r;
g->positions[i * 3 + 1] = g->dirs[i * 3 + 1] * r;
g->positions[i * 3 + 2] = g->dirs[i * 3 + 2] * r;
if (hh < g->min_h)
g->min_h = hh;
if (hh > g->max_h)
g->max_h = hh;
}
g->stage = ST_NORMALS;
g->i = 0;
break;
}
case ST_NORMALS: {
/* g->i: the row (face * n + y) */
int n = g->n, rows = 6 * n;
while (g->i < rows) {
int f = g->i / n, y = g->i % n, x;
int base = f * g->per;
for (x = 0; x < n; ++x) {
int i = base + y * g->row + x;
tri(g, i, i + 1, i + n + 2);
tri(g, i, i + n + 2, i + n + 1);
}
++g->i;
if (budget > 0 && clock_secs() > deadline)
goto out;
}
g->stage = ST_SEAMS;
break;
}
case ST_SEAMS:
seams(g);
g->stage = ST_NORMALIZE;
break;
case ST_NORMALIZE: {
int i;
for (i = 0; i < count; ++i) {
int o = i * 3;
double nx = g->normals[o], ny = g->normals[o + 1], nz = g->normals[o + 2];
double len = sqrt(nx * nx + ny * ny + nz * nz);
if (len == 0)
len = 1;
g->normals[o] = nx / len;
g->normals[o + 1] = ny / len;
g->normals[o + 2] = nz / len;
}
g->stage = ST_COLORS;
g->i = 0;
break;
}
case ST_COLORS:
while (g->i < count) {
color_at(g, g->i++);
if (DUE())
goto out;
}
g->stage = ST_DONE;
break;
}
}
out:
#undef DUE
return g->stage == ST_DONE;
}
int mt_n(const mt_gen* g) { return g->n; }
int mt_count(const mt_gen* g) { return g->count; }
const double* mt_positions(const mt_gen* g) { return g->positions; }
const double* mt_normals(const mt_gen* g) { return g->normals; }
const double* mt_colors(const mt_gen* g) { return g->colors; }
double mt_min_h(const mt_gen* g) { return g->min_h; }
double mt_max_h(const mt_gen* g) { return g->max_h; }
int mt_strip_len(int n)
{
/* per face n rows of 2 (n + 1), joined by 2 repeated vertices (none before the first) */
return 6 * n * 2 * (n + 1) + 2 * (6 * n - 1);
}
void mt_strip(int n, int* out)
{
int row = n + 1, per = row * row, c = 0, f, j, i;
for (f = 0; f < 6; ++f) {
int base = f * per;
for (j = 0; j < n; ++j) {
if (c > 0) {
out[c] = out[c - 1];
out[c + 1] = base + (j + 1) * row;
c += 2;
}
for (i = 0; i <= n; ++i) {
out[c] = base + (j + 1) * row + i;
out[c + 1] = base + j * row + i;
c += 2;
}
}
}
}
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to)
{
int n = g->n, row = n + 1, per = row * row, k = 0, f, j, i, last = -1;
/* the strip of mt_strip, walked without the index array */
#define PUT(v) \
do { \
int v3 = (v) * 3; \
if (k < from || k >= to) { \
last = (v); \
++k; \
break; \
} \
float* pp = pos ? pos + (size_t)k * stride : 0; \
float* nn = nor ? nor + (size_t)k * stride : 0; \
float* cc = col ? col + (size_t)k * stride : 0; \
if (pp) { pp[0] = (float)g->positions[v3]; pp[1] = (float)g->positions[v3 + 1]; pp[2] = (float)g->positions[v3 + 2]; } \
if (nn) { nn[0] = (float)g->normals[v3]; nn[1] = (float)g->normals[v3 + 1]; nn[2] = (float)g->normals[v3 + 2]; } \
if (cc) { cc[0] = (float)g->colors[v3]; cc[1] = (float)g->colors[v3 + 1]; cc[2] = (float)g->colors[v3 + 2]; } \
last = (v); \
++k; \
} while (0)
for (f = 0; f < 6; ++f) {
int base = f * per;
for (j = 0; j < n; ++j) {
if (k > 0) {
int prev = last;
PUT(prev);
PUT(base + (j + 1) * row);
}
for (i = 0; i <= n; ++i) {
PUT(base + (j + 1) * row + i);
PUT(base + j * row + i);
}
}
}
#undef PUT
}
#if MT_THREADS
static void* run_thread(void* arg)
{
mt_gen* g = (mt_gen*)arg;
mt_step(g, 0);
__atomic_store_n(&g->finished, 1, __ATOMIC_RELEASE);
return 0;
}
#endif
int mt_start(mt_gen* g)
{
#if MT_THREADS
if (g->threaded || g->stage == ST_DONE)
return g->threaded;
if (pthread_create(&g->thread, 0, run_thread, g) != 0)
return 0;
g->threaded = 1;
return 1;
#else
(void)g;
return 0;
#endif
}
int mt_done(mt_gen* g)
{
#if MT_THREADS
if (g->threaded)
return __atomic_load_n(&g->finished, __ATOMIC_ACQUIRE);
#endif
return g->stage == ST_DONE;
}
+86
View File
@@ -0,0 +1,86 @@
/* The moon generator in C: a port of the server's web/static/js/terrain.js generation.
* Same random draws (mulberry32, the simplex permutation) and the same arithmetic in the
* same order, in doubles, so the output matches terrain.js within its Float32Array
* rounding.
*
* Plain C, no engine or Lua headers: mmterrain.c binds it for the engine, and
* scripts/moon-check.mjs loads it into luajit through the FFI (a host build) to compare
* it with terrain.js. The work runs in steps with a time budget (mt_step), so a slow
* target (wasm) can spread it over frames.
*/
#ifndef MM_TERRAIN_H
#define MM_TERRAIN_H
#ifndef MT_THREADS
#if defined(__EMSCRIPTEN__) || defined(_WIN32)
#define MT_THREADS 0
#else
#define MT_THREADS 1
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
/* Techniques, in terrain.lua's TECHNIQUES order. */
enum { MT_FBM = 0, MT_RIDGED = 1, MT_WARPED = 2, MT_BILLOW = 3, MT_CRATER_FIELD = 4 };
/* The resolved parameters of one moon (terrain.lua resolve), colors already linear RGB. */
typedef struct mt_params {
double resolution;
int technique;
double shape_strength, shape_frequency, shape_octaves, shape_persistence, shape_lacunarity, shape_min;
double ridge_strength, ridge_frequency, ridge_octaves, ridge_persistence, ridge_lacunarity,
ridge_sharpness, ridge_gain, ridge_min;
double warp_strength, warp_frequency, warp_octaves;
double crater_count, crater_min_radius, crater_max_radius, crater_size_bias, crater_depth,
crater_rim_width, crater_rim_steepness, crater_floor, crater_smoothness, crater_ejecta;
double low[3], mid[3], high[3], steep[3], patch[3];
double steep_threshold, steep_blend, patch_amount, patch_frequency, grain;
double offset[3];
double crater_seed;
} mt_params;
typedef struct mt_gen mt_gen;
/* A generator for p (copied). NULL when out of memory. */
mt_gen* mt_new(const mt_params* p);
void mt_free(mt_gen* g);
/* Works for about `budget` seconds (<= 0: until done); returns 1 when done. */
int mt_step(mt_gen* g, double budget);
/* Where there are threads (desktop: MT_THREADS), runs the whole generation on a thread of
* its own and returns 1 (then only mt_done and, once done, the results may be used);
* else returns 0 (step it instead). */
int mt_start(mt_gen* g);
/* 1 once the generation is complete (threaded or stepped). */
int mt_done(mt_gen* g);
/* The grid size, its vertex count, and (when done) the flat xyz arrays over the grid
* (vertex v at [3v..3v+2]) and the height range, like terrain.js generateMoon's. */
int mt_n(const mt_gen* g);
int mt_count(const mt_gen* g);
const double* mt_positions(const mt_gen* g);
const double* mt_normals(const mt_gen* g);
const double* mt_colors(const mt_gen* g);
double mt_min_h(const mt_gen* g);
double mt_max_h(const mt_gen* g);
/* The triangle strip over the grid (terrain.lua strip): its length, and its 0-based grid
* indices into out (mt_strip_len(n) ints). */
int mt_strip_len(int n);
void mt_strip(int n, int* out);
/* Writes the strip's positions, normals and colors as floats, its vertices k in
* [from, to): vertex k at pos[k * stride ..], nor[k * stride ..], col[k * stride ..] (any
* may be NULL). */
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to);
/* The parameters from a flat array in terrain.lua native_params order (MT_PARAM_COUNT
* numbers); returns 0 when it is too short. */
#define MT_PARAM_COUNT 53
int mt_params_from_array(const double* a, int n, mt_params* p);
#ifdef __cplusplus
}
#endif
#endif
+428
View File
@@ -0,0 +1,428 @@
-- Blocks: parts of the layout that a rebuild keeps when what they show hasn't changed.
-- Every new state rebuilds the screen (core/main.lua); a block is built once into a
-- container node and kept, with its nodes and Druid components, by the next builds while
-- its signature (M.sig of everything it shows) stays the same. Regions are blocks
-- (header, moon list, main panel, aside), and so are costly parts inside them (the
-- market's chart, ticker and sparklines, cards).
--
-- local b, kept = blocks.open(g, key, sig, parent) -- build into b.root unless kept
-- if not kept then ... build into b.root ... end
-- local ret = blocks.close(g, b, ...) -- what it returns, kept or not
-- -- or: blocks.keep(g, key, sig, parent, function(root) ... return ... end)
--
-- Build registrations (test ids and keyboard focus, tooltips, tickers, fields, scroll
-- areas, effect anchors, ...) are reset by every build; those made while a block builds
-- are noted (M.note) and replayed in their order when it's kept, so a kept block is in
-- every registry like a rebuilt one. Druid components made while a block builds are its
-- own: they go when it goes (the screen's Druid instance lives on; core/main.lua).
-- A keyed block inside one that is rebuilt survives it (set aside until the new build
-- asks for it); blocks no build asked for go at the end of the build (M.finish).
local helper = require("druid.helper")
local M = {}
local stack = {} -- blocks being built, innermost last
local replaying = false
local orphans = {} -- components made outside any block's build (not a region's)
local function pack(...)
return { n = select("#", ...), ... }
end
local function alive(node)
return node ~= nil and pcall(gui.get_id, node)
end
---------------------------------------------------------------- signatures
local function ser(v, buf)
local t = type(v)
if t == "table" then
buf[#buf + 1] = "{"
local n = #v
for i = 1, n do
ser(v[i], buf)
buf[#buf + 1] = ","
end
local keys, mixed
for k in pairs(v) do
if type(k) ~= "number" or k < 1 or k > n or k % 1 ~= 0 then
keys = keys or {}
keys[#keys + 1] = k
mixed = mixed or type(k) ~= "string"
end
end
if keys then
if mixed then
table.sort(keys, function(a, b) return tostring(a) < tostring(b) end)
else
table.sort(keys)
end
for _, k in ipairs(keys) do
buf[#buf + 1] = tostring(k)
buf[#buf + 1] = "="
ser(v[k], buf)
buf[#buf + 1] = ";"
end
end
buf[#buf + 1] = "}"
elseif t == "string" then
buf[#buf + 1] = #v .. ":" .. v
elseif t == "function" then
buf[#buf + 1] = "f"
else
buf[#buf + 1] = tostring(v)
end
end
-- A string that is the same exactly when the values are (tables by content, keys sorted).
function M.sig(...)
local buf = {}
for i = 1, select("#", ...) do
ser((select(i, ...)), buf)
buf[#buf + 1] = "|"
end
return table.concat(buf)
end
---------------------------------------------------------------- registrations
-- Note a registration made while building (fn(...) makes it again in a later build that
-- keeps the block). The registry functions call this; it does nothing outside a block.
function M.note(fn, ...)
local b = stack[#stack]
if replaying or not b then
return
end
local j = b.journal
j[#j + 1] = { fn = fn, n = select("#", ...), ... }
end
-- t[field] (a list reset by every build) gets value, noted.
local function push(t, field, value)
local l = t[field]
if not l then
l = {}
t[field] = l
end
l[#l + 1] = value
end
function M.push(t, field, value)
push(t, field, value)
M.note(push, t, field, value)
end
-- t[field][k] = value (a table reset by every build), noted.
local function put(t, field, k, value)
local m = t[field]
if not m then
m = {}
t[field] = m
end
m[k] = value
end
function M.put(t, field, k, value)
put(t, field, k, value)
M.note(put, t, field, k, value)
end
local replay
-- A kept block inside the one being built: noted in it, its registrations made again.
local function adopt(b)
local top = stack[#stack]
if top then
top.journal[#top.journal + 1] = { block = b }
end
replay(b)
end
replay = function(b)
for _, e in ipairs(b.journal) do
if e.block then
if not e.block.dead then
replay(e.block)
end
else
replaying = true
pcall(e.fn, unpack(e, 1, e.n)) -- (a node that went since: a list row scrolled out)
replaying = false
end
end
end
-- fn, made to run inside the block being built now when it's called later, outside a
-- build (a list making rows as it scrolls, a field's on_change building again): what it
-- makes and registers is that block's.
function M.bound(fn)
local b = stack[#stack]
if not b then
return fn
end
return function(...)
if #stack > 0 or b.dead then
return fn(...)
end
stack[1] = b
local r = pack(pcall(fn, ...))
stack = {}
if not r[1] then
error(r[2], 0)
end
return unpack(r, 2, r.n)
end
end
---------------------------------------------------------------- components
-- Components of g.druid made while a block builds belong to it (core/main.lua hooks the
-- instance's new once per instance).
function M.hook(druid_instance)
local new = druid_instance.new
druid_instance.new = function(self, component, ...)
local c = new(self, component, ...)
if not c._meta.parent then -- (a widget's own components go with it)
local b = stack[#stack]
if b then
b.comps[#b.comps + 1] = c
else
orphans[#orphans + 1] = c
end
end
return c
end
end
-- After a kept block moved under another parent: Druid buttons and hovers clip their
-- clicks to the stencil above them, found when they were made (perhaps deleted with the
-- old parent; a deleted node still answers until the frame ends, so always look again).
local function rezone(b)
for _, c in ipairs(b.comps) do
if c.node and c.set_click_zone and (c.hover or c.on_mouse_hover) then
pcall(c.set_click_zone, c, helper.get_closest_stencil_node(c.node))
end
end
for _, e in ipairs(b.journal) do
if e.block and not e.block.dead then
rezone(e.block)
end
end
end
-- True while a kept block's registrations are made again (test.register looks for its
-- clip zone again then: the one it was given may have gone with the old parent).
function M.replaying()
return replaying
end
local function node_of(c)
return c.node or c.root or c.view_node
end
---------------------------------------------------------------- lifetime
local function blocks_of(g)
g.blocks = g.blocks or { by_key = {}, aside = {}, used = {} }
return g.blocks
end
-- Taken by the build going on (kept or made again under a key).
local function taken(reg, b)
return b.key ~= nil and reg.used[b.key] and reg.by_key[b.key] == b
end
local destroy
-- Removes a block: keyed blocks inside it are set aside (detached, still whole) for the
-- build going on to take back; the rest goes.
destroy = function(g, b, set_aside)
local reg = blocks_of(g)
for _, e in ipairs(b.journal) do
local c = e.block
if c and not c.dead and not taken(reg, c) then
if c.key and set_aside and alive(c.root) then
gui.set_parent(c.root, reg.limbo, false)
reg.aside[#reg.aside + 1] = c
else
destroy(g, c, set_aside)
end
end
end
g.tearing_down = true -- (components going away fire events: core/list, scroll)
for i = #b.comps, 1, -1 do
pcall(g.druid.remove, g.druid, b.comps[i])
end
g.tearing_down = false
b.comps = {}
if alive(b.root) then
gui.delete_node(b.root)
end
if b.key and reg.by_key[b.key] == b then
reg.by_key[b.key] = nil
end
b.dead = true
end
-- Components made outside blocks whose nodes went (the moon list's search filtering
-- rows again, ...).
local function sweep(g)
local keep = {}
for _, c in ipairs(orphans) do
local n = node_of(c)
if n and not alive(n) then
pcall(g.druid.remove, g.druid, c)
elseif n then
keep[#keep + 1] = c
end
end
orphans = keep
end
-- Start of a build. full: nothing is kept (core/main.lua makes the layout and the Druid
-- instance again).
function M.begin(g, full)
local reg = blocks_of(g)
stack = {}
replaying = false
if full then
reg.by_key, reg.aside = {}, {}
orphans = {}
end
reg.used = {}
if not alive(reg.limbo) then
reg.limbo = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(1, 1, 0))
gui.set_enabled(reg.limbo, false)
end
end
-- Open a block under parent. Returns the block (build into b.root) and whether it was
-- kept: the last one with this key had the same sig. key nil: never kept (still a
-- block, so keyed blocks inside it can be).
function M.open(g, key, sig, parent)
local reg = blocks_of(g)
if key and reg.used[key] then
key = nil -- (asked for twice in one build: the second isn't kept)
end
local old = key and reg.by_key[key]
if old and old.dead then
old = nil
end
if old and old.sig == sig and alive(old.root) then
reg.used[key] = true
gui.set_parent(old.root, parent, false)
rezone(old)
adopt(old)
return old, true
end
if old then
destroy(g, old, true)
end
local root = gui.new_box_node(vmath.vector3(0, 0, 0), vmath.vector3(1, 1, 0))
gui.set_pivot(root, gui.PIVOT_NW)
gui.set_parent(root, parent, false)
gui.set_visible(root, false) -- (only a container: its children draw)
local b = { key = key, sig = sig, root = root, journal = {}, comps = {} }
if key then
reg.by_key[key] = b
reg.used[key] = true
end
local top = stack[#stack]
if top then
top.journal[#top.journal + 1] = { block = b }
end
stack[#stack + 1] = b
return b, false
end
-- The block open() would keep (same key and sig), without taking it; nil if none.
function M.peek(g, key, sig)
local reg = blocks_of(g)
if not key or reg.used[key] then
return nil
end
local b = reg.by_key[key]
if b and not b.dead and b.sig == sig and alive(b.root) then
return b
end
end
-- Close block b: ... is what its build returns; returns that (kept: what the build that
-- made it returned).
function M.close(g, b, ...)
for i = #stack, 1, -1 do
if stack[i] == b then
for j = #stack, i, -1 do
stack[j] = nil
end
b.ret = { n = select("#", ...), ... }
return ...
end
end
if b.ret then
return unpack(b.ret, 1, b.ret.n)
end
end
-- Open b again to add to it (a card's height, known after its row): what is made until
-- close() is its.
function M.reopen(g, b)
stack[#stack + 1] = b
end
-- Drop block b now (a kept card that has to be built again).
function M.drop(g, b)
local reg = blocks_of(g)
if b.key and reg.used[b.key] and reg.by_key[b.key] == b then
reg.used[b.key] = nil
end
if not b.dead then
destroy(g, b, false)
end
end
-- open + build + close: fn(root) builds; returns what it returns (and that again while
-- the block is kept).
function M.keep(g, key, sig, parent, fn)
local b, kept = M.open(g, key, sig, parent)
if kept then
return M.close(g, b)
end
return M.close(g, b, fn(b.root))
end
-- Destroy a keyed block now (a region not shown this build).
function M.remove(g, key)
local reg = blocks_of(g)
local b = reg.by_key[key]
if b then
destroy(g, b, true)
end
end
-- End of a build: blocks it didn't ask for go.
function M.finish(g)
local reg = blocks_of(g)
stack = {}
for k, b in pairs(reg.by_key) do
if not reg.used[k] then
destroy(g, b, true)
end
end
for _, b in ipairs(reg.aside) do
if not b.dead and not taken(reg, b) then
destroy(g, b, false)
end
end
reg.aside = {}
sweep(g)
end
-- Forget every block (the screen goes away, or before a full rebuild deletes the layout).
function M.clear(g)
local reg = blocks_of(g)
reg.by_key, reg.aside, reg.used = {}, {}, {}
stack, orphans = {}, {}
end
return M
+3 -3
View File
@@ -6,6 +6,7 @@
local ui = require("modules.ui") local ui = require("modules.ui")
local theme = require("modules.theme") local theme = require("modules.theme")
local test = require("modules.test") local test = require("modules.test")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -53,9 +54,8 @@ function M.new(g, parent, key, x, y, w, h, opts)
end end
local input = g.druid:new_input(box, txt, opts.keyboard or gui.KEYBOARD_TYPE_DEFAULT) local input = g.druid:new_input(box, txt, opts.keyboard or gui.KEYBOARD_TYPE_DEFAULT)
test.register("field:" .. key, box, "field", opts.hint) test.register("field:" .. key, box, "field", opts.hint)
g.inputs = g.inputs or {} -- (core/keys: Enter submits; other keys skip while editing) blocks.push(g, "inputs", { input = input, submit = opts.on_submit, step = { min = opts.min, max = opts.max },
g.inputs[#g.inputs + 1] = { input = input, submit = opts.on_submit, step = { min = opts.min, max = opts.max }, esc_clears = opts.esc_clears })
esc_clears = opts.esc_clears }
-- (no "<" or "&": the text node would read them as markup) -- (no "<" or "&": the text node would read them as markup)
input:set_allowed_characters(opts.allowed or "[^<&]") input:set_allowed_characters(opts.allowed or "[^<&]")
if opts.max_len then if opts.max_len then
+2 -2
View File
@@ -203,7 +203,7 @@ local function into_view(g, e)
to = math.max(0, math.min(a.max or 0, to)) to = math.max(0, math.min(a.max or 0, to))
if to ~= pos then if to ~= pos then
g.scroll_pos[a.key] = to g.scroll_pos[a.key] = to
g.dirty = true g.dirty, g.dirty_all = true, true
end end
return return
end end
@@ -266,7 +266,7 @@ local function list_step(g, f, cur, e, dir, scope_)
to = math.max(0, math.min(a.max or 0, to)) to = math.max(0, math.min(a.max or 0, to))
if to ~= pos then if to ~= pos then
g.scroll_pos[a.key] = to g.scroll_pos[a.key] = to
g.dirty = true g.dirty, g.dirty_all = true, true
end end
local now = find(g, key) local now = find(g, key)
f.tip = now and tooltip.show_for(now.node, M.rect(now.node)) or false f.tip = now and tooltip.show_for(now.node, M.rect(now.node)) or false
+6 -7
View File
@@ -29,6 +29,7 @@ local ui = require("modules.ui")
local theme = require("modules.theme") local theme = require("modules.theme")
local sfx = require("modules.sfx") local sfx = require("modules.sfx")
local anim = require("modules.anim") local anim = require("modules.anim")
local blocks = require("modules.blocks")
local M = {} local M = {}
M.__index = M M.__index = M
@@ -102,15 +103,13 @@ end
-- A box an action's sweep covers when its button is inside (fx.js: the button's .card, -- A box an action's sweep covers when its button is inside (fx.js: the button's .card,
-- .line, row or heading): cards and list rows register while they're built. -- .line, row or heading): cards and list rows register while they're built.
function M:box(node) function M:box(node)
self.boxes = self.boxes or {} blocks.push(self, "boxes", node)
self.boxes[#self.boxes + 1] = node
end end
-- An action button of this build by its test id (H.action): effects that come after a -- An action button of this build by its test id (H.action): effects that come after a
-- rebuild find the new node (the error shake, the ready burst). -- rebuild find the new node (the error shake, the ready burst).
function M:button(id, node) function M:button(id, node)
self.buttons = self.buttons or {} blocks.put(self, "buttons", id, node)
self.buttons[id] = node
end end
-- The layout rect {x, y, w, h} (x right, y down, from the page's top-left) of a node with -- The layout rect {x, y, w, h} (x right, y down, from the page's top-left) of a node with
@@ -193,8 +192,8 @@ end
-- A header number node for `key` (box: its chip or frame, the target for particles). -- A header number node for `key` (box: its chip or frame, the target for particles).
function M:bind(key, node, box) function M:bind(key, node, box)
self.binds[key] = { node = node, box = box, color = gui.get_color(node), p = gui.get_position(node), blocks.put(self, "binds", key, { node = node, box = box, color = gui.get_color(node), p = gui.get_position(node),
scale = gui.get_scale(node), w = ui.text_width(node) } scale = gui.get_scale(node), w = ui.text_width(node) })
end end
local function drop_halo(c) local function drop_halo(c)
@@ -740,7 +739,7 @@ function M:reticle(root, w, h, name)
gui.set_pivot(glow, gui.PIVOT_CENTER) gui.set_pivot(glow, gui.PIVOT_CENTER)
gui.set_blend_mode(glow, theme.glow_blend()) gui.set_blend_mode(glow, theme.glow_blend())
gui.move_below(glow, parts[1]) gui.move_below(glow, parts[1])
self.reticles[#self.reticles + 1] = { node = ret, w = w, h = h, name = name, arms = arms, ticks = ticks, glow = glow } blocks.push(self, "reticles", { node = ret, w = w, h = h, name = name, arms = arms, ticks = ticks, glow = glow })
if self.shine_known and not self.shine_known[name] then if self.shine_known and not self.shine_known[name] then
-- It appeared shining: the reticle locks on after the tab's entrance. -- It appeared shining: the reticle locks on after the tab's entrance.
self.shine_known[name] = true self.shine_known[name] = true
+16 -3
View File
@@ -1,4 +1,4 @@
-- The moon in the aside viewport: the procedural moon of the web build (terrain.lua, a -- The moon in the aside viewport: the procedural moon of the web build (mmterrain and terrain.lua, a
-- port of the server's terrain.js; look of scene.js), drawn in 3D behind a GUI node. -- port of the server's terrain.js; look of scene.js), drawn in 3D behind a GUI node.
-- --
-- Use from a gui_script: -- Use from a gui_script:
@@ -13,12 +13,14 @@
-- appearing in front of the moon shown) charges up, then bursts -- appearing in front of the moon shown) charges up, then bursts
-- into moon `id` once its mesh is built. moon.show(id, node) during -- into moon `id` once its mesh is built. moon.show(id, node) during
-- it only moves the viewport. Progress in moon.revealing (below). -- it only moves the viewport. Progress in moon.revealing (below).
-- moon.prefetch(id) start making moon `id`'s mesh ahead (a bought moon, before its
-- reveal is shown).
-- moon.skip_reveal() jump ahead: a charge bursts as soon as the moon is ready, a -- moon.skip_reveal() jump ahead: a charge bursts as soon as the moon is ready, a
-- burst ends. -- burst ends.
-- --
-- How it works: moon/moon.script (in moon/moon.collection, a collection instance in the -- How it works: moon/moon.script (in moon/moon.collection, a collection instance in the
-- game screen) generates meshes (cached per id, spread over frames), spins and orbits the -- game screen) generates meshes (cached per id; natively through the mmterrain extension:
-- camera (drag to rotate, wheel to zoom), and fills `moon.frame`; the render script -- on a thread on desktop, a few ms a frame on the web), spins and orbits the camera (drag to rotate, wheel to zoom), and fills `moon.frame`; the render script
-- (render/mm.render_script) draws the scene into an HDR target and binds it as -- (render/mm.render_script) draws the scene into an HDR target and binds it as
-- `moon_tex` for the GUI node's material, which tone maps it (ACES, like three.js). -- `moon_tex` for the GUI node's material, which tone maps it (ACES, like three.js).
-- --
@@ -126,6 +128,17 @@ function M.reveal(id, node)
set_node(node) set_node(node)
end end
-- Start making moon `id`'s mesh now (a moon just bought, before its reveal shows it), so
-- it's ready when asked for.
function M.prefetch(id)
if debug_id ~= "" then
id = debug_id
end
if id ~= nil and tostring(id) ~= M.id then
M.prefetch_id = tostring(id)
end
end
function M.skip_reveal() function M.skip_reveal()
local r = M.revealing local r = M.revealing
if r then if r then
+30 -445
View File
@@ -1,7 +1,7 @@
-- Procedural moon terrain: a port of the server's web/static/js/terrain.js (generation, -- Moon terrain settings and helpers, a port of the server's web/static/js/terrain.js: the
-- deterministic from the moon id). Same seed and settings give the same moon as the web: -- settings (defaults, merge, per-moon resolve, the description traits), the colors and
-- the integer hashing and random draws are bit-exact, the floats differ only by the JS -- the cube-sphere grid and its triangle strip. The generation itself is native: the
-- Float32Array rounding (~1e-7). -- mmterrain extension (mmterrain/src/terrain.c) takes native_params(resolve(...)).
-- --
-- Pure Lua (LuaJIT `bit`), no engine APIs, so it also runs in plain luajit for checks -- Pure Lua (LuaJIT `bit`), no engine APIs, so it also runs in plain luajit for checks
-- (scripts/moon-check.mjs). Keep it in sync with terrain.js when that changes. -- (scripts/moon-check.mjs). Keep it in sync with terrain.js when that changes.
@@ -131,146 +131,6 @@ function M.rng(seed)
end end
end end
---------------------------------------------------------------- noise
-- 3D simplex noise (Stefan Gustavson's reference implementation), roughly in [-1, 1].
-- 0-based tables (LuaJIT keeps index 0 in the array part). GX/GY/GZ[v] is the gradient of
-- permutation value v (GRAD3 at v % 12, i.e. terrain.js PERM12).
local GRAD3 = {
1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1, 0, 1, 0, 1, -1, 0, 1, 1, 0, -1, -1, 0, -1, 0, 1, 1, 0, -1, 1, 0, 1, -1, 0, -1, -1,
}
local PERM, GX, GY, GZ = {}, {}, {}, {}
do
local p = {}
for i = 0, 255 do
p[i] = i
end
local r = M.rng(1337)
for i = 255, 1, -1 do
local j = floor(r() * (i + 1))
p[i], p[j] = p[j], p[i]
end
for i = 0, 511 do
PERM[i] = p[band(i, 255)]
end
for v = 0, 255 do
local g = (v % 12) * 3
GX[v], GY[v], GZ[v] = GRAD3[g + 1], GRAD3[g + 2], GRAD3[g + 3]
end
end
M.PERM = PERM
local F3 = 1 / 3
local G3 = 1 / 6
-- Branch-free (LuaJIT side traces of the reference's if/else chains abort on register
-- pressure): the simplex corner order comes from 0/1 flags, and corners outside their
-- radius get a zero weight. Bit-identical results: for floats, x >= y iff x - y >= 0, and
-- here |x - y| < 1, so 1 + floor(x - y) is that flag.
local function simplex3(x, y, z)
local P = PERM
local s = (x + y + z) * F3
local i, j, k = floor(x + s), floor(y + s), floor(z + s)
local t = (i + j + k) * G3
local x0, y0, z0 = x - (i - t), y - (j - t), z - (k - t)
local a = 1 + floor(x0 - y0) -- x0 >= y0
local b = 1 + floor(y0 - z0) -- y0 >= z0
local c = 1 + floor(x0 - z0) -- x0 >= z0
local i1 = a * (b + (1 - b) * c)
local j1 = (1 - a) * b
local k1 = 1 - i1 - j1
local i2 = a + (1 - a) * b * c
local j2 = a * b + 1 - a
local k2 = 2 - i2 - j2
local ii, jj, kk = band(i, 255), band(j, 255), band(k, 255)
local tt = max(0, 0.6 - x0 * x0 - y0 * y0 - z0 * z0)
local g = P[ii + P[jj + P[kk]]]
tt = tt * tt
local n = tt * tt * (GX[g] * x0 + GY[g] * y0 + GZ[g] * z0)
local x1, y1, z1 = x0 - i1 + G3, y0 - j1 + G3, z0 - k1 + G3
tt = max(0, 0.6 - x1 * x1 - y1 * y1 - z1 * z1)
g = P[ii + i1 + P[jj + j1 + P[kk + k1]]]
tt = tt * tt
n = n + tt * tt * (GX[g] * x1 + GY[g] * y1 + GZ[g] * z1)
local x2, y2, z2 = x0 - i2 + 2 * G3, y0 - j2 + 2 * G3, z0 - k2 + 2 * G3
tt = max(0, 0.6 - x2 * x2 - y2 * y2 - z2 * z2)
g = P[ii + i2 + P[jj + j2 + P[kk + k2]]]
tt = tt * tt
n = n + tt * tt * (GX[g] * x2 + GY[g] * y2 + GZ[g] * z2)
local x3, y3, z3 = x0 - 1 + 3 * G3, y0 - 1 + 3 * G3, z0 - 1 + 3 * G3
tt = max(0, 0.6 - x3 * x3 - y3 * y3 - z3 * z3)
g = P[ii + 1 + P[jj + 1 + P[kk + 1]]]
tt = tt * tt
n = n + tt * tt * (GX[g] * x3 + GY[g] * y3 + GZ[g] * z3)
return 32 * n
end
M.simplex3 = simplex3
-- Caps on saved settings, which come from the admin page.
local MAX_OCTAVES = 10
local MAX_CRATERS = 2000
-- Fractal (fBm) noise in [-1, 1].
local function fbm(x, y, z, octaves, frequency, persistence, lacunarity)
local sum, amp, norm, f = 0, 1, 0, frequency
for _ = 1, min(octaves, MAX_OCTAVES) do
sum = sum + simplex3(x * f, y * f, z * f) * amp
norm = norm + amp
amp = amp * persistence
f = f * lacunarity
end
return sum / norm
end
-- Billow noise (fBm of |noise|) in [0, 1].
local function billow(x, y, z, octaves, frequency, persistence, lacunarity)
local sum, amp, norm, f = 0, 1, 0, frequency
for _ = 1, min(octaves, MAX_OCTAVES) do
sum = sum + abs(simplex3(x * f, y * f, z * f)) * amp
norm = norm + amp
amp = amp * persistence
f = f * lacunarity
end
return sum / norm
end
-- Ridged multifractal in [0, 1]: sharp crests; each octave is weighted by the previous.
local function ridged(x, y, z, octaves, frequency, persistence, lacunarity, sharpness, gain)
local sum, amp, norm, weight, f = 0, 1, 0, 1, frequency
for _ = 1, min(octaves, MAX_OCTAVES) do
local v = 1 - abs(simplex3(x * f, y * f, z * f))
v = pow(v, sharpness) * weight
weight = min(1, max(0, v * gain))
sum = sum + v * amp
norm = norm + amp
amp = amp * persistence
f = f * lacunarity
end
return sum / norm
end
-- Polynomial smooth min/max (Inigo Quilez), k = smoothing width.
local function smooth_min(a, b, k)
if k <= 0 then
return min(a, b)
end
local h = min(1, max(0, (b - a + k) / (2 * k)))
return a * h + b * (1 - h) - k * h * (1 - h)
end
local function smooth_max(a, b, k)
return -smooth_min(-a, -b, k)
end
local function smoothstep(e0, e1, x)
local d = e1 - e0
if d == 0 then
d = 1e-6
end
local t = min(1, max(0, (x - e0) / d))
return t * t * (3 - 2 * t)
end
---------------------------------------------------------------- colors ---------------------------------------------------------------- colors
local function round(x) local function round(x)
@@ -410,6 +270,32 @@ function M.traits(settings, seed)
} }
end end
-- The resolved parameters as the flat number list the native generator takes
-- (mmterrain/src/terrain.h mt_params_from_array; colors linear, like generate's).
local TECHNIQUE_INDEX = { fbm = 0, ridged = 1, warped = 2, billow = 3, craterField = 4 }
function M.native_params(p)
local sh, rd, wp, cr, sd = p.shape, p.ridge, p.warp, p.craters, p.shading
local off = p.offset or { 0, 0, 0 }
local a = {
p.resolution, TECHNIQUE_INDEX[p.technique] or 0,
sh.strength, sh.frequency, sh.octaves, sh.persistence, sh.lacunarity, sh.minValue,
rd.strength, rd.frequency, rd.octaves, rd.persistence, rd.lacunarity, rd.sharpness, rd.gain, rd.minValue,
wp.strength, wp.frequency, wp.octaves,
cr.count, cr.minRadius, cr.maxRadius, cr.sizeBias, cr.depth, cr.rimWidth, cr.rimSteepness,
cr.floorHeight, cr.smoothness, cr.ejecta,
}
for _, key in ipairs({ "low", "mid", "high", "steep", "patch" }) do
local c = M.linear_color(sd[key])
a[#a + 1], a[#a + 2], a[#a + 3] = c[1], c[2], c[3]
end
local rest = { sd.steepThreshold, sd.steepBlend, sd.patchAmount, sd.patchFrequency, sd.grain,
off[1], off[2], off[3], p.craterSeed or 1 }
for _, v in ipairs(rest) do
a[#a + 1] = v
end
return a
end
---------------------------------------------------------------- mesh ---------------------------------------------------------------- mesh
-- Cube faces: outward normal and two tangent axes (a x b = up, so triangles face out). -- Cube faces: outward normal and two tangent axes (a x b = up, so triangles face out).
@@ -495,305 +381,4 @@ function M.strip(n)
return out return out
end end
-- Craters scattered on the unit sphere, bucketed in a 3D grid for fast lookup.
local function make_craters(c, seed, count_scale)
local r = M.rng(seed)
local count = round(min(c.count, MAX_CRATERS) * count_scale)
local x_max = max(1 + c.rimWidth, sqrt(1 + max(0, c.smoothness)))
local list = {}
for i = 1, count do
local z = r() * 2 - 1
local a = r() * pi * 2
local q = sqrt(1 - z * z)
local radius = c.minRadius + (c.maxRadius - c.minRadius) * pow(r(), c.sizeBias)
list[i] = { x = q * cos(a), y = q * sin(a), z = z, r = radius, reach = radius * x_max }
end
local G = 12
local cell = 2 / G
local grid = {}
for i = 1, G * G * G do
grid[i] = {}
end
local function at(v)
return min(G - 1, max(0, floor((v + 1) / cell)))
end
for _, cr in ipairs(list) do
for x = at(cr.x - cr.reach), at(cr.x + cr.reach) do
for y = at(cr.y - cr.reach), at(cr.y + cr.reach) do
for z = at(cr.z - cr.reach), at(cr.z + cr.reach) do
local g = grid[(x * G + y) * G + z + 1]
g[#g + 1] = cr
end
end
end
end
return {
cell_of = function(x, y, z)
return grid[(at(x) * G + at(y)) * G + at(z) + 1]
end,
x_max = x_max,
}
end
-- Builds the moon for resolved parameters (see M.resolve): flat arrays over the cube-sphere
-- grid (vertex v 0-based at [3v+1..3v+3]): positions (unit radius plus elevation), normals,
-- colors (linear RGB). `checkpoint` (optional) is called every few hundred vertices, e.g.
-- to yield a coroutine and spread the work over frames.
function M.generate(p, checkpoint)
checkpoint = checkpoint or function() end
local n = max(8, min(320, round(p.resolution)))
local row = n + 1
local per = row * row
local count = 6 * per
local dirs, keys = M.cube_sphere(n)
checkpoint()
local heights, ejecta = {}, {}
local technique = p.technique
local sh, rd, wp, cr = p.shape, p.ridge, p.warp, p.craters
local off = p.offset or { 0, 0, 0 }
local ox, oy, oz = off[1], off[2], off[3]
local crater_field = technique == "craterField"
local cparams = cr
if crater_field then
cparams = clone(cr)
cparams.minRadius = cr.minRadius * 0.8
cparams.maxRadius = cr.maxRadius * 0.6
end
local craters = make_craters(cparams, p.craterSeed or 1, crater_field and 3 or 1)
checkpoint()
local x_max = craters.x_max
local shape_strength = crater_field and sh.strength * 0.4 or sh.strength
local smooth = max(0, cr.smoothness)
local ejecta_reach = 1 + cr.rimWidth * 2.5
local rim_width, rim_steep, floor_h = cr.rimWidth, cr.rimSteepness, cr.floorHeight
local s_oct, s_freq, s_pers, s_lac, s_min = sh.octaves, sh.frequency, sh.persistence, sh.lacunarity, sh.minValue
local r_oct, r_freq, r_pers, r_lac, r_sharp, r_gain, r_min, r_str =
rd.octaves, rd.frequency, rd.persistence, rd.lacunarity, rd.sharpness, rd.gain, rd.minValue, rd.strength
for i = 0, count - 1 do
local x, y, z = dirs[i * 3 + 1], dirs[i * 3 + 2], dirs[i * 3 + 3]
local px, py, pz = x + ox, y + oy, z + oz
-- Primary shape layer.
local value
if technique == "ridged" then
value = ridged(px, py, pz, s_oct, s_freq, s_pers, s_lac, r_sharp, r_gain)
elseif technique == "billow" then
value = billow(px, py, pz, s_oct, s_freq, s_pers, s_lac)
elseif technique == "warped" then
local wf = wp.frequency
local wx = fbm(px + 5.2, py + 1.3, pz, wp.octaves, wf, 0.5, 2)
local wy = fbm(px, py + 9.7, pz + 2.8, wp.octaves, wf, 0.5, 2)
local wz = fbm(px + 3.1, py, pz + 7.4, wp.octaves, wf, 0.5, 2)
local s = wp.strength
value = (fbm(px + wx * s, py + wy * s, pz + wz * s, s_oct, s_freq, s_pers, s_lac) + 1) / 2
else
value = (fbm(px, py, pz, s_oct, s_freq, s_pers, s_lac) + 1) / 2
end
local h = max(0, value - s_min) * shape_strength
-- Ridge layer.
if r_str ~= 0 then
local r = ridged(px - 11.3, py + 4.1, pz - 7.9, r_oct, r_freq, r_pers, r_lac, r_sharp, r_gain)
h = h + max(0, r - r_min) * r_str
end
-- Craters: parabola cavity, flattened floor, smooth rim (smooth min/max blend them).
local crater_h, glow = 0, 0
local list = craters.cell_of(x, y, z)
for c = 1, #list do
local k = list[c]
local dx, dy, dz = x - k.x, y - k.y, z - k.z
local d2 = dx * dx + dy * dy + dz * dz
local reach = max(k.reach, k.r * ejecta_reach)
if d2 <= reach * reach then
local dist = sqrt(d2) / k.r
if dist < x_max then
local cavity = dist * dist - 1
local rim_x = min(dist - 1 - rim_width, 0)
local rim = rim_steep * rim_x * rim_x
local shape = smooth_max(cavity, floor_h, smooth)
shape = smooth_min(shape, rim, smooth)
crater_h = crater_h + shape * k.r
end
if dist > 0.8 and dist < ejecta_reach then
glow = glow + 1 - smoothstep(1, ejecta_reach, dist)
end
end
end
heights[i + 1] = h + crater_h * cr.depth
ejecta[i + 1] = min(1, glow)
if i % 64 == 0 then
checkpoint()
end
end
-- Positions on a unit sphere plus elevation, re-centered so the mean radius stays 1.
local mean = 0
for i = 1, count do
mean = mean + heights[i]
if i % 4096 == 0 then
checkpoint()
end
end
mean = mean / count
local positions = {}
local min_h, max_h = math.huge, -math.huge
for i = 0, count - 1 do
local hh = heights[i + 1] - mean
heights[i + 1] = hh
local r = 1 + hh
positions[i * 3 + 1] = dirs[i * 3 + 1] * r
positions[i * 3 + 2] = dirs[i * 3 + 2] * r
positions[i * 3 + 3] = dirs[i * 3 + 3] * r
if hh < min_h then
min_h = hh
end
if hh > max_h then
max_h = hh
end
if i % 2048 == 0 then
checkpoint()
end
end
checkpoint()
-- Area-weighted vertex normals (the same two triangles per quad as terrain.js).
local normals = {}
for i = 1, count * 3 do
normals[i] = 0
if i % 8192 == 0 then
checkpoint()
end
end
local P = positions
local function tri(a, b, c)
a, b, c = a * 3, b * 3, c * 3
local e1x, e1y, e1z = P[b + 1] - P[a + 1], P[b + 2] - P[a + 2], P[b + 3] - P[a + 3]
local e2x, e2y, e2z = P[c + 1] - P[a + 1], P[c + 2] - P[a + 2], P[c + 3] - P[a + 3]
local nx = e1y * e2z - e1z * e2y
local ny = e1z * e2x - e1x * e2z
local nz = e1x * e2y - e1y * e2x
normals[a + 1] = normals[a + 1] + nx
normals[a + 2] = normals[a + 2] + ny
normals[a + 3] = normals[a + 3] + nz
normals[b + 1] = normals[b + 1] + nx
normals[b + 2] = normals[b + 2] + ny
normals[b + 3] = normals[b + 3] + nz
normals[c + 1] = normals[c + 1] + nx
normals[c + 2] = normals[c + 2] + ny
normals[c + 3] = normals[c + 3] + nz
end
for f = 0, 5 do
local base = f * per
for y = 0, n - 1 do
for x = 0, n - 1 do
local i = base + y * row + x
tri(i, i + 1, i + n + 2)
tri(i, i + n + 2, i + n + 1)
end
checkpoint()
end
end
-- Faces duplicate their edge vertices; sum the copies so there are no lighting seams.
local seams = {}
for i = 0, count - 1 do
local key = keys[i + 1]
if key >= 0 then
local g = seams[key]
if g then
g[#g + 1] = i
else
seams[key] = { i }
end
end
if i % 4096 == 0 then
checkpoint()
end
end
checkpoint()
for _, group in pairs(seams) do
if #group > 1 then
local sx, sy, sz = 0, 0, 0
for _, i in ipairs(group) do
sx, sy, sz = sx + normals[i * 3 + 1], sy + normals[i * 3 + 2], sz + normals[i * 3 + 3]
end
for _, i in ipairs(group) do
normals[i * 3 + 1], normals[i * 3 + 2], normals[i * 3 + 3] = sx, sy, sz
end
end
end
for i = 0, count - 1 do
local o = i * 3
local nx, ny, nz = normals[o + 1], normals[o + 2], normals[o + 3]
local len = sqrt(nx * nx + ny * ny + nz * nz)
if len == 0 then
len = 1
end
normals[o + 1], normals[o + 2], normals[o + 3] = nx / len, ny / len, nz / len
if i % 2048 == 0 then
checkpoint()
end
end
checkpoint()
-- Colors: elevation gradient, dark patches, steep slopes, crater halos, grain.
local sd = p.shading
local low, mid, high = M.linear_color(sd.low), M.linear_color(sd.mid), M.linear_color(sd.high)
local steep, patch = M.linear_color(sd.steep), M.linear_color(sd.patch)
local colors = {}
local range = max_h - min_h
if range == 0 then
range = 1
end
local patch_amount, patch_freq, grain = sd.patchAmount, sd.patchFrequency, sd.grain
local st_lo, st_hi = sd.steepThreshold - sd.steepBlend, sd.steepThreshold + sd.steepBlend
local cr_ejecta = cr.ejecta
for i = 0, count - 1 do
local o = i * 3
local x, y, z = dirs[o + 1], dirs[o + 2], dirs[o + 3]
local e = (heights[i + 1] - min_h) / range
local lo, hi, te
if e < 0.5 then
lo, hi, te = low, mid, e * 2
else
lo, hi, te = mid, high, (e - 0.5) * 2
end
local c1 = lo[1] + (hi[1] - lo[1]) * te
local c2 = lo[2] + (hi[2] - lo[2]) * te
local c3 = lo[3] + (hi[3] - lo[3]) * te
if patch_amount > 0 then
local pn = fbm(x + ox + 31.7, y + oy - 12.2, z + oz + 5.5, 4, patch_freq, 0.5, 2)
-- Patches favor low ground, like maria.
local w = smoothstep(-0.05, 0.4, pn) * patch_amount * (1 - e * 0.6)
c1, c2, c3 = c1 + (patch[1] - c1) * w, c2 + (patch[2] - c2) * w, c3 + (patch[3] - c3) * w
end
local g = ejecta[i + 1] * cr_ejecta
c1, c2, c3 = c1 + (high[1] - c1) * g, c2 + (high[2] - c2) * g, c3 + (high[3] - c3) * g
local steepness = 1 - (normals[o + 1] * x + normals[o + 2] * y + normals[o + 3] * z)
local ws = smoothstep(st_lo, st_hi, steepness)
c1, c2, c3 = c1 + (steep[1] - c1) * ws, c2 + (steep[2] - c2) * ws, c3 + (steep[3] - c3) * ws
local gr = 1 + fbm(x + ox, y + oy, z + oz, 2, 14, 0.5, 2) * grain
colors[o + 1], colors[o + 2], colors[o + 3] = c1 * gr, c2 * gr, c3 * gr
if i % 128 == 0 then
checkpoint()
end
end
return {
n = n,
count = count,
positions = positions,
normals = normals,
colors = colors,
min_h = min_h,
max_h = max_h,
}
end
return M return M
+15 -5
View File
@@ -29,6 +29,7 @@
-- p95, max); "reset" starts over -- p95, max); "reset" starts over
local focus = require("modules.focus") local focus = require("modules.focus")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -73,11 +74,21 @@ end
-- Register an interactive element. kind: "button", "link", "tab", "field", "row", ... -- Register an interactive element. kind: "button", "link", "tab", "field", "row", ...
-- label: its text (for click "label=..."); zone: the stencil node clipping it, if any; -- label: its text (for click "label=..."); zone: the stencil node clipping it, if any;
-- no_focus: not in the keyboard focus order (the web's tabindex=-1). -- no_focus: not in the keyboard focus order (the web's tabindex=-1).
local function name(node, id)
if id then
names[gui.get_id(node)] = { id = id, scope = scope }
end
end
function M.register(id, node, kind, label, zone, no_focus) function M.register(id, node, kind, label, zone, no_focus)
blocks.note(M.register, id, node, kind, label, zone, no_focus) -- (a kept block registers again)
if zone and blocks.replaying() then
zone = require("druid.helper").get_closest_stencil_node(node) -- (kept, perhaps under a new stencil)
end
if not no_focus then if not no_focus then
focus.add(id, node, kind, label, zone) focus.add(id, node, kind, label, zone)
end end
M.name(node, id) name(node, id)
if not M.enabled or not id then if not M.enabled or not id then
return return
end end
@@ -97,9 +108,8 @@ end
-- Give a node an element's id (register does; also a node that takes the clicks for a -- Give a node an element's id (register does; also a node that takes the clicks for a
-- registered one). id_of: the id of a node, or nil. -- registered one). id_of: the id of a node, or nil.
function M.name(node, id) function M.name(node, id)
if id then blocks.note(name, node, id)
names[gui.get_id(node)] = { id = id, scope = scope } name(node, id)
end
end end
function M.id_of(node) function M.id_of(node)
@@ -347,7 +357,7 @@ function COMMANDS.scroll_to(g, cmd)
local scale = g.size[1] / sys.get_config_int("display.width", 1280) local scale = g.size[1] / sys.get_config_int("display.width", 1280)
local off = (g.scroll_pos and g.scroll_pos[a.key] or 0) + (vy - ey) * scale local off = (g.scroll_pos and g.scroll_pos[a.key] or 0) + (vy - ey) * scale
g.scroll_pos[a.key] = math.max(0, math.min(a.max, off)) g.scroll_pos[a.key] = math.max(0, math.min(a.max, off))
g.dirty = true g.dirty, g.dirty_all = true, true
return { ok = true, key = a.key, offset = g.scroll_pos[a.key] } return { ok = true, key = a.key, offset = g.scroll_pos[a.key] }
end end
end end
+2
View File
@@ -15,6 +15,7 @@
local ui = require("modules.ui") local ui = require("modules.ui")
local screen = require("modules.screen") local screen = require("modules.screen")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -59,6 +60,7 @@ function M.add(node, text)
if not node or not text or text == "" then if not node or not text or text == "" then
return return
end end
blocks.note(M.add, node, text) -- (a kept block adds it again)
tips[#tips + 1] = { node = node, text = tostring(text), zone = zone_of(node) } tips[#tips + 1] = { node = node, text = tostring(text), zone = zone_of(node) }
end end
+8 -1
View File
@@ -9,6 +9,8 @@
-- difference at the rebuild is kept as an offset that eases to 0 while the value goes on -- difference at the rebuild is kept as an offset that eases to 0 while the value goes on
-- ticking. Static values are the same thing with a constant function. -- ticking. Static values are the same thing with a constant function.
local blocks = require("modules.blocks")
local M = {} local M = {}
M.__index = M M.__index = M
@@ -46,9 +48,14 @@ end
-- source (the number itself for a static value, e.g. "start:end" for a timer): while it -- source (the number itself for a static value, e.g. "start:end" for a timer): while it
-- stays the same a running ease goes on; when it changes, the ease starts from what is -- stays the same a running ease goes on; when it changes, the ease starts from what is
-- shown. Returns the entry. -- shown. Returns the entry.
local function seen(self, key)
self.seen[key] = true
end
function M:track(key, value, sig) function M:track(key, value, sig)
sig = sig == nil and value or sig sig = sig == nil and value or sig
self.seen[key] = true seen(self, key)
blocks.note(seen, self, key) -- (a kept block keeps the key)
local e = self.vals[key] local e = self.vals[key]
if not e then if not e then
e = { shown = value, off = 0, t0 = 0, sig = sig } e = { shown = value, off = 0, t0 = 0, sig = sig }
+1 -1
View File
@@ -2,7 +2,7 @@
// The viewport node: shows the moon scene the render script drew into `moon_tex` (HDR, // The viewport node: shows the moon scene the render script drew into `moon_tex` (HDR,
// up to 2x the node's size: linear filtering at the pixel centers averages 2x2 samples) plus // up to 2x the node's size: linear filtering at the pixel centers averages 2x2 samples) plus
// its bloom (`bloom_tex`, half size, upsampled), with three.js's output pass: ACES // its bloom (`bloom_tex`, half the GUI reference size, upsampled), with three.js's output pass: ACES
// filmic tone mapping (exposure) and the sRGB transfer. // filmic tone mapping (exposure) and the sRGB transfer.
// Without a moon (moon_rect.z == 0) the node keeps its plain color. // Without a moon (moon_rect.z == 0) the node keeps its plain color.
+1 -1
View File
@@ -1,6 +1,6 @@
#version 140 #version 140
// The moon surface (vertex colors are linear RGB from terrain.lua). // The moon surface (vertex colors are linear RGB from mmterrain).
const float MOON_RADIUS = 2.0; // scene.js MOON_RADIUS const float MOON_RADIUS = 2.0; // scene.js MOON_RADIUS
in highp vec4 position; in highp vec4 position;
+114 -64
View File
@@ -1,5 +1,5 @@
-- The moon scene (see modules/moon/moon.lua): builds meshes for the requested moon id -- The moon scene (see modules/moon/moon.lua): builds meshes for the requested moon id
-- (terrain.lua, spread over frames, cached per id), spins the moon, orbits the camera -- (the mmterrain extension; cached per id), spins the moon, orbits the camera
-- (drag to rotate, wheel to zoom, like three.js OrbitControls with damping) and writes -- (drag to rotate, wheel to zoom, like three.js OrbitControls with damping) and writes
-- what the render script needs into moon.frame. Values follow server scene.js. -- what the render script needs into moon.frame. Values follow server scene.js.
-- --
@@ -24,9 +24,6 @@ local FOV = math.rad(35)
local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag local RESUME = 1.5 -- seconds before auto-rotation resumes after a drag
local ROTATE_SPEED = 0.6 local ROTATE_SPEED = 0.6
local DAMPING = 0.05 local DAMPING = 0.05
local BUDGET = html5 and 0.008 or 0.006 -- seconds of generation work per frame (the web runs
-- plain Lua, no LuaJIT: several times slower, so it gets more of each frame; 12 ms there
-- overran 60 fps frames: 33 fps for the same ~4 s)
local CACHE_SIZE = 6 local CACHE_SIZE = 6
local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing), local SUPERSAMPLE = 2 -- the scene renders at 2x and is averaged down (antialiasing),
-- counted in GUI reference pixels: a HiDPI display (or UI zoom) already gives the viewport -- counted in GUI reference pixels: a HiDPI display (or UI zoom) already gives the viewport
@@ -185,44 +182,21 @@ local function mesh_key(id, settings)
return id .. "|" .. terrain.serialize(rest) return id .. "|" .. terrain.serialize(rest)
end end
-- A job: a coroutine that generates the moon and fills a buffer, yielding when the -- A job generates one moon (the mmterrain extension, mmterrain/src/terrain.c) and fills
-- frame's budget is spent. -- its buffer: on a thread of its own on desktop; on the web (wasm, no threads) a share of
local WEB_RESOLUTION = 96 -- the web's cap on the grid (plain Lua: a quarter of the work of 180) -- each frame, then the buffer a part a frame.
local native = mmterrain
local NATIVE_SHARE, NATIVE_MIN, NATIVE_MAX = 0.3, 0.002, 0.012 -- (seconds of work a frame there)
local NATIVE_FILL = 32768 -- strip vertices copied into the buffer a frame there
local WEB_RESOLUTION = 128 -- the web's grid (desktop: the settings')
local function start_job(self, id, key, params) local function start_job(self, id, key, params)
if html5 and params.resolution and params.resolution > WEB_RESOLUTION then if html5 and params.resolution and params.resolution > WEB_RESOLUTION then
params.resolution = WEB_RESOLUTION params.resolution = WEB_RESOLUTION
end end
local job = { id = id, key = key, params = params, started = now(), frames = 0, work = 0 } local job = { id = id, key = key, params = params }
job.co = coroutine.create(function() job.gen = native.new(terrain.native_params(params))
local function checkpoint() job.threaded = native.start(job.gen)
if now() > job.deadline then
coroutine.yield()
end
end
local m = terrain.generate(params, checkpoint)
job.gen_work = job.work + (now() - job.resume_at)
checkpoint()
local strip = terrain.strip(m.n)
checkpoint()
local buf = buffer.create(#strip, STREAMS)
local pos = buffer.get_stream(buf, hash("position"))
local nor = buffer.get_stream(buf, hash("normal"))
local col = buffer.get_stream(buf, hash("color"))
local P, N, C = m.positions, m.normals, m.colors
for k = 1, #strip do
local v = strip[k] * 3
local o = (k - 1) * 3
pos[o + 1], pos[o + 2], pos[o + 3] = P[v + 1], P[v + 2], P[v + 3]
nor[o + 1], nor[o + 2], nor[o + 3] = N[v + 1], N[v + 2], N[v + 3]
col[o + 1], col[o + 2], col[o + 3] = C[v + 1], C[v + 2], C[v + 3]
if k % 2048 == 0 then
checkpoint()
end
end
job.buffer = buf
job.vertices = #strip
end)
self.job = job self.job = job
end end
@@ -272,30 +246,88 @@ local function set_mesh(self, key, res, params)
self.shown_params = params self.shown_params = params
end end
local function step_job(self) -- A finished job's buffer becomes a mesh resource in the cache (and the shown moon when
local job = self.job -- it's the wanted one).
local t0 = now() local function job_done(self, job)
job.resume_at = t0
job.deadline = t0 + BUDGET
job.frames = job.frames + 1
local ok, err = coroutine.resume(job.co)
job.work = job.work + (now() - t0)
if not ok then
print("moon: generation failed: " .. tostring(err))
self.job = nil
return
end
if coroutine.status(job.co) == "dead" then
self.job = nil
local res = new_buffer_resource(job.buffer) local res = new_buffer_resource(job.buffer)
cache_put(self, job.key, res) cache_put(self, job.key, res)
print(string.format("moon: %s (%s) built: %.0f ms of work (terrain %.0f, buffer %.0f) over %d frames (%.0f ms wall), %d vertices", print(string.format("moon: %s (%s) built, %d vertices", job.id, job.params.technique, job.vertices))
job.id, job.params.technique, job.work * 1000, job.gen_work * 1000, (job.work - job.gen_work) * 1000,
job.frames, (now() - job.started) * 1000, job.vertices))
if job.key == self.want_key then if job.key == self.want_key then
set_mesh(self, job.key, res, job.params) set_mesh(self, job.key, res, job.params)
end end
end end
-- Done (threaded) or stepped to the end, then the buffer is filled (on the web a part a
-- frame, the frames after the last step). Returns true when complete.
local function step_native(self, job)
if not job.buffer then
local done
if job.threaded then
done = native.done(job.gen)
else
done = native.step(job.gen, math.max(NATIVE_MIN, math.min(NATIVE_MAX, (self.frame_dt or 0) * NATIVE_SHARE)))
end
if not done then
return false
end
job.vertices = native.vertices(job.gen)
job.buffer = buffer.create(job.vertices, STREAMS)
job.filled = 0
if not job.threaded then
return false -- (the fill starts next frame)
end
end
local to = job.threaded and job.vertices or math.min(job.vertices, job.filled + NATIVE_FILL)
native.fill(job.gen, job.buffer, job.filled + 1, to)
job.filled = to
if to < job.vertices then
return false
end
native.free(job.gen)
job.gen = nil
return true
end
-- A job left behind for another moon: a thread runs to the end (freeing it would wait
-- for it), and the moon goes into the cache; a stepped one is dropped.
local function orphan(self, job)
if job.threaded and type(job.gen) == "userdata" then
self.orphans[#self.orphans + 1] = job
elseif type(job.gen) == "userdata" then
native.free(job.gen)
end
end
local function step_orphans(self)
for i = #self.orphans, 1, -1 do
local job = self.orphans[i]
if native.done(job.gen) then
table.remove(self.orphans, i)
step_native(self, job)
job_done(self, job)
end
end
end
local function step_job(self)
local job = self.job
if step_native(self, job) then
self.job = nil
job_done(self, job)
end
end
-- The mesh for moon `id` (from the cache, or a job for it); returns its key and params.
local function want(self, id)
local params = terrain.resolve(moon.settings, id)
local key = mesh_key(id, moon.settings)
if not cache_get(self, key) and not (self.job and self.job.key == key) then
if self.job then
orphan(self, self.job)
end
start_job(self, id, key, params)
end
return key, params
end end
---------------------------------------------------------------- camera ---------------------------------------------------------------- camera
@@ -325,6 +357,7 @@ end
function init(self) function init(self)
msg.post(".", "acquire_input_focus") msg.post(".", "acquire_input_focus")
self.cache = {} self.cache = {}
self.orphans = {}
self.spin = 0 self.spin = 0
self.spin_speed = 0 self.spin_speed = 0
self.dragging = false self.dragging = false
@@ -366,6 +399,12 @@ function final(self)
for _, e in ipairs(self.cache) do for _, e in ipairs(self.cache) do
resource.release(e.res) resource.release(e.res)
end end
if self.job and type(self.job.gen) == "userdata" then
native.free(self.job.gen)
end
for _, job in ipairs(self.orphans) do
native.free(job.gen)
end
resource.release(self.shell_res) resource.release(self.shell_res)
resource.release(self.stars_res) resource.release(self.stars_res)
resource.release(self.motes_res) resource.release(self.motes_res)
@@ -373,18 +412,13 @@ end
function update(self, dt) function update(self, dt)
dt = math.min(dt, 0.1) dt = math.min(dt, 0.1)
self.frame_dt = self.frame_dt and (self.frame_dt * 0.8 + dt * 0.2) or dt -- (smoothed, for the native budget)
local f = moon.frame local f = moon.frame
local rect = moon.rect local rect = moon.rect
local mystery = moon.mode == "mystery" local mystery = moon.mode == "mystery"
if (not moon.id and not mystery) or not rect or rect.w <= 0 or rect.h <= 0 then
f.visible = false
return
end
if mystery and moon.settings and not self.mystery_values then
self.mystery_values = render_values(terrain.resolve(moon.settings, "mystery"))
end
-- The wanted mesh: from the cache, or start (or keep) a job for it. -- The wanted mesh: from the cache, or start (or keep) a job for it. A moon asked for
-- ahead (moon.prefetch: a bought one, before its reveal shows) gets one too.
if moon.settings and moon.id then if moon.settings and moon.id then
if moon.id ~= self.want_id or moon.settings ~= self.want_settings then if moon.id ~= self.want_id or moon.settings ~= self.want_settings then
self.want_id = moon.id self.want_id = moon.id
@@ -400,12 +434,28 @@ function update(self, dt)
end end
end end
if self.shown_key ~= self.want_key and not (self.job and self.job.key == self.want_key) then if self.shown_key ~= self.want_key and not (self.job and self.job.key == self.want_key) then
start_job(self, moon.id, self.want_key, self.want_params) want(self, moon.id)
end end
elseif moon.settings and moon.prefetch_id and not self.job then
want(self, moon.prefetch_id)
moon.prefetch_id = nil
end
if moon.prefetch_id == moon.id then
moon.prefetch_id = nil
end end
if self.job then if self.job then
step_job(self) step_job(self)
end end
if #self.orphans > 0 then
step_orphans(self)
end
if (not moon.id and not mystery) or not rect or rect.w <= 0 or rect.h <= 0 then
f.visible = false
return
end
if mystery and moon.settings and not self.mystery_values then
self.mystery_values = render_values(terrain.resolve(moon.settings, "mystery"))
end
local values = self.values local values = self.values
if mystery then if mystery then
+5 -2
View File
@@ -57,7 +57,9 @@ local BLOOM_KERNELS = { 3, 5, 7, 9, 11 }
local CLEAR_BLACK = { [graphics.BUFFER_TYPE_COLOR0_BIT] = vmath.vector4(0, 0, 0, 0) } local CLEAR_BLACK = { [graphics.BUFFER_TYPE_COLOR0_BIT] = vmath.vector4(0, 0, 0, 0) }
-- three.js UnrealBloomPass on the moon scene (moon_post material): high pass at half the -- three.js UnrealBloomPass on the moon scene (moon_post material): high pass at half the
-- viewport size, 5 mips of separable blur, composite into "bloom_h1". -- viewport's GUI reference size (the web's CSS pixels: a HiDPI display or UI zoom does the
-- same work as 1x, and the blur's texels cover the same UI pixels, so the glow looks the
-- same), 5 mips of separable blur, composite into "bloom_h1".
local function draw_bloom(self, f, scene_rt) local function draw_bloom(self, f, scene_rt)
local p = self.predicates.moon_post local p = self.predicates.moon_post
local c = self.post_constants local c = self.post_constants
@@ -69,7 +71,8 @@ local function draw_bloom(self, f, scene_rt)
render.clear(CLEAR_BLACK) render.clear(CLEAR_BLACK)
render.draw(p, options) render.draw(p, options)
end end
local w, h = math.floor(f.rect.w / 2 + 0.5), math.floor(f.rect.h / 2 + 0.5) local u = math.max(1, moon.unit) -- (window pixels per GUI reference pixel)
local w, h = math.max(1, math.floor(f.rect.w / u / 2 + 0.5)), math.max(1, math.floor(f.rect.h / u / 2 + 0.5))
local bright = hdr_target(self, "bloom_bright", w, h) local bright = hdr_target(self, "bloom_bright", w, h)
c.post = vmath.vector4(0, 0, 0, 0) c.post = vmath.vector4(0, 0, 0, 0)
c.post2 = vmath.vector4(0, 0, b.z, 0) c.post2 = vmath.vector4(0, 0, b.z, 0)
+4 -1
View File
@@ -308,7 +308,10 @@ function M.build(self, root, px, py, w, h, H)
end end
y = card.grid(self, c, x, y, inner, listed, function(cd, a) y = card.grid(self, c, x, y, inner, listed, function(cd, a)
advisor_card(self, H, ad, cd, a) advisor_card(self, H, ad, cd, a)
end) end, { key = function(a) return "adv:" .. tostring(a.name) end, sig = function()
-- (kept while what it shows stays the same: modules/blocks)
return { ad.selected or false, self.advisor_pick or false, self.inflight, self.view }
end })
area:finish(y + py) area:finish(y + py)
end end
+2 -1
View File
@@ -5,6 +5,7 @@
-- thinned to about one point per few pixels so a chart stays a few hundred nodes. -- thinned to about one point per few pixels so a chart stays a few hundred nodes.
local ui = require("modules.ui") local ui = require("modules.ui")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -199,7 +200,7 @@ function M.chart(parent, x, y, w, h, data, opts)
gui.set_position(tip, vec(tx, -math.max(0, math.min(ch - 22, cy - 30)))) gui.set_position(tip, vec(tx, -math.max(0, math.min(ch - 22, cy - 30))))
end end
if opts.pointers then if opts.pointers then
opts.pointers[#opts.pointers + 1] = move blocks.push(opts.pointers, "pointers", move) -- (opts.pointers: the screen, g)
end end
return box return box
end end
+8 -1
View File
@@ -10,6 +10,7 @@ local button = require("components.button")
local stat = require("components.stat") local stat = require("components.stat")
local anim = require("modules.anim") local anim = require("modules.anim")
local test = require("modules.test") local test = require("modules.test")
local blocks = require("modules.blocks")
local S = theme.size local S = theme.size
@@ -141,7 +142,13 @@ function M.action(g, b, nodes, name, params, on_done, predictor, id)
ready_look(g, b, key) ready_look(g, b, key)
end end
if g.autoplay then if g.autoplay then
g.autoplay:register(name, type(params) == "function" and params() or params, fire) local p = type(params) == "function" and params() or params
g.autoplay:register(name, p, fire)
blocks.note(function() -- (a kept block registers again)
if g.autoplay then
g.autoplay:register(name, p, fire)
end
end)
end end
return fire return fire
end end
+178 -45
View File
@@ -3,12 +3,14 @@
-- for the module contract the regions and sections follow. -- for the module contract the regions and sections follow.
-- --
-- Rebuild model: every new state (a response, a prediction) or a window/UI-scale change -- Rebuild model: every new state (a response, a prediction) or a window/UI-scale change
-- sets g.dirty and the next update rebuilds the whole layout from the state: the Druid -- sets g.dirty and the next update rebuilds the layout from the state. The regions and the
-- instance and every layout node are dropped and made again. What must survive a rebuild -- costly parts inside them are blocks (modules/blocks): one whose inputs (its signature)
-- lives on g (scroll offsets g.scroll_pos, fields g.fields, eased values g.tween, effects -- didn't change is kept with its nodes and Druid components, the rest is built again; a
-- g.fx and toasts g.toasts on their own layers). Regions are laid out here: banner and -- full rebuild (g.dirty_all: the theme, reduced motion, a size change, ...) drops the
-- header across the top, then the moon list, the main panel (the current section) and -- Druid instance and every layout node first. What must survive a rebuild lives on g
-- the aside. -- (scroll offsets g.scroll_pos, fields g.fields, eased values g.tween, effects g.fx and
-- toasts g.toasts on their own layers). Regions are laid out here: banner and header
-- across the top, then the moon list, the main panel (the current section) and the aside.
local druid = require("druid.druid") local druid = require("druid.druid")
local theme = require("modules.theme") local theme = require("modules.theme")
@@ -36,6 +38,9 @@ local moonlist = require("screens.game.moonlist")
local aside = require("screens.game.aside") local aside = require("screens.game.aside")
local reveal = require("screens.game.reveal") local reveal = require("screens.game.reveal")
local moon_view = require("modules.moon.moon") local moon_view = require("modules.moon.moon")
local blocks = require("modules.blocks")
local sfx = require("modules.sfx")
local ready = require("screens.game.core.ready")
local S = theme.size local S = theme.size
local NARROW_W = 1250 -- below this layout width the aside gets its own row (app.css) local NARROW_W = 1250 -- below this layout width the aside gets its own row (app.css)
@@ -130,26 +135,6 @@ local function boot_screen(g, W, H)
end end
end end
local function build_main(g, parent, x, y, w, h)
local st = g.state
local p = panel.new(parent, x, y, w, h)
g.main_panel = p
local root = p.body
local px, py = S.main_pad_x, S.main_pad_y
local section = g.view.section
local mod = M.SECTIONS[section]
if g.reveal:hiding() then
return -- what the new moon holds stays hidden until its card is closed
elseif section == "new-moon" then
local inner = math.min(w - px * 2, S.content_max)
reveal.build_teaser(g, root, px + math.floor((w - px * 2 - inner) / 2), py, inner)
elseif mod and (NO_BLOCK[section] or st[section]) then
mod.build(g, root, px, py, w, h, g.h) -- modules center their own content
elseif not mod then
coming_soon(g, root, px, py, w)
end
end
-- Whether the aside shows, like the web's (page.html: hidden unless the view selects -- Whether the aside shows, like the web's (page.html: hidden unless the view selects
-- something for the viewport, views.go Scene.Select): a moon (s=moons with a moon), the -- something for the viewport, views.go Scene.Select): a moon (s=moons with a moon), the
-- new-moon teaser, the drill panel (Equipment with a drill) or an advisor's panel. Other -- new-moon teaser, the drill panel (Equipment with a drill) or an advisor's panel. Other
@@ -193,12 +178,126 @@ function M.aside_shown(g)
return false return false
end end
-- What each section shows from the state (besides the view and the screen-wide inputs):
-- its block is kept while these stay the same (modules/blocks). The aside shows the
-- section's own aside or the moon's well panel.
local ALWAYS = { "rules", "unlocked", "catalogVersion", "bonuses", "liveEvents", "showNav" }
local SECTION_KEYS = {
moons = { "moon", "moons", "past", "tools", "well", "moonPrice", "stakes", "idle" },
market = { "market" },
refinery = { "refinery", "jobs", "refined", "credits", "resources" },
leaderboard = { "leaderboard" },
help = { "help" },
equipment = { "equipment", "moons", "tools", "refined", "credits", "resources" },
store = { "store", "credits", "moonPrice", "resources", "refined" },
advisors = { "advisors", "credits" },
["new-moon"] = { "newMoon", "moonPrice", "credits" },
}
local ASIDE_KEYS = { "moon", "moons", "past", "tools", "well", "stakes", "equipment", "advisors", "newMoon", "moonPrice", "credits", "refined" }
local function pick_keys(st, ...)
local out = {}
for _, list in ipairs({ ... }) do
for _, k in ipairs(list) do
if out[k] == nil then
out[k] = st[k] == nil and false or st[k]
end
end
end
if st.catalogVersion == nil then
out.catalog = st.catalog or false
end
return out
end
-- Drills and batches whose end passed on our clock (core/ready counts them before the
-- refetch): the counts they show change with it.
local function ready_key(g)
local st, now = g.state, session.now(g)
local out = {}
for _, list in ipairs({ st.stakes or {}, st.jobs or {} }) do
for _, x in ipairs(list) do
if not x.ready and x["end"] and x["end"] <= now then
out[#out + 1] = tostring(x.id or x.toolId) .. "@" .. x["end"]
end
end
end
return table.concat(out, ",")
end
-- What every region depends on besides the state: busy buttons, Claims that turned ready
-- on our clock, the toggles.
local function local_sig(g)
return blocks.sig(g.inflight, g.ready_at, ready_key(g), sfx.muted(), anim.reduced, theme.name)
end
-- The reveal hides the main panel, the aside's content and the new moon's row.
local function reveal_sig(g)
local r = g.reveal
return blocks.sig(r:active(), r:hiding(), r.r and r.r.data and r.r.data.moonId or "")
end
-- The section's block in the main panel.
local function build_main(g, parent, x, y, w, h, common)
local st = g.state
local section = g.view.section
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, g.view, g.advisor_pick, g.refine_qty, g.sale_open,
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}))
blocks.keep(g, "main", sig, parent, function(holder)
for _, m in pairs(M.SECTIONS) do
if m.teardown then
m.teardown(g)
end
end
g.well_nodes = {}
g.frames = {} -- nested frames (cards) the main panel's entrance staggers
local p = panel.new(holder, x, y, w, h)
g.main_panel = p
local root = p.body
local px, py = S.main_pad_x, S.main_pad_y
local mod = M.SECTIONS[section]
if g.reveal:hiding() then
return -- what the new moon holds stays hidden until its card is closed
elseif section == "new-moon" then
local inner = math.min(w - px * 2, S.content_max)
reveal.build_teaser(g, root, px + math.floor((w - px * 2 - inner) / 2), py, inner)
elseif mod and (NO_BLOCK[section] or st[section]) then
mod.build(g, root, px, py, w, h, g.h) -- modules center their own content
elseif not mod then
coming_soon(g, root, px, py, w)
end
end)
end
-- The aside's block (narrow: the row under the panels).
local function build_aside(g, parent, x, y, w, h, row, common)
local st = g.state
local section = g.view.section
local sig = blocks.sig(common, reveal_sig(g), x, y, w, h, row, g.view, g.advisor_pick, g.sale_open,
pick_keys(st, ALWAYS, SECTION_KEYS[section] or {}, ASIDE_KEYS))
return blocks.keep(g, "aside", sig, parent, function(holder)
return aside.build(g, holder, x, y, w, h, g.h, M.SECTIONS[section], row)
end)
end
-- A full rebuild drops every node and component and builds them all again; otherwise the
-- regions (and the blocks inside them) whose inputs didn't change are kept (modules/
-- blocks). Full: the first state, a window or UI-scale change, the narrow layout (one
-- page scroll), the theme or another screen-wide toggle (g.dirty_all).
function M.build(g) function M.build(g)
g.builds = (g.builds or 0) + 1 -- (test counter) g.builds = (g.builds or 0) + 1 -- (test counter)
local W, H = screen.fit(g.root)
local size = { W, H, select(3, screen.layout()) }
local narrow = W < NARROW_W
local full = not g.layout or not g.state or not g.layout_state or g.dirty_all or narrow
or not g.size or g.size[1] ~= size[1] or g.size[2] ~= size[2] or g.size[3] ~= size[3]
g.dirty_all = false
g.full_builds = (g.full_builds or 0) + (full and 1 or 0) -- (test counter)
-- The page's ground and its top-right glow (game.gui) in the theme's colors. -- The page's ground and its top-right glow (game.gui) in the theme's colors.
gui.set_color(gui.get_node("bg"), theme.color.bg) gui.set_color(gui.get_node("bg"), theme.color.bg)
gui.set_color(gui.get_node("glow"), theme.color.bg_glow) gui.set_color(gui.get_node("glow"), theme.color.bg_glow)
button.begin_build() -- (a press the rebuild interrupts carries over: components/button) button.begin_build() -- (a press the rebuild interrupts carries over: components/button)
if full then
each_module(function(m) each_module(function(m)
if m.teardown then if m.teardown then
m.teardown(g) m.teardown(g)
@@ -209,41 +308,57 @@ function M.build(g)
g.druid:final() g.druid:final()
g.tearing_down = false g.tearing_down = false
g.druid = druid.new(g) g.druid = druid.new(g)
blocks.hook(g.druid)
gui.delete_node(g.layout) gui.delete_node(g.layout)
g.layout = nil
end end
local W, H = screen.fit(g.root) blocks.clear(g)
g.size = { W, H, select(3, screen.layout()) } end
blocks.begin(g, full)
g.size = size
gui.set_size(g.page, vec(W, H)) gui.set_size(g.page, vec(W, H))
gui.set_position(g.page, vec(-W / 2, H / 2)) gui.set_position(g.page, vec(-W / 2, H / 2))
g.tickers = {} g.tickers = {}
g.scroll_areas = {} g.scroll_areas = {}
g.tween:begin() g.tween:begin()
g.tab_nodes, g.moon_nodes, g.well_nodes = {}, {}, {}
g.fx:begin_build() g.fx:begin_build()
test.begin_build() test.begin_build()
tooltip.begin_build() tooltip.begin_build()
g.inputs = {} -- text fields (core/keys: Enter submits, keys skip while editing) g.inputs = {} -- text fields (core/keys: Enter submits, keys skip while editing)
g.pointers = {} -- mouse-move handlers (the market chart's crosshair)
if g.autoplay then if g.autoplay then
g.autoplay:reset() g.autoplay:reset()
end end
if not g.layout then
g.layout = ui.rect(g.page, 0, 0, W, H, "none") g.layout = ui.rect(g.page, 0, 0, W, H, "none")
gui.move_below(g.layout, g.toast_layer) gui.move_below(g.layout, g.toast_layer)
end
g.toasts:set_height(H) g.toasts:set_height(H)
g.dialog:layout(W, H) g.dialog:layout(W, H)
if not g.state then if not g.state then
g.tab_nodes, g.moon_nodes, g.well_nodes = {}, {}, {}
boot_screen(g, W, H) boot_screen(g, W, H)
g.layout_state = false
g.dirty = false g.dirty = false
blocks.finish(g)
return return
end end
g.pointers = {} -- mouse-move handlers (the market chart's crosshair) g.layout_state = true
-- Announcements and live events above the header.
local banner_h = banner.build(g, g.h, g.layout, W)
local header_root = ui.rect(g.layout, 0, banner_h, W, 1, "none")
g.header_node = header_root
local header_h = header.build(g, header_root, 0, 0, W, nil, g.h) + banner_h
local st = g.state local st = g.state
local common = local_sig(g)
-- Announcements and live events above the header.
local banner_h = blocks.keep(g, "banner", blocks.sig(common, W, st.banner), g.layout, function(holder)
return banner.build(g, g.h, holder, W)
end)
local header_h = blocks.keep(g, "header", blocks.sig(common, W, banner_h, g.view.section, st.credits, st.resources,
st.unlocked, st.shine, st.ready, st.nextEnd, ready.drills(g), ready.batches(g)), g.layout, function(holder)
g.tab_nodes = {}
local header_root = ui.rect(holder, 0, banner_h, W, 1, "none")
g.header_node = header_root
return header.build(g, header_root, 0, 0, W, nil, g.h) + banner_h
end)
local pad, gap = S.body_pad, S.gap local pad, gap = S.body_pad, S.gap
local top = header_h + pad local top = header_h + pad
local body_h = H - top - pad local body_h = H - top - pad
@@ -251,7 +366,6 @@ function M.build(g)
-- Narrow windows (the web's @media max-width 1250px): the aside goes to its own row -- Narrow windows (the web's @media max-width 1250px): the aside goes to its own row
-- under the moon list and the main panel, the viewport and the side panel side by -- under the moon list and the main panel, the viewport and the side panel side by
-- side, and the page scrolls to it (a page scroll area under the header). -- side, and the page scrolls to it (a page scroll area under the header).
local narrow = W < NARROW_W
local aside_w = has_aside and math.max(S.aside_w_min, math.min(S.aside_w_max, math.floor(W * 0.33))) or 0 local aside_w = has_aside and math.max(S.aside_w_min, math.min(S.aside_w_max, math.floor(W * 0.33))) or 0
-- The moon list shows once there is a moon (or the store is open), like the web. -- The moon list shows once there is a moon (or the store is open), like the web.
local list_w = math.max(S.moonlist_w, math.min(S.moonlist_w_max, math.floor(W * 0.15))) local list_w = math.max(S.moonlist_w, math.min(S.moonlist_w_max, math.floor(W * 0.15)))
@@ -279,12 +393,24 @@ function M.build(g)
parent, y0 = page.content, pad parent, y0 = page.content, pad
end end
if list_w > 0 then if list_w > 0 then
moonlist.build(g, g.h, parent, pad, y0, list_w, body_h) -- (the rows' ready counts: per moon, from the stakes on our clock)
local v = g.view
local hides = {}
for _, m in ipairs(st.moons) do
hides[#hides + 1] = g.reveal:hides(m.id) or false
end end
g.frames = {} -- nested frames (cards) the main panel's entrance staggers blocks.keep(g, "moonlist", blocks.sig(common, reveal_sig(g), pad, y0, list_w, body_h, v.section, v.past, v.page, v.moon,
build_main(g, parent, main_x, y0, main_w, body_h) st.moons, st.past, st.pastPage, st.unlocked, st.moonPrice, hides), parent, function(holder)
g.moon_nodes = {}
moonlist.teardown(g)
moonlist.build(g, g.h, holder, pad, y0, list_w, body_h)
end)
else
g.moon_nodes = {}
end
build_main(g, parent, main_x, y0, main_w, body_h, common)
if page then if page then
g.aside_panel = aside.build(g, parent, pad, y0 + body_h + gap, aside_w, row_h, g.h, M.SECTIONS[g.view.section], true) g.aside_panel = build_aside(g, parent, pad, y0 + body_h + gap, aside_w, row_h, true, common)
local s = page:finish(y0 + body_h + gap + row_h + pad) local s = page:finish(y0 + body_h + gap + row_h + pad)
if g.viewport_node then if g.viewport_node then
moon_view.move(g.viewport_node) -- (where the page's offset put it) moon_view.move(g.viewport_node) -- (where the page's offset put it)
@@ -311,7 +437,7 @@ function M.build(g)
end) end)
end end
elseif has_aside then elseif has_aside then
g.aside_panel = aside.build(g, parent, W - pad - aside_w, top, aside_w, body_h, g.h, M.SECTIONS[g.view.section]) g.aside_panel = build_aside(g, parent, W - pad - aside_w, top, aside_w, body_h, nil, common)
else else
g.aside_panel, g.viewport_node, g.viewport_rect, g.view_was_mystery = nil, nil, nil, false g.aside_panel, g.viewport_node, g.viewport_rect, g.view_was_mystery = nil, nil, nil, false
moon_view.hide() moon_view.hide()
@@ -319,6 +445,8 @@ function M.build(g)
if not page then if not page then
g.page_key, g.page_main = nil, nil g.page_key, g.page_main = nil, nil
end end
blocks.finish(g)
ready.watch(g) -- (rebuild when the next drill or batch end passes)
enter(g) enter(g)
g.tween:finish() g.tween:finish()
ticks.run(g) ticks.run(g)
@@ -370,6 +498,7 @@ function M.init(g)
end end
msg.post(".", "acquire_input_focus") msg.post(".", "acquire_input_focus")
g.druid = druid.new(g) g.druid = druid.new(g)
blocks.hook(g.druid)
g.root = gui.get_node("root") g.root = gui.get_node("root")
g.page = gui.get_node("page") g.page = gui.get_node("page")
theme.set(theme.name) -- the saved theme's clear color (the render script starts dark) theme.set(theme.name) -- the saved theme's clear color (the render script starts dark)
@@ -403,6 +532,7 @@ function M.final(g)
end) end)
g.dialog:remove() g.dialog:remove()
g.druid:final() g.druid:final()
blocks.clear(g)
end end
local TOUCH = hash("touch") local TOUCH = hash("touch")
@@ -460,7 +590,7 @@ local TEST_API = {
g.scroll_pos = g.scroll_pos or {} g.scroll_pos = g.scroll_pos or {}
g.scroll_pos[k:sub(8)] = tonumber(v) or 0 g.scroll_pos[k:sub(8)] = tonumber(v) or 0
end end
g.dirty = true g.dirty, g.dirty_all = true, true
end, end,
} }
@@ -468,7 +598,10 @@ function M.update(g, dt)
local t0 = test.enabled and test.clock() local t0 = test.enabled and test.clock()
local built local built
local W, H, scale = screen.layout() local W, H, scale = screen.layout()
if g.dirty or W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3] then local resized = W ~= g.size[1] or H ~= g.size[2] or scale ~= g.size[3]
-- (while the reveal card flies into the moon list, a new state waits for it to land:
-- the page is hidden under it anyway, and the flight keeps its frames)
if resized or (g.dirty and not g.reveal:closing()) then
M.build(g) M.build(g)
built = t0 and test.clock() - t0 built = t0 and test.clock() - t0
end end
@@ -534,7 +667,7 @@ function M.on_reload(g)
theme.set(theme.name) -- (the page's clear color) theme.set(theme.name) -- (the page's clear color)
ensure(g) ensure(g)
g.h = hooks.table() g.h = hooks.table()
g.dirty = true g.dirty, g.dirty_all = true, true
end end
return M return M
+5 -4
View File
@@ -5,6 +5,7 @@
local ui = require("modules.ui") local ui = require("modules.ui")
local session = require("screens.game.core.session") local session = require("screens.game.core.session")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -20,7 +21,7 @@ function M.tick_text(g, node, ends, fmt, done, key, on_done)
if key then if key then
g.tween:track(key, ends - now(g), ends) g.tween:track(key, ends - now(g), ends)
end end
g.tickers[#g.tickers + 1] = { kind = "time", node = node, ends = ends, fmt = fmt, done = done, key = key, on_done = on_done } blocks.push(g, "tickers", { kind = "time", node = node, ends = ends, fmt = fmt, done = done, key = key, on_done = on_done })
end end
local function bar_fill(t, start, ends) local function bar_fill(t, start, ends)
@@ -33,7 +34,7 @@ function M.tick_bar(g, node, start, ends, w, key, on_full)
if key then if key then
g.tween:track(key, bar_fill(now(g), start, ends), start .. ":" .. ends) g.tween:track(key, bar_fill(now(g), start, ends), start .. ":" .. ends)
end end
g.tickers[#g.tickers + 1] = { kind = "bar", node = node, start = start, ends = ends, w = w, key = key, on_done = on_full } blocks.push(g, "tickers", { kind = "bar", node = node, start = start, ends = ends, w = w, key = key, on_done = on_full })
end end
-- A number text node showing fmt(value), easing from the shown value when it changes. -- A number text node showing fmt(value), easing from the shown value when it changes.
@@ -41,7 +42,7 @@ end
function M.tick_num(g, node, key, value, fmt) function M.tick_num(g, node, key, value, fmt)
g.tween:track(key, value) g.tween:track(key, value)
local k = { kind = "num", node = node, key = key, value = value, fmt = fmt or ui.money } local k = { kind = "num", node = node, key = key, value = value, fmt = fmt or ui.money }
g.tickers[#g.tickers + 1] = k blocks.push(g, "tickers", k)
local v = g.tween:value(key, value) local v = g.tween:value(key, value)
if v ~= value and value == math.floor(value) then if v ~= value and value == math.floor(value) then
v = math.floor(v + 0.5) -- counts tick as whole numbers v = math.floor(v + 0.5) -- counts tick as whole numbers
@@ -55,7 +56,7 @@ function M.run(g)
local t = now(g) local t = now(g)
local tw = g.tween local tw = g.tween
for _, k in ipairs(g.tickers) do for _, k in ipairs(g.tickers) do
if k.dead then if k.dead or not pcall(gui.get_id, k.node) then
-- node gone (deleted outside a rebuild) -- node gone (deleted outside a rebuild)
elseif k.kind == "time" then elseif k.kind == "time" then
local left = k.ends - t local left = k.ends - t
+5 -1
View File
@@ -225,7 +225,11 @@ function M.build(self, root, px, py, w, h, H)
end end
y = card.grid(self, c, x, y, inner, eq.tools, function(cd, t) y = card.grid(self, c, x, y, inner, eq.tools, function(cd, t)
tool_card(self, H, st, cd, t, open) tool_card(self, H, st, cd, t, open)
end) end, { key = function(t) return "eq:tool:" .. tostring(t.id) end, sig = function(t)
-- (kept while what it shows stays the same: modules/blocks)
local moon = t.stake and find(st.moons, "id", t.stake.moonId)
return { open, moon and moon.name or false, st.refined, st.credits, st.resources, st.catalogVersion, self.inflight, self.view }
end })
area:finish(y + py) area:finish(y + py)
end end
+3 -3
View File
@@ -77,7 +77,7 @@ local function wallet(g, parent, W)
tooltip.add(mute, sfx.muted() and "Sound off" or "Sound on") tooltip.add(mute, sfx.muted() and "Sound off" or "Sound on")
g.druid:new_button(mute, function() g.druid:new_button(mute, function()
sfx.toggle_mute() sfx.toggle_mute()
g.dirty = true g.dirty, g.dirty_all = true, true
end):set_animations_disabled() end):set_animations_disabled()
-- Light / dark theme (saved); the icon shows the theme it switches to, like the web. -- Light / dark theme (saved); the icon shows the theme it switches to, like the web.
local tx = cx - 14 - 28 - 8 local tx = cx - 14 - 28 - 8
@@ -88,7 +88,7 @@ local function wallet(g, parent, W)
tooltip.add(tgl, "Light / dark theme") tooltip.add(tgl, "Light / dark theme")
g.druid:new_button(tgl, function() g.druid:new_button(tgl, function()
theme.toggle() theme.toggle()
g.dirty = true g.dirty, g.dirty_all = true, true
end):set_animations_disabled() end):set_animations_disabled()
-- Reduced motion (saved): speed lines, lit while motion is on (the web follows the -- Reduced motion (saved): speed lines, lit while motion is on (the web follows the
-- system's setting; here it's the player's). -- system's setting; here it's the player's).
@@ -109,7 +109,7 @@ local function wallet(g, parent, W)
g.druid:new_button(mot, function() g.druid:new_button(mot, function()
anim.set_reduced_motion(not anim.reduced) anim.set_reduced_motion(not anim.reduced)
sfx.play("toggle") sfx.play("toggle")
g.dirty = true g.dirty, g.dirty_all = true, true
end):set_animations_disabled() end):set_animations_disabled()
-- Display settings (UI scale, fullscreen: screens/game/settings.lua); a screen icon. -- Display settings (UI scale, fullscreen: screens/game/settings.lua); a screen icon.
local dx = mx - 28 - 8 local dx = mx - 28 - 8
+22 -5
View File
@@ -15,6 +15,7 @@ local chart = require("screens.game.chart")
local ticker = require("screens.game.ticker") local ticker = require("screens.game.ticker")
local sale = require("screens.game.sale") local sale = require("screens.game.sale")
local scroll = require("components.scroll") local scroll = require("components.scroll")
local blocks = require("modules.blocks")
-- Windows this narrow (layout units) stack the detail and the sell ticket, and the -- Windows this narrow (layout units) stack the detail and the sell ticket, and the
-- section scrolls (app.css @media max-width 1100px: .mk-grid one column). -- section scrolls (app.css @media max-width 1100px: .mk-grid one column).
@@ -168,7 +169,11 @@ local function card(self, H, root, x, y, w, h, q, on)
ui.text(c, boost_text(q), { font = "mono", px = 11, color = "ready" }, w - 10, cy, gui.PIVOT_E) ui.text(c, boost_text(q), { font = "mono", px = 11, color = "ready" }, w - 10, cy, gui.PIVOT_E)
end end
end end
chart.spark(c, 10, h - 20, w - 20, 13, q.spark, (q.change >= 0) and "up" or "danger") -- (kept while the series is the same: it moves on with the 5-minute tick)
local spark_color = (q.change >= 0) and "up" or "danger"
blocks.keep(self, "mk:spark:" .. q.item, blocks.sig(w, h, q.spark, spark_color), c, function(r)
chart.spark(r, 10, h - 20, w - 20, 13, q.spark, spark_color)
end)
-- Hover (app.css .mk-card:hover): a brighter edge and fill; the selected card keeps its look. -- Hover (app.css .mk-card:hover): a brighter edge and fill; the selected card keeps its look.
local b = H.link(self, c, quote_view(self, q.item)) local b = H.link(self, c, quote_view(self, q.item))
if not on then if not on then
@@ -224,8 +229,14 @@ function M.build(self, root, px, py, w, h, H, opts)
-- Ticker tape across the panel. -- Ticker tape across the panel.
local cy = 14 local cy = 14
cy = cy + ticker.build(self, root, 14, cy, w - 28, mk.quotes, function(item) return color_of(st, item) end, function(item) local tape = {}
for i, q in ipairs(mk.quotes) do
tape[i] = { q.item, q.symbol, q.price, q.up, q.change, q.changeText }
end
cy = cy + blocks.keep(self, "mk:ticker", blocks.sig(cy, w, tape), root, function(r)
return ticker.build(self, r, 14, cy, w - 28, mk.quotes, function(item) return color_of(st, item) end, function(item)
H.navigate(self, quote_view(self, item)) H.navigate(self, quote_view(self, item))
end)
end) + 8 end) + 8
-- Heading, with the holdings stats on its right. -- Heading, with the holdings stats on its right.
@@ -401,9 +412,13 @@ function M.build(self, root, px, py, w, h, H, opts)
end end
ui.rect(d, 14, ohl_y - 8, dw - 28, 1, "line") ui.rect(d, 14, ohl_y - 8, dw - 28, 1, "line")
if mk.chart and mk.chart.item == q.item then if mk.chart and mk.chart.item == q.item then
chart.chart(d, 14, y + 4, dw - 28, ohl_y - 16 - (y + 4), mk.chart, { local events = price_events(st, q.item)
color = up and "up" or "danger", events = price_events(st, q.item), pointers = self.pointers, local ch = ohl_y - 16 - (y + 4)
blocks.keep(self, "mk:chart", blocks.sig(y, dw, ch, up, mk.chart, events), d, function(r)
chart.chart(r, 14, y + 4, dw - 28, ch, mk.chart, {
color = up and "up" or "danger", events = events, pointers = self,
}) })
end)
end end
-- Sell ticket. -- Sell ticket.
@@ -423,7 +438,8 @@ function M.build(self, root, px, py, w, h, H, opts)
-- Payout: what the amount typed would earn, or Sell all with none (display only). -- Payout: what the amount typed would earn, or Sell all with none (display only).
-- After a sale: what it earned, powering down (sale.lua). -- After a sale: what it earned, powering down (sale.lua).
local pay, pay_tick, sale_n local pay, pay_tick, sale_n
local function payout() local payout
payout = function()
if pay_tick then if pay_tick then
pay_tick.dead = true pay_tick.dead = true
pay_tick = nil pay_tick = nil
@@ -476,6 +492,7 @@ function M.build(self, root, px, py, w, h, H, opts)
end end
end end
end end
payout = blocks.bound(payout) -- (made again by the field and the sale: the market's block)
local sell_now -- (the Sell button's click: Enter in the field sells, like the web's form) local sell_now -- (the Sell button's click: Enter in the field sells, like the web's form)
local input = field.new(self, t, "sell", 14, 52, tin, 36, { local input = field.new(self, t, "sell", 14, 52, tin, 36, {
hint = "0", font = "mono", hint_font = "mono", allowed = "[%d%.]", hint = "0", font = "mono", hint_font = "mono", allowed = "[%d%.]",
+7
View File
@@ -9,6 +9,7 @@ local theme = require("modules.theme")
local ui = require("modules.ui") local ui = require("modules.ui")
local tooltip = require("modules.tooltip") local tooltip = require("modules.tooltip")
local test = require("modules.test") local test = require("modules.test")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -57,12 +58,18 @@ function M.build(self, H, root, x, y, w, moon)
end end
if sale.canSell then if sale.canSell then
cards.confirm_server_button(self, b, sale.confirm, "sell-moon", { ["sell-moon"] = moon.id }, "sell-moon") cards.confirm_server_button(self, b, sale.confirm, "sell-moon", { ["sell-moon"] = moon.id }, "sell-moon")
if self.autoplay then
local function reg()
if self.autoplay then if self.autoplay then
self.autoplay:register("sell-moon", { ["sell-moon"] = moon.id }, function() self.autoplay:register("sell-moon", { ["sell-moon"] = moon.id }, function()
H.act(self, "sell-moon", { ["sell-moon"] = moon.id }) H.act(self, "sell-moon", { ["sell-moon"] = moon.id })
end) end)
end end
end end
reg()
blocks.note(reg) -- (a kept block registers again)
end
end
self.druid:new_button(hit, function() self.druid:new_button(hit, function()
self.sale_open[moon.id] = not open self.sale_open[moon.id] = not open
self.dirty = true self.dirty = true
+3 -2
View File
@@ -20,6 +20,7 @@ local ready = require("screens.game.core.ready")
local tabs = require("components.tabs") local tabs = require("components.tabs")
local list_c = require("components.list") local list_c = require("components.list")
local row_c = require("components.row") local row_c = require("components.row")
local blocks = require("modules.blocks")
local M = {} local M = {}
@@ -401,9 +402,9 @@ function M.build(self, H, parent, x, y, w, h)
local rows_y = cy + 34 + 12 local rows_y = cy + 34 + 12
field.new(self, root, "find", pad, cy, inner, 34, { field.new(self, root, "find", pad, cy, inner, 34, {
hint = "Name", px = 14, max_len = 40, esc_clears = true, -- (type=search) hint = "Name", px = 14, max_len = 40, esc_clears = true, -- (type=search)
on_change = function(text) on_change = blocks.bound(function(text) -- (what it makes is the moon list's: modules/blocks)
build_rows(text, rows_y) build_rows(text, rows_y)
end, end),
}) })
cy = rows_y cy = rows_y
end end
+2 -2
View File
@@ -6,6 +6,7 @@
local theme = require("modules.theme") local theme = require("modules.theme")
local ui = require("modules.ui") local ui = require("modules.ui")
local blocks = require("modules.blocks")
local tooltip = require("modules.tooltip") local tooltip = require("modules.tooltip")
local test = require("modules.test") local test = require("modules.test")
local scroll = require("components.scroll") local scroll = require("components.scroll")
@@ -221,8 +222,7 @@ local function recipe_row(self, root, H, st, rf, r, x, y, w)
H.action(self, mx, mn, "refine", { recipe = r.item, all = "1" }) H.action(self, mx, mn, "refine", { recipe = r.item, all = "1" })
tooltip.add(mx, "Refine as many as you can (" .. ui.money(r.maxQty) .. ")") tooltip.add(mx, "Refine as many as you can (" .. ui.money(r.maxQty) .. ")")
-- Enter in the quantity refines (the web's form). -- Enter in the quantity refines (the web's form).
self.inputs = self.inputs or {} blocks.push(self, "inputs", { input = input, submit = refine, step = { min = 1, max = rf.maxBatch } })
self.inputs[#self.inputs + 1] = { input = input, submit = refine, step = { min = 1, max = rf.maxBatch } }
else else
tooltip.add(rb, why) tooltip.add(rb, why)
tooltip.add(mx, why) tooltip.add(mx, why)
+36 -17
View File
@@ -198,6 +198,11 @@ function M:hiding()
return self.r ~= nil return self.r ~= nil
end end
-- The card is flying away (Start mining).
function M:closing()
return self.r ~= nil and self.r.closing ~= nil
end
-- The new moon's entry in the moon list stays hidden too. -- The new moon's entry in the moon list stays hidden too.
function M:hides(id) function M:hides(id)
return self.r ~= nil and self.r.data.moonId == id return self.r ~= nil and self.r.data.moonId == id
@@ -307,6 +312,7 @@ function M:start(data, toast)
-- Reduced motion (reveal.js quick): no charge-up, no particles or shake, the card fades -- Reduced motion (reveal.js quick): no charge-up, no particles or shake, the card fades
-- in complete, silently; the moon only fades out of its shroud (moon.script). -- in complete, silently; the moon only fades out of its shroud (moon.script).
r.reduced = anim.reduced r.reduced = anim.reduced
moon_view.prefetch(data.moonId) -- (its mesh starts now, whether the viewport shows yet or not)
if g.viewport_node then if g.viewport_node then
moon_view.reveal(data.moonId, g.viewport_node) moon_view.reveal(data.moonId, g.viewport_node)
if moon_view.revealing then if moon_view.revealing then
@@ -367,9 +373,18 @@ function M:place()
r.v = v r.v = v
end end
-- Druid components for the layer: once per Druid instance (the game screen's Druid is -- Druid components for the layer: once per Druid instance (the game screen's lives until
-- recreated on every build), the card's once it exists. Registered after the layout's, -- a full rebuild), the card's once it exists. They take the input before the page's
-- so they take the input first. -- (a high input priority: page blocks rebuilt later are newer).
local PRIORITY = 20 -- (Druid's PRIORITY_INPUT_HIGH; the page's are 10)
local function first(c)
if c and c.set_input_priority then
c:set_input_priority(PRIORITY)
end
return c
end
function M:bind() function M:bind()
local r = self.r local r = self.r
if not r then if not r then
@@ -379,38 +394,42 @@ function M:bind()
if self.bound ~= g.druid then if self.bound ~= g.druid then
self.bound, self.card_bound = g.druid, nil self.bound, self.card_bound = g.druid, nil
self.components = {} self.components = {}
local hit = g.druid:new_button(r.hit, function() local hit = first(g.druid:new_button(r.hit, function()
self:advance() self:advance()
end):set_animations_disabled() end):set_animations_disabled())
self.components[#self.components + 1] = hit self.components[#self.components + 1] = hit
g.druid:new_hotkey("key_space", function() self:advance() end) self.components[#self.components + 1] = first(g.druid:new_hotkey("key_space", function() self:advance() end))
g.druid:new_hotkey("key_enter", function() self:advance() end) self.components[#self.components + 1] = first(g.druid:new_hotkey("key_enter", function() self:advance() end))
g.druid:new_hotkey("key_esc", function() self.components[#self.components + 1] = first(g.druid:new_hotkey("key_esc", function()
if self.r and self.r.card then if self.r and self.r.card then
self:close() self:close()
else else
self:advance() self:advance()
end end
end) end))
end end
if r.card and self.card_bound ~= g.druid then if r.card and self.card_bound ~= g.druid then
self.card_bound = g.druid self.card_bound = g.druid
self.components[#self.components + 1] = g.druid:new_button(r.close_btn, function() self.components[#self.components + 1] = first(g.druid:new_button(r.close_btn, function()
if r.done then if r.done then
self:close() self:close()
else else
self:advance() self:advance()
end end
end) end))
if r.another_btn and r.another_ok then
self.components[#self.components + 1] = first(g.druid:new_button(r.another_btn, function()
self:buy_another()
end))
end
end
-- (every build resets these)
if r.card then
if g.autoplay then if g.autoplay then
g.autoplay:register("reveal-close", {}, function() self:close() end) g.autoplay:register("reveal-close", {}, function() self:close() end)
end end
if r.another_btn and r.another_ok then if r.another_btn and not r.another_ok then
self.components[#self.components + 1] = g.druid:new_button(r.another_btn, function() tooltip.add(r.another_btn, r.another_why)
self:buy_another()
end)
elseif r.another_btn then
tooltip.add(r.another_btn, r.another_why) -- (tooltips go with every page build)
end end
end end
end end
+6 -2
View File
@@ -168,16 +168,20 @@ function M.build(self, root, px, py, w, h, H)
ui.text(c, "Tools", "cap", x, y + 6) ui.text(c, "Tools", "cap", x, y + 6)
y = y + 20 y = y + 20
-- (cards are kept while what they show stays the same: modules/blocks)
local function shared()
return { st.credits, st.moonPrice, st.catalogVersion, self.inflight, self.view }
end
y = card.grid(self, c, x, y, inner, sto.tools, function(cd, t) y = card.grid(self, c, x, y, inner, sto.tools, function(cd, t)
tool_card(self, H, st, cd, t) tool_card(self, H, st, cd, t)
end) end, { key = function(t) return "store:tool:" .. tostring(t.storeItemId) end, sig = shared })
if #(sto.attachments or {}) > 0 then if #(sto.attachments or {}) > 0 then
y = y + 8 y = y + 8
ui.text(c, "Attachments", "cap", x, y + 6) ui.text(c, "Attachments", "cap", x, y + 6)
y = y + 20 y = y + 20
y = card.grid(self, c, x, y, inner, sto.attachments, function(cd, a) y = card.grid(self, c, x, y, inner, sto.attachments, function(cd, a)
gear_card(self, H, st, cd, a) gear_card(self, H, st, cd, a)
end) end, { key = function(a) return "store:gear:" .. tostring(a.name) end, sig = shared })
end end
area:finish(y + py) area:finish(y + py)
end end
+1 -1
View File
@@ -1,6 +1,6 @@
-- The Market's ticker tape (the web's .mk-tape): every quote's symbol, price and 24h -- The Market's ticker tape (the web's .mk-tape): every quote's symbol, price and 24h
-- change, scrolling left in a clipped strip; a click picks the quote. The strip is -- change, scrolling left in a clipped strip; a click picks the quote. The strip is
-- rebuilt with every state, so its phase comes from a clock kept on the screen -- rebuilt when its quotes change (a block), so its phase comes from a clock kept on the screen
-- (g.tape_t0): a refresh doesn't jump it back to the start. It pauses under the mouse -- (g.tape_t0): a refresh doesn't jump it back to the start. It pauses under the mouse
-- (app.css .mk-tape:hover): the clock shifts by the time it held. Still with reduced motion. -- (app.css .mk-tape:hover): the clock shifts by the time it held. Still with reduced motion.
+12
View File
@@ -7,6 +7,8 @@
- characters the fonts lack: non-ASCII characters in Lua string literals and in a - characters the fonts lack: non-ASCII characters in Lua string literals and in a
sample of server texts (GET /api/state of a fixture player, every section) must be in sample of server texts (GET /api/state of a fixture player, every section) must be in
the fonts that draw them (text fonts; the symbols ui.rich sends to the sym font) the fonts that draw them (text fonts; the symbols ui.rich sends to the sym font)
- the moon generator (scripts/moon-check.mjs: the mmterrain extension's C, built for the
host, and terrain.lua's settings side, against the server's terrain.js)
- engine warnings in a scenario run's log: font batches / characters / text entries - engine warnings in a scenario run's log: font batches / characters / text entries
over their limits, out of nodes or animations, stencil overflow, invalid markup, over their limits, out of nodes or animations, stencil overflow, invalid markup,
script errors script errors
@@ -226,10 +228,20 @@ def check_scenario(api):
check_log(os.path.join(ROOT, "dist/drive/check/game.log")) check_log(os.path.join(ROOT, "dist/drive/check/game.log"))
def check_moon():
r = subprocess.run(["node", os.path.join(HERE, "moon-check.mjs")], cwd=ROOT, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, text=True)
if r.returncode != 0:
fail("moon-check.mjs failed:\n" + "\n".join(l for l in r.stdout.splitlines() if not l.startswith("ok "))[-2000:])
else:
print("moon: " + (r.stdout.strip().splitlines() or [""])[-1])
def main(): def main():
api = os.environ.get("MM_API", "http://localhost:3000").rstrip("/") api = os.environ.get("MM_API", "http://localhost:3000").rstrip("/")
check_lua() check_lua()
check_flags() check_flags()
check_moon()
check_fonts(api) check_fonts(api)
if "--static" not in sys.argv: if "--static" not in sys.argv:
check_scenario(api) check_scenario(api)
+103 -22
View File
@@ -1,11 +1,16 @@
// Checks the Lua moon generator (modules/moon/terrain.lua) against the server's // Checks the moon generator against the server's terrain.js: the native one
// terrain.js: same random draws, traits and colors, and the same mesh within float32 // (mmterrain/src/terrain.c, built here for the host and loaded into luajit through the
// rounding. Needs node and luajit. // FFI) gives the same mesh within float32 rounding, and the Lua settings side
// (modules/moon/terrain.lua: random draws, traits, resolve, the strip) matches too.
// Every seed runs with the default settings and a few others (each technique, many
// craters, no ridges or patches). Needs node, luajit and a C compiler (cc).
// //
// node scripts/moon-check.mjs [resolution] [seeds...] // node scripts/moon-check.mjs [resolution] [seeds...]
// //
// terrain.js is read from $MM_SERVER (default ../server). // terrain.js is read from $MM_SERVER (default ../server).
import { execFileSync } from "node:child_process"; import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path"; import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url"; import { fileURLToPath, pathToFileURL } from "node:url";
@@ -13,34 +18,112 @@ const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const server = process.env.MM_SERVER || path.join(root, "..", "server"); const server = process.env.MM_SERVER || path.join(root, "..", "server");
const T = await import(pathToFileURL(path.join(server, "web/static/js/terrain.js"))); const T = await import(pathToFileURL(path.join(server, "web/static/js/terrain.js")));
const [res = "32", ...rest] = process.argv.slice(2); const [res = "32", ...rest] = process.argv.slice(2);
const seeds = rest.length ? rest : ["1042", "1187", "1301", "1555", "29dea5d5-709d-4c55-a269-cd90e4379fe9"]; // The fixed seeds, plus moon-id-like ones (deterministic).
const ids = [];
{
let h = 0x9e3779b9;
const next = () => ((h = (Math.imul(h ^ (h >>> 15), 0x2c1b3c6d) + 0x6d2b79f5) >>> 0), h.toString(16).padStart(8, "0"));
for (let i = 0; i < 24; i++) ids.push(`${next()}-${next().slice(0, 4)}-4${next().slice(0, 3)}-a${next().slice(0, 3)}-${next()}${next().slice(0, 4)}`);
}
const seeds = rest.length ? rest : ["1042", "1187", "1301", "1555", "29dea5d5-709d-4c55-a269-cd90e4379fe9", ...ids];
const fixed = (t) => ({ technique: t, variation: { technique: false } });
const SETTINGS = [
{}, fixed("fbm"), fixed("ridged"), fixed("warped"), fixed("billow"), fixed("craterField"),
{ craters: { count: 1500, smoothness: 0, maxRadius: 0.4 } }, { ridge: { strength: 0 }, shading: { patchAmount: 0 } },
];
// Each case: a seed and a settings index (the default settings for every seed, the
// others over a rotating subset).
const cases = [];
seeds.forEach((seed, i) => {
cases.push([seed, 0]);
for (let s = 1; s < SETTINGS.length; s++) if ((i + s) % 4 === 0 || rest.length) cases.push([seed, s]);
});
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "mmterrain-"));
const lib = path.join(dir, "libmmterrain.so");
execFileSync("cc", ["-O2", "-std=c99", "-shared", "-fPIC", "-o", lib, path.join(root, "mmterrain/src/terrain.c"), "-lm"]);
const luaValue = (v) => (typeof v === "object" ? "{" + Object.entries(v).map(([k, x]) => `${k}=${luaValue(x)}`).join(",") + "}" : JSON.stringify(v));
const lua = ` const lua = `
package.path = ${JSON.stringify(root + "/?.lua;")} .. package.path package.path = ${JSON.stringify(root + "/?.lua;")} .. package.path
local T = require("modules.moon.terrain") local T = require("modules.moon.terrain")
local SETTINGS = { ${SETTINGS.map(luaValue).join(", ")} }
local ffi = require("ffi")
ffi.cdef[[
typedef struct mt_params mt_params;
typedef struct mt_gen mt_gen;
int mt_params_from_array(const double* a, int n, mt_params* p);
mt_gen* mt_new(const mt_params* p);
void mt_free(mt_gen* g);
int mt_step(mt_gen* g, double budget);
int mt_count(const mt_gen* g);
int mt_n(const mt_gen* g);
const double* mt_positions(const mt_gen* g);
const double* mt_normals(const mt_gen* g);
const double* mt_colors(const mt_gen* g);
int mt_strip_len(int n);
void mt_strip(int n, int* out);
void mt_fill(const mt_gen* g, float* pos, float* nor, float* col, int stride, int from, int to);
]]
local N = ffi.load(${JSON.stringify(lib)})
local function num(x) return string.format("%.17g", x) end local function num(x) return string.format("%.17g", x) end
local function arr(a) local o = {} for k = 1, #a do o[k] = num(a[k]) end return "[" .. table.concat(o, ",") .. "]" end local function carr(p, n) local o = {} for k = 0, n - 1 do o[k + 1] = num(p[k]) end return "[" .. table.concat(o, ",") .. "]" end
local out = {} local out = {}
for i = 2, #arg do -- arg[1]: resolution, then the seeds for i = 2, #arg, 2 do -- arg[1]: resolution, then seed, settings index pairs
local seed = arg[i] local seed, si = arg[i], tonumber(arg[i + 1])
local s = T.merge_settings({}) local s = T.merge_settings(SETTINGS[si + 1])
local r = T.rng(T.hash_string(seed)) local r = T.rng(T.hash_string(seed))
local draws = {} for k = 1, 16 do draws[k] = num(r()) end local draws = {} for k = 1, 16 do draws[k] = num(r()) end
local tr = T.traits(s, seed) local tr = T.traits(s, seed)
local p = T.resolve(s, seed) local p = T.resolve(s, seed)
p.resolution = tonumber(arg[1]) p.resolution = tonumber(arg[1])
local m = T.generate(p) do
out[#out + 1] = string.format('{"draws":[%s],"traits":[%q,%q,%s,%s,%s,%s],"positions":%s,"normals":%s,"colors":%s}', local a = T.native_params(p)
local buf = ffi.new("double[?]", #a, a)
local params = ffi.new("char[1024]") -- (larger than mt_params)
assert(N.mt_params_from_array(buf, #a, ffi.cast("mt_params*", params)) == 1)
local g = N.mt_new(ffi.cast("mt_params*", params))
local steps = 0
repeat steps = steps + 1 until N.mt_step(g, 0.0005) == 1
local c = N.mt_count(g) * 3
-- The strip: the native order equals the Lua's (terrain.strip), and its fill writes
-- those vertices.
local strip = T.strip(N.mt_n(g))
local len = N.mt_strip_len(N.mt_n(g))
local sidx = ffi.new("int[?]", len)
N.mt_strip(N.mt_n(g), sidx)
local same = len == #strip
for k = 1, #strip do if not same then break end same = sidx[k - 1] == strip[k] end
local fill = ffi.new("float[?]", len * 9)
local half = math.floor(len / 2) -- (in two ranges, like the web's chunks)
N.mt_fill(g, fill, fill + 3, fill + 6, 9, 0, half)
N.mt_fill(g, fill, fill + 3, fill + 6, 9, half, len)
local P = N.mt_positions(g)
local fdiff = 0
for k = 1, #strip do
local v = strip[k]
for d = 0, 2 do fdiff = math.max(fdiff, math.abs(fill[(k - 1) * 9 + d] - P[v * 3 + d])) end
end
out[#out + 1] = string.format('{"draws":[%s],"traits":[%q,%q,%s,%s,%s,%s],"positions":%s,"normals":%s,"colors":%s,"strip":%s,"fill":%s,"steps":%d}',
table.concat(draws, ","), tr.technique, tr.mid, num(tr.shapeStrength), num(tr.ridgeStrength), num(tr.craterCount), table.concat(draws, ","), tr.technique, tr.mid, num(tr.shapeStrength), num(tr.ridgeStrength), num(tr.craterCount),
num(tr.maxRadius), arr(m.positions), arr(m.normals), arr(m.colors)) num(tr.maxRadius), carr(N.mt_positions(g), c), carr(N.mt_normals(g), c), carr(N.mt_colors(g), c),
same and "true" or "false", num(fdiff), steps)
N.mt_free(g)
end
end end
print("[" .. table.concat(out, ",") .. "]") print("[" .. table.concat(out, ",") .. "]")
`; `;
const got = JSON.parse(execFileSync("luajit", ["-", res, ...seeds], { input: lua, maxBuffer: 1 << 30 }).toString()); const got = JSON.parse(execFileSync("luajit", ["-", res, ...cases.flat().map(String)], { input: lua, maxBuffer: 1 << 30 }).toString());
const maxDiff = (a, b) => (a.length !== b.length ? Infinity : a.reduce((d, v, k) => Math.max(d, Math.abs(v - b[k])), 0));
const close = (m, g) => {
const d = { positions: maxDiff(m.positions, g.positions), normals: maxDiff(m.normals, g.normals), colors: maxDiff(m.colors, g.colors) };
return [d.positions < 1e-5 && d.normals < 1e-4 && d.colors < 1e-4, d];
};
const fmt = (d) => `positions ${d.positions.toExponential(1)}, normals ${d.normals.toExponential(1)}, colors ${d.colors.toExponential(1)}`;
let failed = false; let failed = false;
seeds.forEach((seed, i) => { cases.forEach(([seed, si], i) => {
const s = T.mergeSettings({}); const s = T.mergeSettings(SETTINGS[si]);
const r = T.rng(T.hashString(seed)); const r = T.rng(T.hashString(seed));
const draws = Array.from({ length: 16 }, () => r()); const draws = Array.from({ length: 16 }, () => r());
const t = T.moonTraits(s, seed); const t = T.moonTraits(s, seed);
@@ -48,15 +131,13 @@ seeds.forEach((seed, i) => {
const p = T.resolveMoon(s, seed); const p = T.resolveMoon(s, seed);
p.resolution = Number(res); p.resolution = Number(res);
const m = T.generateMoon(p); const m = T.generateMoon(p);
const maxDiff = (a, b) => (a.length !== b.length ? Infinity : a.reduce((d, v, k) => Math.max(d, Math.abs(v - b[k])), 0));
const d = {
positions: maxDiff(m.positions, got[i].positions),
normals: maxDiff(m.normals, got[i].normals),
colors: maxDiff(m.colors, got[i].colors),
};
const exact = JSON.stringify(draws) === JSON.stringify(got[i].draws) && JSON.stringify(traits) === JSON.stringify(got[i].traits); const exact = JSON.stringify(draws) === JSON.stringify(got[i].draws) && JSON.stringify(traits) === JSON.stringify(got[i].traits);
const ok = exact && d.positions < 1e-5 && d.normals < 1e-4 && d.colors < 1e-4; const nat = got[i];
const [natOk, d] = close(m, nat);
const ok = exact && natOk && nat.strip && nat.fill < 1e-5;
const line = `draws and traits ${exact ? "identical" : "DIFFER"} | native: ${fmt(d)}; strip ${nat.strip ? "same" : "DIFFERS"}, fill ${nat.fill.toExponential(1)}, ${nat.steps} steps`;
failed ||= !ok; failed ||= !ok;
console.log(`${ok ? "ok " : "FAIL"} ${seed} ${t.technique}: draws and traits ${exact ? "identical" : "DIFFER"}, max diff positions ${d.positions.toExponential(1)}, normals ${d.normals.toExponential(1)}, colors ${d.colors.toExponential(1)}`); console.log(`${ok ? "ok " : "FAIL"} ${seed} [settings ${si}] ${t.technique}: ${line}`);
}); });
console.log(`${cases.length} cases at resolution ${res}: ${failed ? "FAILED" : "all ok"}`);
process.exit(failed ? 1 : 0); process.exit(failed ? 1 : 0);