- C++ 40.6%
- Python 35.3%
- GDScript 21.2%
- GDShader 1%
- CMake 0.7%
- Other 1.2%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
A shader node (Shader Editor > Add, or Strata Material in the Momforge Mesh panel, which assigns it to the selected faces in Edit Mode) naming a game material by its VMT path. The path field searches the install's materials as you type (loose folders and VPKs; model, UI and effect materials left out). Faces of Blocks / Shell meshes using it get that material in the VMF; the viewport shows the game's base texture, world aligned at Hammer's default scale. Its Role (Auto: a tool texture's own, else the object's Faces) is what the playtester and validator use. Core: Face::material overrides the role's material; BrushesParams.paths holds one per piece (a "paths" block in the project, so paths may have spaces). Tests: core (VMF sides, round trip); add-on (export per piece, Auto and set roles, preview with and without the game, material search, missing material, the button) and the node drawn in a Blender window. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
| blender | ||
| capi | ||
| gdext | ||
| godot | ||
| src | ||
| tests | ||
| third_party | ||
| tools | ||
| .gitignore | ||
| .gitmodules | ||
| CMakeLists.txt | ||
| README.md | ||
momforge
A procedural level editor for Momentum Mod bhop and surf maps. You place generators rather than individual brushes: a bhop path, a surf ramp, a painted scatter of platforms. Each one rebuilds its brushes whenever you change a parameter. The editor exports a VMF, which you compile with Strata Hammer and vbsp as usual. You can also play the map inside the editor before compiling it. The playtester runs a port of Momentum's own movement code.
The front end is a Godot 4 app with Blender controls. The geometry,
generators, VMF export and movement simulation are a C++17 library
(momcore), loaded into Godot as a GDExtension. momforge also runs inside
Blender as an add-on: the same generators and checks, F12 compiles the map
from Blender, and P opens the Godot playtester on it (see
Blender add-on).
Run (no compiling)
On Linux x86_64, a prebuilt extension ships in godot/bin/. All you need is Godot ≥ 4.5
from your package manager, on any distro with glibc ≥ 2.35 (Ubuntu 22.04 or newer, any current Arch).
godot --path godot
Build from source (only to change the C++)
git submodule update --init # godot-cpp bindings (pinned to godot-4.5-stable)
cmake -S . -B build && cmake --build build -j # ~25 s: godot-cpp is trimmed to the classes momforge uses
(cd build && ctest) # core tests + the Blender-keymap self-test
tools/build-prebuilt.sh # refresh the shipped .so files (Ubuntu 22.04 container, needs podman)
This does not compile Godot. It compiles momcore and the small godot-cpp binding
layer into godot/bin/libmomforge_gd.so, and Godot loads that through
godot/momforge.gdextension. The build also needs glm. The UI fonts (Barlow Semi
Condensed, IBM Plex Sans/Mono; OFL) ship in godot/fonts/.
Objects (Shift A)
| Object | What it does |
|---|---|
| Bhop path | A spline. Platforms are placed at the distance a real jump covers: speed × air time(Δz), using Momentum's 57-unit jump and 800 gravity. Speed ramps from speed start to speed end. Platform size, shape, yaw and height can be jittered, steps are capped by max rise, and the layout is seeded. Options add a checkpoint every N platforms and a fail trigger underneath. Jump arcs are drawn green, or red when a step is above the jump apex. |
| Surf ramp | A ramp cross-section swept along a spline. The profile can be an A-frame, left-sided or right-sided. You set the angle, height, base, max bend and tilt (roll) per point. The ramp is built like a hand-made Hammer ramp (modelled on surf_utopia): each segment's surf face is one flat polygon, dropped straight down to a flat floor at the ramp's base, so the cross-section is a solid triangle; neighbouring pieces share their joint corners exactly (no steps, gaps or near-miss vertices). Each face continues the previous face's plane; where the designed tilt drifts more than 1° from that, the segment becomes two flat triangle slabs split on the convex diagonal. A new segment starts every 6° of turn by default (surf_utopia bends 6-8° per joint). Ramps export as one func_detail each, so vbsp doesn't chop them (on the surf demo: 0 bad start verts, 16 T-junction edges, against 1,409 and 10,305 as world brushes). test_ramps surfs them through the movement sim at 700-3400 u/s. Clip split exports playerclip collision plus a non-solid func_brush visual. Projects saved before this change move ramps still on the old 2° default to 6° when loaded. Model exports the ramp as a prop_static instead, like surf_surreal's ramps: F12 compiles it with Momentum's studiomdl (render mesh plus one convex collision piece per slab, each shrunk 0.02u so studiomdl keeps them apart) and packs it into the BSP with bspzip. Props get no vbsp brush bevels, a cause of rampbugs at turned joints. The playtester still collides with the brush version. |
| Scatter | A painted set of platforms (Paint Mode). LMB paints, Ctrl LMB erases, and F sets the brush spacing. Each stamp keeps its own seed, so repainting one area doesn't reshuffle the others. |
| Box | Pads, walls and slopes. Shape is Box or Wedge, and rotation is free: R then X or Y tilts it into any slope. The material can be a tool texture (PLAYERCLIP, NODRAW…), and its name is written on the faces in the viewport. |
| Zone / Spawn | Start/End/Stage/Checkpoint/Fail zones, with their role written on their walls; info_player_start plus the teleport destination that fail triggers use. New zones are 256 units tall, the minimum in Momentum's zoning guide. |
| Booster | A trigger_setspeed volume (surf boosters, launch pads, speed caps). On entry it sets the horizontal speed along a direction and/or the vertical speed. Each axis is Set, SetKeepDirection (horizontal: the speed, the way you're already going), IncreaseOnly (only if you are slower), DecreaseOnly (only if faster) or Ignore. A cyan arrow shows the push, and R turns it. The playtester applies it, with a whoosh sound. |
| Empty | A point with a rotation (Blender plain axes). Builds nothing; modifiers can follow it. |
| Entity | Any game entity, as in Hammer (Blender: Add → Momforge → Entity…). See Entities under the Blender add-on. |
Every object has a modifier stack:
- Mirror: across the X, Y or Z plane through a pivot.
- Radial: N copies around a vertical axis.
- Array: N copies with an offset and a yaw step.
Modifiers are non-destructive. Edit the source object and every copy follows. A modifier can target an Empty: the mirror plane goes through the Empty and turns with its yaw, radial copies circle it, and an array repeats the Empty's offset and rotation (a rotated Empty curves the array). The "New Empty" button on a modifier creates one at its pivot.
Controls (Blender keymap, left-click select)
| View | MMB orbit · Shift MMB pan · Ctrl MMB / wheel zoom · Numpad 1 / 3 / 7 front / right / top (Ctrl = opposite) · Numpad 5 persp/ortho · Numpad 4 6 8 2 orbit · Numpad . frame selected · Home frame all · Shift ` walk (WASD, QE, wheel = speed) |
| Select | LMB · Shift LMB extend/toggle · LMB drag box · A all · Alt A none · Ctrl I invert · clicking the same spot again cycles through everything under the cursor (solids before zones) |
| Transform | G move · R rotate (about Z; R X / R Y tilt) · S scale. While transforming: X / Y / Z lock to an axis, Shift X / Y / Z lock to a plane, type a number (e.g. G X 128), Ctrl snaps to the grid, LMB / Enter confirms, RMB / Esc cancels |
| Edit | Shift A add at the 3D cursor · Shift RMB place the 3D cursor · Shift S snap menu · Shift D duplicate · X / Del delete · H hide · Alt H reveal · Shift H hide others · Ctrl Z / Ctrl Shift Z undo / redo |
| Modes | Tab: Edit Mode, for control points. G/R/S work on points, E extrudes, Ctrl RMB adds a point at the mouse, Ctrl T tilts surf ramp points, X deletes. The header switches to Paint Mode for Scatter objects. |
| Measure | Shift Space then M (Blender's tool popup), or the header's Measure button. LMB drag a ruler between two surfaces. It shows the length, horizontal distance, rise and angle, and the horizontal speed a jump across the gap needs (edge to edge, minus the 32-unit hull), or "too high" above the 57-unit apex. Ctrl snaps to the grid, X clears, Esc leaves |
| Display | Z toggles slope shading: green = walkable (normal.z ≥ 0.7), blue = surfable. Alt Z cycles zones solid / wireframe / hidden. N toggles properties, T toggles the outliner |
| Play | P plays from the Spawn, Shift P plays from the current view. In game: WASD (null binds: last pressed of A/D or W/S wins; toggle on the Playtest tab), Space or mouse wheel to jump (hold Space to autohop), Ctrl duck, T restart (Shift T map start), R back to the checkpoint, Esc back to the editor |
| Files | Ctrl S save · Ctrl Shift S save as · Ctrl O open · Ctrl Shift E export VMF |
The HUD shows your strafe keys, sync (air ticks where the turn matches the strafe key), run sync and gain, plus fps. Sounds for jump, land, timer start, checkpoint, finish, personal best and fail are synthesized in code (volume on the Playtest tab).
The playtester uses Source's mouse formula: degrees = counts × sensitivity × m_yaw, with
m_yaw 0.022. It reads raw mouse motion, so your in-game sensitivity carries over (the
default is 4.5). The FOV works like Momentum's fov_desired: horizontal degrees at 4:3,
wider on widescreen. It defaults to 100 and goes from 60 to 130. Movement variables for each
gamemode are also on the Playtest tab.
The Problems panel re-validates on every edit. It flags walkable "surf" faces, unreachable bhop steps, and a missing spawn, start zone or end zone. It also checks the rules in Momentum's zoning guide and review checklist:
- Start and stage zones must be at least 256 units tall.
- No zone may be thinner than 64 units, because fast players can pass through it.
- The spawn must be inside the start zone.
- Stage numbers start at 2 and are unique, with no gaps.
- Zones must not overlap.
- Every bonus track needs a start and an end zone.
- Generated checkpoints should be roughly 10 s apart.
- Boosters must actually do something and stay under sv_maxvelocity.
- The map must fit the BSP v25 limits for brushes, sides, entities and the ±65536 grid.
Click a problem to jump to it.
The Map tab starts with Stats: an estimated completion time per route, plus brush, side and entity counts against those limits. The status bar also shows the estimate (≈). Bhop paths count as perfect hops, so the time is the sum of the generated jumps' air times. Surf ramps count as a frictionless slide from 300 u/s with no strafe gain. Treat both as a yardstick for pacing, not as a world record.
Compile (F12)
momforge builds the BSP with Momentum's own Linux tools from its bin/linux64, so there's no Hammer and no Wine.
-
Set the install. On the Map tab, under Compile, set the install folder. The one that has
bin/linux64andmomentum/is usually~/.local/share/Steam/steamapps/common/Momentum Mod Playtest. Detect finds it in the usual Steam folders, and the setting is remembered. -
Press F12 (or the header's Compile). momforge runs:
- zonemaker, which writes the zones JSON;
- vbsp;
- vvis;
- vrad (Fast or Final quality).
Each tool's output streams into a log window.
-
Leaks. If the map leaks, the compile stops after vbsp and the leak line is drawn in red in the viewport, from the void to the entity that sees it. View → Clear Leak Line removes it.
-
Install. On success the BSP is copied to
momentum/maps/<name>.bspand the zones tomomentum/maps/zones/local/<name>.json. The console commandmap <name>is copied to the clipboard. The map name comes from the project file name: lowercase, with underscores.
Build files go in Godot's user folder (~/.local/share/godot/app_userdata/momforge/compile/<name>/).
zonemaker crashes on exit after writing its JSON; momforge checks the file rather than the exit code.
The same goes for vbsp, which reports success even when the map leaks, so momforge checks for its .lin file.
Blender add-on
The same momforge in Blender (4.2 or newer). The generators run in the same C++ core, which
Blender loads through ctypes, so any Blender with any Python version works. The playtester
stays in the Godot app: Play (P) in Blender opens it on your map, and Esc there brings you
back.
Install (Linux x86_64; a prebuilt blender/momforge/lib/libmomforge_c.so ships):
tools/install-blender-addon.sh # or: tools/install-blender-addon.sh /path/to/blender
The script links blender/momforge into Blender's user add-ons folder, so a git pull updates
it, then enables it. Restart Blender. Then set your Momentum install under Edit → Preferences →
Add-ons → momforge, or with Detect on the Compile panel. The same preferences set the Godot
binary used for playtesting (godot on PATH by default).
Saving: save your work as a normal .blend. To bring in a map from the Godot editor, use File → Import → momforge Project (.mfp) once, then keep working in the .blend. File → Export → .mfp writes a file the Godot editor can open, but you don't need it to playtest: Play writes its own copy.
How it maps onto Blender:
| momforge | In Blender |
|---|---|
| Bhop path, surf ramp | A curve. Its points are the control points, so Tab, E, Ctrl RMB and X edit them. Tilt (Ctrl T) is the ramp's roll. Right-click → Subdivide Path (keep curve) inserts points on the curve. In Edit Mode the add-on draws the points large and on top (Path point size). |
| Box | A mesh cube shown as wire. Its bounds are the box. G / R / S move, turn and size it; Tab moves its faces. |
| Zone, booster | The same, with rotation locked so they stay axis-aligned. |
| Spawn, Empty | An arrows empty. Rotate Z sets the facing (yaw). |
| Scatter | A mesh with one vertex per stamp. Paint Scatter paints with LMB and erases with Ctrl LMB. Tab moves stamps as vertices. |
| Mirror, Array, Radial | Blender's own Mirror and Array modifiers (Modifiers tab), exported with Blender's meaning. Radial is Blender's radial array: an Array whose Object Offset is a turned Empty, which Add Radial Array sets up. See the notes below. |
| Generator parameters | Properties editor → Object tab → Momforge. |
| Map settings, compile, problems, stats, display | Properties editor → Scene tab → Momforge. |
| Generated brushes | Child meshes (<name>.solid, .zone). They rebuild as you edit, and clicking one selects its momforge object. Zones draw translucent and click through. |
| "Hidden: not exported" | Disable in Renders (camera icon), or Leave out of the map on the panel. H only hides in the viewport. |
| Undo, outliner, snapping, pies, box select, navigation | Blender's own. |
New panels go to the bottom of a Properties tab; drag the Momforge panel's ⋮⋮ handle to the top once and Blender keeps it there.
Modifiers:
- Mirror: on X, Y and/or Z, through the object's origin or the Mirror Object.
- Array:
- Count or Fit Length;
- relative offset (the object's size), constant offset, and Object Offset.
- Brushes only turn about Z, so a tilted mirror plane or array step uses just its Z turn.
- Anything a modifier does that can't export is listed in the Modifiers tab's Momforge Export panel and in Problems:
- Bevel, Subdivision and other modifier types;
- Bisect, caps, Fit Curve.
- Projects from the Godot editor convert on import: its Mirror, Radial and Array (including Arrays that follow an Empty) become these modifiers, and
test_addonchecks the map comes out the same. - Blend files from the add-on's first version convert when opened.
Playtest:
- P (Object Mode), or the Play button in the viewport header, starts the Godot playtester at the Spawn.
- Shift P (the camera button) starts where the viewport camera is, looking the same way.
- Esc in the playtester closes it and you're back in Blender.
F12 over the 3D viewport runs the same steps as the Godot editor:
- studiomdl for model ramps;
- zonemaker;
- vbsp, vvis and vrad;
- bspzip;
- installs the BSP and zones and copies
map <name>to the clipboard.
The log streams into the panel; Compile Log opens it in full. A leak stops the compile and adds a red momforge leak curve, framed in the view. While a compile runs, Shift Esc cancels it. F12 still renders everywhere except over a viewport, and the add-on preferences can turn the binding off. The map name comes from the panel's Map name field, or else from the .blend file name. Momentum picks the gamemode from its prefix (bhop_ / surf_), and the panel warns when the prefix doesn't match. Builds run in ~/.cache/momforge/compile/<name>/.
Files:
- File → Import / Export → momforge Project (.mfp) reads and writes the Godot editor's project files.
- File → Export → Valve Map (.vmf) writes a VMF.
- Saving the bhop or surf preset from Blender gives a VMF byte-identical to the core's (
test_addonchecks this).
Brushes from meshes (enclosed geometry): an ordinary mesh goes into the map as a Model by default. Object tab → Momforge Mesh → Export as makes it map brushes instead, which seal the map like Hammer brushes do. This is the Quake-in-Blender workflow, without its cleanup steps:
- Blocks: each loose part of the mesh is one brush. No Separate by Loose Parts and no Convex Hull script: momforge takes each part's hull, and the Problems list names parts that aren't convex (split them, or use Shell).
- Shell: each face becomes a wall slab Thickness deep behind it. Model a room (or rooms and corridors) as one closed mesh with normals pointing out, and you get sealed walls, floor and ceiling. Slabs are mitred where faces meet, so neighbours share their side exactly and leave no gap; faces that aren't flat or convex are split into triangles. Walls: Inside grows them against the normals instead.
- A face's role (its material path in the map) comes from its Blender material when that's named
after a role (
floor,wall,skybox,nodraw,playerclip, …, with or withoutmomforge), else the object's Faces. Askyboxceiling makes an outdoor room. - Strata Source Material node (Shader Editor → Add, or Strata Material in the Momforge Mesh
panel, which assigns it to the selected faces in Edit Mode): any game material, by its VMT path
(
concrete/concretefloor001a). The field searches the install's materials as you type. The faces get that material in the map, world-aligned like Hammer's default, and the viewport shows the game's texture the same way. Its Role (Auto: a tool texture's own, else Faces) is what the playtester and the Problems list treat them as. A Blocks part takes the material covering most of it. - Detail exports them as one
func_detail: it doesn't seal or cut visibility (pillars, trim). - Vertices within 0.001 of a whole unit are snapped to it, and modifiers (Mirror, Array, Solidify…) are applied.
- To let vbsp check the seal, turn Sky shell off (Scene tab). A leak shows as a red line in the viewport, as before.
test_addoncompiles an L-shaped Shell room through the real vbsp: sealed, and leaking once a wall is removed.
Entities (Add → Momforge → Entity…): any entity the game knows, as in Hammer.
- The class list, keyvalues, choices, spawnflags, inputs and outputs come from the game's
momentum/momentum.fgd, so set the Momentum install in the add-on preferences. Without it you can still type any classname and add keys by hand. - A point entity (
light,info_target,logic_relay, …) is an Empty: its location and rotation are the entity'soriginandangles(rotate X = roll, Y = pitch, Z = yaw). - A brush entity (
trigger_*,func_*) is a wire box like a Zone, sized with S and kept axis-aligned. Faces picks its role: tool trigger for triggers, a wall texture forfunc_detail,func_walland the like. - The Object tab lists the class's keyvalues. Keys changed from the class default always show, and All shows every key. Choices are dropdowns, booleans toggles, spawnflags checkboxes; Add Key adds one the FGD doesn't list.
- Outputs: When (the class's outputs) → Target (a named entity, or
!activator,!player, …) → Input (the target's inputs), with a parameter, delay and how many times. They go into the map'sconnections. The Problems list warns about outputs whose target nothing is named. - Change Class (the search button by the class) keeps the keys both classes share, and turns a point entity into a box or back when needed.
- In the playtester, solid brush entities (
func_detail,func_wall,func_brush, doors, …) collide, standing still. No entity logic runs there (triggers, outputs, lights): only in game.
Share (Scene tab → Momforge → Share): uploads the map for playtesting and copies the link
friends open in their browser: https://apps.wizardhat.info/momforge/?map=<id>.
- Sharing again adds a version under the same link; every version stays playable (
&v=<n>), and the web playtester's menu lists them with their notes. Share as a New Map starts a new link. - The map's id is kept in the
.blend. - No Momentum or Valve textures are uploaded: the bundle is the
.mfp, with material and sky names replaced by neutral ones, plus the models as.glbwithout their images. - The upload token goes in the add-on preferences, or the add-on reads the one
~/.config/wizardhat-apps/config.jsonnames. The contract isdocs/SHARES.mdin wizardhat-apps.
Checkpoints (Scene tab → Momforge → Checkpoints):
- Every checkpoint in the map is listed in the order the run passes it, with the number it gets in the map. That includes the ones bhop paths make and checkpoint zones you place yourself.
- Each checkpoint's number floats above it in the viewport (Checkpoint numbers, on this panel and on Display, turns them off). The one selected in the list is drawn in the active colour.
- Click one to select its object and fly the view to it; ▲ / ▼ move it earlier or later; Reset Order goes back to the course order.
- momforge numbers them itself, 2, 3, 4… per track, so there are no gaps or repeats. Before, each bhop path counted from 2, and a hand-placed checkpoint kept whatever number it was given. For every missing number zonemaker writes an empty checkpoint, which breaks checkpoints in game.
Models: every ordinary mesh in the scene goes into the map as a model (prop_static):
- any mesh you model or import, that isn't a momforge object and isn't Disable in Renders;
- F12 writes its studiomdl sources (SMD, QC) and its materials, converting the image each
material shows (or its base colour) to a VTF behind a
VertexLitGenericVMT; - studiomdl compiles it, and bspzip packs the model and its textures into the BSP, so the map works for players who don't have the model;
- Solid (Object tab → Momforge Model): Hull collides with the mesh's convex hull (the playtester uses up to 26 of its extreme points); None is decoration you pass through;
- the playtester draws the real mesh (Play exports each one as a
.glb); - New Bhop / New Surf remove Blender's untouched startup cube, which would otherwise become a model.
The pack list used to be written before studiomdl ran, so model ramps were never actually packed into the BSP, only their materials. It is written just before bspzip now.
In-game look (Scene tab → Momforge → Display → In-Game Look): renders (Cycles, F12 outside the viewport, or the viewport's Rendered mode) are set up to look like the game:
- The game's own textures on every brush, read from your Momentum install (VTF and VPK files,
Strata's compressed VTFs included) and placed with the same texture axes the VMF writes. They're
cached in
~/.cache/momforge/textures. - The
light_environmentthe export writes: the sun's direction, colour and brightness, and its ambient colour lighting everything the sun doesn't. - The map's skybox as the background, the way the game projects it.
- Tool textures (nodraw, clip, trigger, skybox) and trigger volumes render as nothing.
- Camera at Spawn: the player's eye (64 units up) facing the Spawn's way, with the vertical field
of view Momentum's default
fov_desired90 gives. - The Standard view transform: colours as the game shows them, with no film curve.
It's an approximation. Cycles path-traces where vrad bakes lightmaps, and there's no bloom, HDR or fog. Solid mode keeps momforge's flat colours for editing.
Play gives the Godot playtester the same look: the same textures, sky, sun and ambient, measured to match the Cycles render. The game's light colours are gamma-space numbers and both convert them to linear light. The playtester lights the look in one pass of its own, because Godot's Compatibility renderer adds its ambient and light passes after gamma encoding, which comes out brighter. Its view reaches 160,000 units, across any map.
Hammer units are Blender units. Set Up Viewport (run by New and Import) sets the clipping to 262144, the grid to 16 and the shading to Attribute colours.
Tests:
blender/tests/run.sh /path/to/blender test_addon.py # headless: round trips, modifiers, editing, files, problems
blender/tests/run.sh /path/to/blender test_ui.py # a real window under Xvfb: F12, P / Esc, clicks, G X 256 + undo, measure, paint
ctest runs both when blender is on PATH (or -DBLENDER_BIN=). With MOMFORGE_STRATA_BIN set, both also compile maps with Momentum's tools:
- a leaking map;
- a sealed one;
- one with a model ramp.
test_addon.py also runs on the bpy module from PyPI: python blender/tests/test_addon.py.
Not ported:
- face labels (tool texture names written on faces);
- click-to-cycle through stacked objects.
Blender's Alt Z (X-ray), Z (shading pie) and Shift Space (tools) keep their Blender meaning. Zone display and shading are on the Scene tab's Display panel instead.
Web playtester
The Godot playtester also runs in a browser at https://apps.wizardhat.info/momforge/. It plays
shared maps (?map=<id>&v=<n>, see Share above) and its menu switches between their versions.
Without a share link (or with a broken one) it offers momforge's bhop and surf sample maps;
Esc (which first releases the mouse) brings the menu back. On the web it shows momforge's own look:
role colours with a 16 / 64 / 256-unit grid on the map's texture axes, the light_environment's
sun and ambient, a procedural sky, no game textures, and tool faces hidden.
Build and test it:
EMSDK=~/emsdk tools/build-web.sh # the GDExtension for the web: Emscripten 4.0.20 (what Godot 4.7.2's templates use), no threads
godot --headless --path godot --export-release Web ../web-dist/index.html # needs Godot's export templates
tools/web-dev-server.py --dist web-dist --apps-root /tmp/mf-apps # a local stand-in for the site
tools/test-web.py --apps ~/wizardhatprojects/wizardhat-apps --bpy-python PY --pw-python PY
- No threads: the host can't send the COOP/COEP headers threads need.
- WebGL: has 16 texture units, so the web look uses a shader without samplers.
- Browser mouse lock: the first mouse movement after locking jumps (Chrome), so the first quarter-second of motion is ignored.
- Testing:
test-web.pyshares a map twice from Blender to a localhook.py serve(never the live server: shares are permanent), then checks the links, the version menu, playing and a bad link in headless Chromium. It also checks that no game texture is in the uploads or the app.
Demos (recording a bug)
Every playtest is recorded tick by tick: your inputs, your position and velocity, and a copy
of the map and movement settings. Leaving the playtest (Esc) saves the recording to
demos/<map>_<time>.mfdemo; F9 saves it mid-run. Commit and push a demo to share a bug.
- File → Play Demo… replays it in first person (Space pause, ←/→ seek, Esc back).
- File → Analyze Demo… and
momforge-cli demo <file>re-simulate it exactly. They list every airborne tick where a collision took speed: time, position, the plane hit, the object, and whether the loss went beyond a clean clip against the surface (rampbug-like).
CLI
build/momforge-cli new bhop|surf demo.mfp # starter project
build/momforge-cli export demo.mfp demo.vmf # VMF + problem list (exit 1 on errors)
build/momforge-cli check demo.mfp
build/momforge-cli stats demo.mfp # estimated time per route, counts vs BSP limits
build/momforge-cli demo demos/run.mfdemo # replay + speed-loss report
Export notes and what is not verified yet
-
Plane winding. Planes are written clockwise as seen from outside. That way vbsp's
PlaneFromPoints, which computes(p0-p1)×(p2-p1), gets the outward normal. This was checked against the SDK source, andtest_genre-reads every exported solid the same way. -
Zones. By default zones export as
zone_timer_start/_end/_stage/_checkpointbrush entities with the keys Momentum'szonemakerreads:- every zone has
track_number; - start and stage zones have
safe_height(per zone, default 64) andrestart_destination; - stage zones have
stage_number; - checkpoints have
stage_numberandcheckpoint_number, counting from 2 (the start zone is checkpoint 1).
These were verified against the shipped zonemaker: both demo maps produce a zones JSON, and the optional
strata_zonemakertest checks that whenMOMFORGE_STRATA_BINpoints at the game'sbin/linux64. The Map tab can switch to the legacytrigger_momentum_timer_*names (0.8 keys). Every face is written with Strata'spoint_datavertex list, which zonemaker needs to find a zone's top and bottom. - every zone has
-
Boosters. These export as
trigger_setspeedwith the keys in the game's ownmomentum/momentum.fgd:HorizontalSpeedMode,HorizontalSpeed,HorizontalSpeedAngle,VerticalSpeedMode,VerticalSpeedandStrictMode0. The entity docs list older keys (HorizontalSpeedAmount,Direction, modes 0–3) that the current game ignores; momforge followed those before, so boosters did nothing in game.- Modes follow the FGD. Horizontal: 0 = speed and direction (momforge's Set), 1 = speed, keeping your direction (SetKeepDirection), 2 = increase only, 3 = decrease only, 4 = ignore. Vertical: the same without 0.
- In the playtester, every horizontal mode except Set keeps the direction you're moving in, as the FGD describes. That part is untested in game.
-
Teleports back to your last checkpoint (the game's progress system, from its
momentum.fgd):- Every start zone, checkpoint and stage zone gets a
trigger_momentum_progressin the same volume. Touching it makes that zone's restart point the place progress teleports send you. progress_number: the start zone is 0 (it always counts, so a restart sends you back to the spawn), checkpoints use their checkpoint number, stages 1000 × their stage number. The game ignores a lower number than the last one you touched, so going back through an earlier checkpoint doesn't make it your return point.- Fail floors and Fail zones are
trigger_momentum_teleportwithProgressTeleport1, so one fail floor under the whole map works. FuncBhop platforms (func_bhop) and Multihop platforms (trigger_momentum_bhop,TimeLimit0.5 as in the playtester) useProgressTeleport1 too. - All of them reset your speed (
VelocityMode1 /ResetVelocityOnTeleport1); the game would otherwise keep it through a teleport. - The playtester follows the same rules.
- Legacy (0.8) zone export keeps plain
trigger_teleports to fixed destinations.
- Every start zone, checkpoint and stage zone gets a
-
Materials. These default to the HL2 dev textures and the
tools/*set. They are editable on the Map tab. -
Compiling. Strata's Linux
vbsp(from Momentum'sbin/linux64) compiles the exported surf demo to a BSP v25 with no leak. That run used a stand-in game folder, so materials were not resolved; vvis, vrad and an in-game test are still to do. Not yet opened in Strata Hammer. -
Tickrate. Surf defaults to 66 tick (current docs) and bhop to 100.
Layout
src/core geometry (convex brushes as planes, exact hulls), KeyValues with type hints, scene types
src/gen generators (bhop / surf / scatter / zones / boosters / modifiers), presets, edit ops, jump check
src/io VMF export, project files (.mfp), generic field access, validation, map stats, display meshes
src/sim player movement: port of Momentum's CMomentumGameMovement (Source 1 SDK License,
see src/sim/LICENSE-SOURCE-SDK) + box-sweep collision with bevels
src/play playtest rules: zones, timer, checkpoints, fail teleports, mouse look
src/cli headless tool
gdext/ GDExtension facade (MomForge class): all Source<->Godot axis conversion lives here
capi/ plain C interface over momcore (project text in, JSON / arrays out) for the Blender add-on
blender/ Blender add-on (Python, loads capi's libmomforge_c.so with ctypes) and its tests
godot/ Godot project: theme tokens, Blender navigation + modal transforms, inspector, HUD
tests/ ctest suites; test_course jumps every generated gap in the movement sim;
godot/scripts/selftest.gd drives the Blender keymap through Godot's input pipeline
Headless screenshots: godot --path godot -- --preset surf --slope --shot out.png --frames 10
(add --play --hold fwd,jump to capture the playtest).