Skip to content

Latest commit

 

History

History
167 lines (134 loc) · 9.04 KB

File metadata and controls

167 lines (134 loc) · 9.04 KB

NodeShuffle — Design

Server-side SML mod (mod reference NodeShuffle) for Satisfactory 1.1+ (>=491125), SML 3.12, UE 5.6.1-CSS. It relocates the world's resource nodes to new, map-wide locations per save and shuffles which are active and what resource and purity each carries — for solids, oil/liquids, and modded nodes — with balance minimums that keep every playthrough completable.

Model: Hide & Replace

Every unoccupied original node (vanilla and modded) is hidden whole-actor (which also removes its rock, including instanced meshes) and recorded persistently. Its resource lives on as one of our own relocated nodes, spawned at a new location. Occupied originals — any node with a miner, extractor, or portable miner — are left 100% untouched, at roll time and re-checked continuously, so the player's factory is never disturbed and the save stays safe. Hiding is reversible (nothing is destroyed), so disabling the mod restores the vanilla world.

Relocated nodes spawn as their original class

A relocated node is spawned as its own resource-node class (resolved from the saved NodeClassPath), not a generic stand-in. This is what makes modded nodes behave correctly: a lithium node spawns as its Alkali class, so it keeps its native rules (rejects a normal Miner, accepts its intended extractor). Each spawned node carries a UNodeShuffleNodeComponent — a runtime identity marker that also hosts the fallback rock/oil-decal and a bForceAccept flag. Identity lives in the SaveGame layout (a FGuid per entry), not the actor class, so nodes re-adopt by GUID or by location across reloads.

Visuals

In priority order: an authored table (FNodeShuffleNodeAssets) of the game's own node meshes/materials for vanilla resources; a look captured from the original node for modded resources the table doesn't cover — from its paired mesh actor (e.g. RefinedPower thorium, modded lead) or, for self-rendering node BPs with no engine mesh-actor links, from the node's own/attached mesh components (e.g. FicsitFarming dirt); a quartz placeholder otherwise; and an oil decal for liquids. When a modded-class node is dealt a vanilla resource, the assigned resource's look wins (the native mesh is hidden and our rock is dressed) so it doesn't wear the wrong appearance.

Placement and terrain

New nodes use spawn-on-discovery: they materialize only once a player is within range and the terrain has streamed in, so they always settle correctly on the ground. Settling (RaycastSettle):

  • Slope-fit — the rock takes the full (smoothed) slope so it beds into the hill; the node actor is tilt-clamped (~12°) so the Miner hologram gets near-vanilla geometry and can place. Hills of any steepness are fair game.
  • Cliff avoidance — hits steeper than 60° re-deal to better ground.
  • Water avoidance — a learned land/water map plus a depth guard and the game's water-volume test keep nodes off the seabed and out of lakes; a water-locked entry re-deals randomly within the map-wide deal box.
  • Caves — the shuffle would otherwise empty caverns (it hides their originals). Roofed originals seed a budgeted trace flood-fill that maps cavern floors; nodes are then dealt onto them at their natural share, only where a Miner fits.
  • Overlap/enclosure/machinery guards keep nodes off factories and out of boxed-in rock pockets.

Learned + prebuilt terrain map

The land/water grid (100 m cells) and cave-floor cells (8 m) are learned from every ground probe and persisted globally (NodeShuffle_WaterGrid.json, NodeShuffle_CaveFloors.json — the terrain is the same across saves). A snapshot is also embedded in the mod (generated into NodeShuffleBakedData.h) and merged on load with local knowledge winning, so a fresh install places nodes well from the first launch and the player's own exploration keeps refining it.

Per-save layout

The single source of truth is a UPROPERTY(SaveGame) array of FNodeShuffleEntry on the subsystem (plus seed and layout version). It is rolled once per save from a seeded FRandomStream and thereafter only re-applied idempotently every ~5 s (server-only). The roll preserves balance by dealing resources and purities from the vanilla multiset (so overall balance is retained) and enforces a per-resource active floor (MinNodesPerResource, and a separate floor for modded resources) — the completability guarantee. New SaveGame fields are additive with defaults, so older saves load unchanged.

What the roll covers — measured, 2026-08-08

The initial roll captures the whole map's vanilla node set, not a streamed subset. This was believed the other way round until it was measured, and the belief produced two wrong investigations, so the evidence is recorded here rather than left as an assumption:

  • The roll's pool comes from a live TActorIterator<AFGResourceNodeBase>. That iterator sees only resident actors — but every level-placed resource node is resident from load. All 630 originals resolve and hide inside a single 8–11 ms frame at boot, in three independent boots, including all 8 Uranium and all 23 Bauxite nodes in biomes tens of kilometres apart. No streaming radius contains both.
  • MinVanillaNodesForRoll = 50 is therefore not a streaming gate and never was. It is a sanity floor ("a populated world exists"). Raising it fixes nothing and is explicitly rejected — see docs/TECH-DEBT.md.
  • Corroboration from the game's own design: CSS built cook-time manifests (AFGWorldScannableDataGenerator) for item pickups, drop pods and creature spawners — precisely the things that are streamed — and built none for resource nodes. The Radar Tower takes live AFGResourceNodeBase* pointers and has no registry behind it; it works for the same reason our scan does.

Two things remain player-presence-gated, and both by physics, not by choice: spawning a replacement node and probing a well destination each need a downward trace against resident terrain (RaycastSettle). Neither is affected by how the roll enumerates.

The one genuine discovery gap is other mods' nodes. Nodes SpawnActor'd at runtime by other mods have been measured arriving minutes after boot (28 in one session) and are absent from the first roll's pool. A re-roll re-scans live and enrols them. See docs/TECH-DEBT.md T14.

Diagnostics: every number above is emitted per roll on the ROLLCENSUS: line, including the live-node provenance split (ours / level-placed / runtime-spawned by other mods) and the per-resource active counts, with a ROLLCENSUS ZERO-ACTIVE: warning naming any managed resource that ends a roll with no active entries.

Function hooks (SML)

A small number of engine methods are hooked via SUBSCRIBE_UOBJECT_METHOD:

  • Miner hologram acceptance — force-accept our relocated nodes so Miner buildings snap to them (never fracking or gas nodes, which would crash or must keep native rules).
  • Resource scanner — strip hidden originals from the scan clusters at the source, so an emptied original doesn't leave a phantom ping or map marker.
  • Portable miner dispenser — lift the spawn onto the rock surface the player aimed at instead of the terrain beneath it.

Radiation

Radiation is positional data in AFGRadioactivitySubsystem. Hiding an original removes its emitter (otherwise it would keep irradiating an empty spot), and a relocated/restored node re-registers radiation at its new location — so radiation moves with the shuffle instead of leaving invisible hot zones.

Safety rules

  • A node with a miner/extractor/portable miner is never retyped, moved, purity-changed, or deactivated — enforced at roll time and re-checked each pass.
  • Re-rolling is never implicit: only the edge-triggered RerollNow one-shot toggle triggers it (live mid-session or on next load), and it clears itself. Occupied nodes are carried and pinned through a re-roll.
  • With Enabled=false the subsystem does nothing; hidden originals return and no new nodes spawn.

Console commands

  • NodeShuffle.Here — logs the player's position, the ground slope at their feet (and whether the cliff gate accepts it), and a 300 m census of nearby nodes and originals (state, distance, radiation).
  • NodeShuffle.SeedHere — marks a roofed spot (cave/arch/overhang) as a cave-placement seed.

Access transformers

Config/AccessTransformers.ini friends ANodeShuffleSubsystem (and, for the hooks, FNodeShuffleModule) to the engine members the design needs: AFGResourceNode/AFGResourceNodeBase (overrides, radioactivity, mesh actor, placement flags), AFGResourceNodeManager (registration), AFGResourceScanner (cluster refresh + phantom-ping strip), AFGRadioactivitySubsystem (emitter verification), AFGPortableMinerDispenser, and the Miner extractor hologram.

Config and data files

See the README for the full in-game config table and the optional Configs/*.json data files (generated-node report, custom node list, rock-pattern overrides, and the learned terrain maps).