How this was built
Not from the community type definitions, and not from memory. The game ships dist/js/external-mod-runtime.js and external-mod-worker-runtime.js — the bridges that every mod call actually passes through. Those files are the API.
- Extract both runtimes from
app.asarand parse each namespace group out of the minified source, resolving variable references to their nearest preceding definition. - Record, for every method, its argument count and the engine function it delegates to — the third column in the reference tables below.
- Diff the result against a
sandkit.d.ts, in both directions.
sandkit.d.ts is accurate. 281 main methods and 88 worker methods line up, with no phantom entries in either direction. The one omission is constants (constants.physics → normal, skip, aggressiveSkip), which the type file does not declare.The whole pipeline is three commands against your own install, so this page can be regenerated for any build rather than trusted as a snapshot.
Anatomy of a mod
Four surfaces, declared in modinfo.json. Most mods use one; the ones that feel like new game content use three.
entry main.js
Runs on the render thread. Registration, UI, energy, tech, settings, persistence.
Compiled with new Function("__sandkit", …) inside an async IIFE: top-level await works, import and export throw and the entry silently never runs.
workerEntry worker.js
Runs on every simulation thread. The only place that sees individual particles move.
Same wrapper, much smaller API: no settings, no timers, no real UI.
patches.json string patching
Up to 256 patches per mod against any .js under the game's js/ — including bundle.js and simulation-worker.js.
expectedMatches is mandatory, and patches can be grouped so the group rolls back atomically if one member misses.
manifest
dependencies is resolved with a real topological sort with cycle detection; loadOrder breaks ties.
That makes a library mod a supported pattern: publish it, have other mods name it, and load order is guaranteed.
Main vs worker
The single most important distinction in this API, and the one that decides where your code has to live.
| Main entry | Worker entry | |
|---|---|---|
| Sees an element move | no | yes |
| Reads mod settings | yes | no |
| Draws UI | yes | toasts only |
| Registers structures, tech, energy | yes | no |
| Repeating timers | triggers | no |
| Talks to the other side | shared buffers | main.emitEvent |
main.emitEvent, and custom payloads pass through unmapped. The other direction has exactly one channel: a SharedArrayBuffer. The main entry calls shared.buffers.create at top level, the worker calls require on the same key — keys are namespaced per mod on both sides, and main entries always load first. That byte is how a worker learns whether your mod is switched on.What can I build?
Read this column first. It is the question people actually arrive with.
| I want to… | Verdict | How |
|---|---|---|
| Add a new machine | supported | structures.register plus sprites.loadFromMod. Render size is in pixels at 4 px per cell, so a 16×16 sprite is a 4×4-cell machine. Passing plain name and description strings auto-registers the translation keys. |
| Make it generate or use power | supported | energy.registerType(id, "conductor" | "storage"), then addAtCell and consume. Conductors produce into the grid and store nothing themselves. |
| React to a single particle | supported | Worker only. events.on("element:moved", …) with a guard. |
| Add a new element or terrain | supported | elements.register and terrains.register. Both hand out numeric type ids as max + 1, which is the drift trap below. |
| Add a tech-tree node | supported | tech.registerNode(id, def, { parentId }). Auto-places in the first free slot under the parent; throws on a duplicate id, a missing parent, or a full row. |
| Add recipes to existing machines | supported | structures.recipes.register covers the planter box, shaker, kinetic press, condenser, steam dryer, synthesizer, snowmaker and smelter. |
| Configure a machine as it is placed | supported | structures.registerPlacementConfig. Values land in structure.data; labels need translation keys for now. |
| Open a panel on a machine already standing | no API | The vanilla Filter and Sound Box panels are opened by hardcoded structure-id checks hanging off the building session, and no event carries “the player interacted with this structure”. Reachable only by patching. |
| Draw your own HUD | with care | ui.overlays.register or ui.inject. The global slot draws over the main menu too — gate on scene.getActive(). |
| Persist data in the save | supported | storage.get and storage.set, keyed by mod id. storage.local is machine-local instead. |
| Veto or rewrite an engine call | supported | hooks.intercept to cancel, hooks.modify to rewrite arguments. Available on both threads. |
| Read or write files, open sockets | no API | Nothing in the surface touches Node. This is the line a third-party loader crosses and a Workshop mod does not. |
| Run code before the game boots | no API | Entries run during world initialisation, once per process. |
Hard limits
Verified in the shipped code, not inferred from silence. These are the ones that change a design.
Entries run once per process
The runtime keeps a set of loaded ids and reports alreadyLoaded on a second world load. Anything skipped because a setting was off stays skipped until the game restarts — so gate behaviour, never registration.
Registration is one-way
energy.registerType, structures.register, addProcessor and event subscriptions cannot be undone at runtime, and addProcessor throws if the structure already has periodic processing.
Uninstalling strands buildings
Mod structures are saved under their string id. Remove the mod and the save keeps entries whose type nothing recognises, which fall back to a plain 4×4 block.
Element type ids drift
Element and matter numbers are handed out as max + 1 at registration, and nothing lets a mod pin its own. Add or remove a type and every number after it shifts while the save still holds the old ones. Structures escape this: their ids are strings, and the numeric block grid is rebuilt from the structure list on load.
Some worker events demand a guard
element:moved and terrain:update throw without one, and the guard must carry exactly one of elementType or terrainType. The simulation only emits these at all when a handler for that type exists.
The engine escape hatch is unversioned
sandkit.engine.api exposes the internal engine and is explicitly not covered by apiVersion — it can change in any patch.
Events & hooks
Ids found in the shipped bundle (45 in total). Payloads for a handful are remapped into a frozen shape for mods; anything the game does not recognise passes through untouched, which is what makes worker → main custom events work.
Lifecycle & world
Building & structures
Simulation (worker)
Player, input & projectiles
Resources & rendering
Outlined: a guard is required, or the subscription throws.
Hooks
hooks.intercept cancels an engine call before it happens; hooks.modify rewrites its arguments. The ids are a whitelist kept as a literal in the bundle — these are all of them, not a sample. Some ids appear in the event list too: the event fires after the fact and can only watch, the hook runs first and gets a vote.
Interceptors — cancel the call (18)
Modifiers — rewrite the arguments (15)
Outlined ids accept a guard option, so a handler can subscribe to only its own structure, weapon or projectile types instead of filtering every call by hand:
| hook | guard option | matched against |
|---|---|---|
building:place | structureTypes | structureId |
weapon:reload:prepare | weaponIds | weaponId |
projectile:travel:prepare | projectileTypes | projectileType |
projectile:impact:prepare | projectileTypes | projectileType |
trigger:schedule:prepare | triggerIds | triggerId |
resource:collection:prepare | resourceIds | resourceId |
resource:delivery:prepare | resourceIds | resourceId |
input:keydown beats a DOM listener — it sits in the game's own input chain, in order, with a cancel() the game respects. building:place can veto a placement a mod considers invalid, which no event can.Patching the game
The part most people miss: a plain Workshop mod can already rewrite the game bundle. No loader, no installer, no touching app.asar.
Scope
Any .js under the game's js/. Absolute paths, .. segments and drive letters are rejected.
Discipline
expectedMatches must be a positive integer and the patch is refused if the real count differs — so a game update breaks your mod loudly.
Atomicity
Patches sharing an atomicGroup stage together. One miss rolls the whole group back and marks the rest skipped.
applied, actualMatches and a reason such as atomic_group_rolled_back — and shows the player none of it. That gap is a mod waiting to be written.Main API 281 methods
Sorted by size. Open a namespace for its methods, argument count, and the engine function behind each one.
structuresRegister machines, patch existing ones, query and mutate what is built.31
| method | args | engine call |
|---|---|---|
recipes.register | — | — |
processing.register | 2 | structures.processing.register |
processing.isEnabledAt | 2 | structures.processing.isEnabledAt |
processing.setEnabledAt | 3 | structures.processing.setEnabledAt |
addProcessor | 2 | structures.addProcessor |
register | 2 | structures.register |
updateDefinition | 3 | structures.updateDefinition |
addVariant | 3 | structures.addVariant |
forEachOfType | 2 | structures.forEachOfType |
registerPlacementConfig | 1 | structures.registerPlacementConfig |
getAtCell | 2 | structures.getAtCell |
getDefinitionByType | 1 | structures.getConfig |
getUnlockedTypes | 0 | structures.getUnlockedTypes |
getTypeFromId | 1 | structures.resolveTypeName |
hasBuiltAtCell | 2 | structures.hasBuiltAtCell |
isBlockedByPlayerAtCell | 2 | structures.isBlockedByPlayer |
isLauncherAtCell | 2 | structures.isLauncherAt |
isType | 2 | structures.isType |
isTypeAtCell | 3 | structures.isTypeAt |
isUnlockedByType | 1 | structures.isUnlocked |
mapValueToSpritesheetIndex | — | structures.mapValueToSpritesheetIndex |
setSpritesheetIndex | 2 | structures.setSpritesheetIndex |
setSpritesheetIndexAtCell | 3 | structures.setSpritesheetIndexAt |
setSpritesheetIndexByValue | 3 | structures.setSpritesheetIndexByValue |
setSpritesheetIndexByValueAtCell | 4 | structures.setSpritesheetIndexByValueAt |
update | 2 | structures.update |
setData | 3 | structures.setData |
buildAtCellWhenIdle | 4 | world.runWhenSimulationIdle, structures.build |
removeAtCellWhenIdle | 3 | world.runWhenSimulationIdle, structures.removeAt |
removeBetweenCellsWhenIdle | 5 | world.runWhenSimulationIdle, structures.removeBetween |
removeAtCellsWhenIdle | 2 | world.runWhenSimulationIdle, structures.removeAtPositions |
elementsRegister sand, liquid and gas types; read and write individual cells.29
| method | args | engine call |
|---|---|---|
getRegisteredTypes | 0 | elements.getRegisteredTypes |
register | 1 | elements.register |
updateDefinition | 2 | elements.updateDefinition |
addInteractionInfo | 2 | elements.addInteractionInfo |
getTypeFromId | 1 | elements.getElementTypeFromId |
getNameByType | 1 | elements.getName |
getDefinitionByType | 1 | elements.getConfig |
getTypeAtCell | 2 | elements.getTypeAtCell |
getResolvedTypeAtCell | 2 | elements.getResolvedTypeAtCell |
getResolvedTypeFromCellId | 1 | elements.getResolvedTypeFromCellId |
getInfoAtCell | 2 | elements.getInfoAtCell |
getMatterTypeAtCell | 2 | elements.getMatterTypeAtCell |
isTypeAtCell | 3 | elements.isTypeAt |
isFreeFallingAtCell | 2 | elements.getResolvedTypeAtCell, elements.isFreeFalling |
findFreeCellInStructure | 3 | elements.findFreePositionInStructure |
createAtCellWhenIdle | 4 | world.isCellEmpty, elements.createAt |
replaceAtCellWhenIdle | 4 | world.getCellId, world.mutateCellWhenIdle, elements.replaceAt |
removeAtCellWhenIdle | 3 | elements.getResolvedTypeAtCell, elements.removeAt |
teleportBetweenCellsWhenIdle | 4 | world.getCellId, world.runWhenSimulationIdle, elements.moveAtSimulationIdle |
getVelocityAtCell | 2 | elements.getResolvedTypeAtCell, elements.getVelocity |
setVelocityAtCellWhenIdle | 3 | elements.setVelocity |
addParticleVelocityAtCellWhenIdle | 4 | elements.addParticleVelocity |
convertToParticleAtCellWhenIdle | 3 | elements.convertToParticle |
convertFromParticleAtCellWhenIdle | 2 | elements.convertFromParticle |
getDataFieldAtCell | 3 | elements.getResolvedTypeAtCell, elements.getDataField |
setDataFieldAtCellWhenIdle | 4 | elements.getResolvedTypeAtCell, elements.setDataField |
refreshColorAtCellWhenIdle | 2 | elements.refreshColorAt |
setPhysicsAtCellWhenIdle | 3 | elements.getResolvedTypeAtCell, elements.setPhysics, world.reportActivityToChunk |
setDurationAtCellWhenIdle | 4 | elements.getResolvedTypeAtCell, elements.setDuration |
i18nTranslation keys, locale switching, number formatting.17
| method | args | engine call |
|---|---|---|
t | — | i18n.t |
register | — | i18n.register |
getLocale | — | i18n.getLocale |
hasTranslation | — | i18n.hasTranslation |
setLocale | — | i18n.setLocale |
getLanguages | — | i18n.getLanguages |
getAvailableLocales | — | i18n.getAvailableLocales |
formatNumber | — | i18n.formatNumber |
key | — | i18n.key |
getName | — | i18n.getName |
getDescription | — | i18n.getDescription |
translatable | — | i18n.translatable |
setGlobal | — | i18n.setGlobal |
getGlobal | — | i18n.getGlobal |
clearGlobal | — | i18n.clearGlobal |
getGlobals | — | i18n.getGlobals |
formatKeyForDisplay | — | i18n.formatKeyForDisplay |
uiReact injection, overlay slots, toasts, dialogs, controller focus.14
| method | args | engine call |
|---|---|---|
update | 2 | ui.update |
openPauseMenu | 0 | ui.openPauseMenu |
showTooltip | 1 | ui.showTooltip |
toast | 2 | ui.toast |
alert | 2 | ui.alert |
confirm | 2 | ui.confirm |
prompt | 5 | ui.prompt |
overlays.register | 3 | ui.overlays.register |
overlays.unregister | 2 | ui.overlays.unregister |
overlays.update | 1 | ui.overlays.update |
inject | 2 | ui.overlays.register, ui.overlays.unregister |
navigation.useFocusable | — | — |
navigation.useFocusScope | — | — |
navigation.controllerFocusClass | — | — |
terrainsRegister terrain; read and damage the terrain layer.13
| method | args | engine call |
|---|---|---|
register | 1 | terrains.register |
updateDefinition | 2 | terrains.updateDefinition |
getTypeFromId | 1 | terrains.getTypeByName |
getTypeAtCell | 2 | terrains.getTypeAtCell |
getDataAtCell | 2 | terrains.getTerrainData |
isAtCell | 2 | terrains.isAtCell |
isTypeAtCell | 3 | terrains.isTypeAtCell |
isCellIdTerrain | — | terrains.isCellIdTerrain |
createAtCellWhenIdle | 4 | world.isCellEmpty, terrains.createAt |
replaceAtCellWhenIdle | 4 | world.getCellId, world.mutateCellWhenIdle, terrains.replaceAt |
removeAtCellWhenIdle | 3 | world.getCellId, terrains.isCellIdTerrain, terrains.removeAt |
damageAtCell | 3 | terrains.damageTerrain |
setHpAtCellWhenIdle | 3 | terrains.isAtCell, terrains.setTerrainHP |
worldCell ids, fog, excavation, dropped world items.13
| method | args | engine call |
|---|---|---|
getCellIdAtCell | 2 | world.getCellId |
isCellEmptyAtCell | 2 | world.isCellEmpty |
isTerrainAtCell | 2 | world.isTerrainAt |
runWhenSimulationIdle | 1 | world.runWhenSimulationIdle |
reportActivityAtCell | 2 | world.reportActivityToChunk |
excavateAtCell | 5 | world.excavate |
revealFogAtCell | 2 | world.revealFogAtCell |
redrawAroundCellWhenIdle | 3 | world.mutateCellWhenIdle, world.redrawSurroundingCells |
pickups.spawnAtWorld | 5 | world.pickups.spawn |
pickups.destroy | 1 | world.pickups.destroy |
pickups.pickUp | 1 | world.pickups.pickUp |
pickups.getAll | 0 | world.pickups.getAll |
pickups.getById | 1 | world.pickups.getById |
playerPosition, movement, inventory, unlocked buildings.12
| method | args | engine call |
|---|---|---|
getWorldPosition | 0 | player.getPosition |
setWorldPosition | 2 | player.setPosition |
setVelocity | 2 | player.setVelocity |
setMovementSpeedMultiplier | 1 | player.setMovementSpeedMultiplier |
setMovementMode | 1 | player.setMovementMode |
isOnGround | 0 | player.isOnGround |
teleportToGround | 0 | player.teleportToGround |
isCollidingWithCell | 2 | player.isCollidingWithCell |
isWithinRadiusOfCell | 3 | player.isWithinRadius |
isWorldPositionClear | 2 | player.isPositionClear |
inventory.addFromId | 1 | player.inventory.add |
buildings.unlockByType | 1 | player.buildings.add |
inputKey bindings, mouse cell position, modifier state.10
| method | args | engine call |
|---|---|---|
registerBinding | 3 | input.registerKeyBinding |
getMouseCellPosition | 0 | input.getMouseCellPosition |
getBoundKeys | 1 | input.getBoundKeys |
getDisplayKey | 2 | input.getDisplayKey |
triggerBinding | 1 | input.triggerBinding |
pressBinding | 1 | input.pressBinding |
releaseBinding | 1 | input.releaseBinding |
resetMouseState | 0 | input.resetMouseState |
isCtrlHeld | 0 | input.isCtrlHeld |
isAltHeld | 0 | input.isAltHeld |
projectilesRegister and spawn projectiles.7
| method | args | engine call |
|---|---|---|
register | 1 | projectiles.register |
getDefinitionById | 1 | — |
createBlueprintFromId | 1 | projectiles.createBlueprint |
getAll | 0 | projectiles.getAll |
getById | 1 | projectiles.getAll |
remove | 1 | projectiles.remove |
spawnAtWorld | 4 | projectiles.spawn |
soundPlay sounds, layered audio, distance falloff.7
| method | args | engine call |
|---|---|---|
play | 2 | sound.play |
playActive | 2 | sound.playActive |
playLayers | 2 | sound.playLayers |
calculateDistanceOptionsAtWorld | 3 | sound.calculateDistanceOptions |
stopById | 1 | sound.stop |
stopActive | 0 | sound.stopActive |
stopAll | 0 | sound.stopAll |
storagePer-mod persistence — in the save, or machine-local.7
| method | args | engine call |
|---|---|---|
ensure | 1 | storage.ensure |
get | 2 | storage.get |
set | 3 | storage.set |
remove | 2 | storage.remove |
local.get | — | storage.local.get |
local.set | — | storage.local.set |
local.remove | — | storage.local.remove |
authorizationWhether the player may build, dig or use tools at a cell.6
| method | args | engine call |
|---|---|---|
canBuildAtCell | 2 | authorization.canBuild |
canGrabAtCell | 2 | authorization.canGrab |
canUseTool | 2 | authorization.canUseTool |
canUseToolAtCell | 3 | authorization.canUseToolAt |
getZoneIdAtCell | 2 | authorization.getZoneIdAt |
getPlayerZoneId | 0 | authorization.getPlayerZoneId |
effectsParticles, temporary lights, lasers, distortion waves.6
| method | args | engine call |
|---|---|---|
createDistortionWaveAtWorld | 3 | effects.createDistortionWave |
createEffectAtWorld | 4 | effects.createEffect |
createLaserAtWorld | 5 | effects.createLaser |
createLightAtWorld | 3 | effects.createLight |
createParticlesAtWorld | 3 | effects.createParticles |
removeLightById | 1 | effects.removeLight |
energyJoin the power grid, produce and consume energy.6
| method | args | engine call |
|---|---|---|
registerType | 3 | energy.registerType |
addAtCell | 4 | energy.add |
consume | 2 | energy.consume |
consumeExcludingNetworkAtCell | 3 | energy.consumeExcludingNetwork |
getNetworkAtCell | 2 | energy.getNetwork |
getNetworkFreeCapacityAtCell | 2 | energy.getNetworkFreeCapacity |
itemsRegister inventory items and tools.6
| method | args | engine call |
|---|---|---|
register | 1 | items.register |
updateDefinition | 2 | items.updateDefinition |
getDefinitionById | 1 | — |
createFromId | 1 | items.create |
getActive | 0 | items.getActive |
isActiveById | 2 | items.isActive |
lightsTemporary VFX lights and persistent world lights.6
| method | args | engine call |
|---|---|---|
vfx.createAtWorld | — | — |
vfx.removeById | — | — |
persistent.createAtWorld | 3 | lights.persistent.create |
persistent.removeAtWorld | 2 | lights.persistent.removeAt |
persistent.fadeAtWorld | 3 | lights.persistent.fadeAt |
persistent.markDirty | 0 | lights.persistent.markDirty |
techRead, patch and add tech-tree nodes.6
| method | args | engine call |
|---|---|---|
getDefinitionById | — | tech.getDefinition |
updateDefinition | — | tech.updateDefinition |
addDefinition | — | tech.addDefinition |
registerNode | — | tech.registerNode |
isLockedById | 1 | tech.isLocked |
setLockedById | 2 | tech.setLocked |
collectorGold value of cells and pickup notification.5
| method | args | engine call |
|---|---|---|
getValueFromCellId | 1 | collector.getValueFromCellId |
getValueByType | 1 | collector.getValueFromElementType |
isCellIdCollectable | 1 | collector.isCellCollectable |
isCellIdCollectableForSprite | 1 | collector.isCellCollectableForSprite |
notifyPickupAtCell | 2 | collector.notifyPickup |
spritesLoad textures, including from the mod's own folder.5
| method | args | engine call |
|---|---|---|
load | 3 | sprites.load |
loadFromMod | 3 | sprites.load |
getById | 1 | sprites.get |
hideAllPlayerModSprites | 0 | sprites.hideAllPlayerModSprites |
rotatePlayerModSprites | 1 | sprites.rotatePlayerModSprites |
upgradesRegister upgrade categories and levels.5
| method | args | engine call |
|---|---|---|
registerCategory | 1 | upgrades.registerCategory |
register | 1 | upgrades.register |
updateDefinition | 3 | upgrades.updateDefinition |
getLevelById | 2 | upgrades.getLevel |
getAvailableLevelById | 2 | upgrades.getAvailableLevel |
buildingPlacement snapping, blocking, structure selection.4
| method | args | engine call |
|---|---|---|
getSnappedPositionAtCell | 2 | building.getSnappedCellPosition |
isBlockedAtCell | 2 | building.isBlockedByTerrainOrElements |
cancelPlacement | 0 | building.cancelPlacement |
selectStructure | 1 | building.selectStructure |
renderingDraw positions, grid metrics, overlay context, shaders.4
| method | args | engine call |
|---|---|---|
getDrawPositionAtCell | 2 | rendering.getCellDrawPos |
getGridMetrics | 0 | rendering.getGridMetrics |
getOverlayViewportSize | 0 | rendering.getOverlayViewportSize |
withOverlayContext | 1 | rendering.withOverlayContext |
toolsGrabber size and state.4
| method | args | engine call |
|---|---|---|
grabber.setSize | 1 | tools.setGrabberSize |
grabber.getSize | 0 | tools.getGrabberSize |
grabber.isActive | 0 | tools.isGrabberActive |
grabber.isLoaded | 0 | tools.isGrabberLoaded |
utilsDistance, direction, angle, line between points.4
| method | args | engine call |
|---|---|---|
getDistance | — | utils.getDistance |
getDirection | — | utils.getDirection |
getAngle | — | utils.getAngle |
getCoordinatesBetweenPoints | — | utils.getCoordinatesBetweenPoints |
actionThe active and selected action, and its custom data.3
| method | args | engine call |
|---|---|---|
getActive | 0 | action.getActive |
getSelected | 0 | action.getSelected |
setCustomData | 1 | action.setCustomData |
assetsAsset URLs and provider selection.3
| method | args | engine call |
|---|---|---|
getUrl | 1 | (local) x.o |
getSelectedProvider | 1 | (local) L.Ob |
selectProvider | 2 | (local) L.jq |
cameraFocus and follow.3
| method | args | engine call |
|---|---|---|
snapToPlayer | 0 | camera.snapToPlayer |
setFocusAtWorld | 2 | camera.setFocusAtWorld |
releaseFocus | 1 | camera.releaseFocusToPlayer |
mapsActive and available maps; starting a map.3
| method | args | engine call |
|---|---|---|
getActive | 0 | maps.getActive |
getAvailable | 0 | maps.getAvailable |
start | 1 | maps.start |
processingRecipes for growers, shakers and the kinetic press.3
| method | args | engine call |
|---|---|---|
registerGrower | 1 | — |
registerShaker | 1 | — |
registerKineticPress | 1 | — |
settingsThis mod's own config values and change notifications.3
| method | args | engine call |
|---|---|---|
get | 1 | — |
getAll | 0 | (local) W.FG |
onChange | 1 | — |
cooldownShared cooldown timers.2
| method | args | engine call |
|---|---|---|
check | 2 | cooldown.check |
isReady | 2 | cooldown.isReady |
discoveriesWhat the player has discovered.2
| method | args | engine call |
|---|---|---|
addElementByType | 1 | discoveries.addElement |
addTerrainByType | 1 | discoveries.addTerrain |
eventsSubscribe to and emit game events.2
| method | args | engine call |
|---|---|---|
on | 2 | events.on |
emit | 2 | events.emit |
fireWhether a cell burns, and setting it burning.2
| method | args | engine call |
|---|---|---|
canBurnElementAtCell | 2 | fire.canBurnElementAt |
burnElementAtCellWhenIdle | 2 | fire.canBurnElementAt, fire.burnElementAt |
gameConfigRead the game's config values.2
| method | args | engine call |
|---|---|---|
get | 1 | (local) D.nO |
getAll | 0 | (local) D.dx |
gridIterate cells in a rect or circle.2
| method | args | engine call |
|---|---|---|
forEachCellInRect | 5 | grid.iterateRect |
forEachCellInCircle | 4 | grid.iterateCircle |
hooksIntercept (veto) or modify engine calls.2
| method | args | engine call |
|---|---|---|
intercept | 3 | hooks.intercept |
modify | 3 | hooks.modify |
patternsCircle patterns and pattern excavation.2
| method | args | engine call |
|---|---|---|
createCircle | — | patterns.createCircle |
excavateAtCell | 6 | patterns.excavate |
randomSeeded integers and floats.2
| method | args | engine call |
|---|---|---|
int | — | random.int |
float | — | random.float |
resourcesResource amounts.2
| method | args | engine call |
|---|---|---|
collectFluxiteAtCell | 2 | resources.collectFluxiteAtCell |
updateEnergy | 2 | resources.updateEnergy |
sharedSharedArrayBuffers — the only main-to-worker channel.2
| method | args | engine call |
|---|---|---|
buffers.create | 2 | workers.shared.create |
buffers.get | 1 | workers.shared.get |
structureBehaviorsTurn a structure into a conveyor or a launcher.2
| method | args | engine call |
|---|---|---|
registerConveyorType | 2 | conveyors.registerType |
registerLauncherType | 1 | launchers.registerType |
timeMilliseconds and tick count.2
| method | args | engine call |
|---|---|---|
getTimeMs | 0 | — |
getTick | 0 | — |
excavationRegister excavation profiles.1
| method | args | engine call |
|---|---|---|
registerProfile | 2 | excavation.registerProfile |
modsAsset providers from other mods.1
| method | args | engine call |
|---|---|---|
getProviders | 1 | (local) L.KT |
progressionComplete progression steps.1
| method | args | engine call |
|---|---|---|
complete | 1 | progression.complete |
raycastCast a ray through the world.1
| method | args | engine call |
|---|---|---|
castFromWorld | 4 | raycast.cast |
reactionsRegister element reactions.1
| method | args | engine call |
|---|---|---|
registerContact | 1 | reactions.registerContact |
sceneWhich screen is active. MainMenu 1, Intro 2, Deploy 3, Game 4.1
| method | args | engine call |
|---|---|---|
getActive | 0 | scene.getActive |
scheduleDeferred callbacks.1
| method | args | engine call |
|---|---|---|
nextTick | 1 | schedule.nextTick |
signalsRegister a structure as a signal target.1
| method | args | engine call |
|---|---|---|
targets.register | 2 | signals.targets.register |
triggersRegister a repeating timer.1
| method | args | engine call |
|---|---|---|
register | 2 | triggers.register |
workersToggle post-update on the simulation workers.1
| method | args | engine call |
|---|---|---|
setPostUpdateEnabled | 1 | workers.shared.create |
Worker API 88 methods
A quarter the size, and deliberately so: this thread exists to touch cells fast.
elementsRead and write cells directly — this is where per-particle work belongs.26
| method | args | engine call |
|---|---|---|
getTypeFromId | 1 | elements.getElementTypeFromId |
getDefinitionByType | 1 | elements.getConfig |
getTypeAtCell | 2 | elements.getTypeAtCell |
getResolvedTypeAtCell | 2 | elements.getResolvedTypeAtCell |
getResolvedTypeFromCellId | 1 | elements.getResolvedTypeFromCellId |
getInfoAtCell | 2 | elements.getInfoAtCell |
getMatterTypeAtCell | 2 | elements.getMatterTypeAtCell |
isTypeAtCell | 3 | elements.isTypeAt |
isFreeFallingAtCell | 2 | elements.getResolvedTypeAtCell, elements.isFreeFalling |
createAtCell | 4 | world.isCellEmpty, elements.createAt |
replaceAtCell | 4 | elements.replaceAt |
removeAtCell | 3 | elements.getResolvedTypeAtCell, elements.removeAt |
moveBetweenCells | 4 | elements.move |
teleportBetweenCells | 4 | elements.teleport |
swapCells | 4 | elements.swap |
getVelocityAtCell | 2 | elements.getResolvedTypeAtCell, elements.getVelocity |
setVelocityAtCell | 3 | elements.setVelocity |
addParticleVelocityAtCell | 4 | elements.addParticleVelocity |
convertToParticleAtCell | 3 | elements.convertToParticle |
convertFromParticleAtCell | 2 | elements.convertFromParticle |
getDataFieldAtCell | 3 | elements.getResolvedTypeAtCell, elements.getDataField |
setDataFieldAtCell | 4 | elements.getResolvedTypeAtCell, elements.setDataField |
refreshColorAtCell | 2 | elements.refreshColorAt |
markMovementBlockedByElementIndex | 1 | elements.markMovementBlocked |
setPhysicsAtCell | 3 | elements.getResolvedTypeAtCell, elements.setPhysics, world.reportActivityToChunk |
setDurationAtCell | 4 | elements.getResolvedTypeAtCell, elements.setDuration |
structuresTest what is built at a cell and read its processing state.14
| method | args | engine call |
|---|---|---|
processing.isEnabledAt | 2 | structures.processing.isEnabledAt |
getAtCell | 2 | structures.getAtCell |
getDefinitionByType | 1 | structures.getConfig |
getTypeFromId | 1 | structures.resolveTypeName |
hasBuiltAtCell | 2 | structures.hasBuiltAtCell |
isType | 2 | structures.isType |
isTypeAtCell | 3 | structures.isTypeAt |
forEachOfType | 2 | structures.forEachOfType |
update | 2 | structures.update |
setData | 3 | structures.setData |
setSpritesheetIndex | 2 | structures.setSpritesheetIndex |
setSpritesheetIndexAtCell | 3 | structures.setSpritesheetIndexAt |
setSpritesheetIndexByValue | 3 | structures.setSpritesheetIndexByValue |
setSpritesheetIndexByValueAtCell | 4 | structures.setSpritesheetIndexByValueAt |
terrainsRead, create and damage terrain from the simulation thread.11
| method | args | engine call |
|---|---|---|
getTypeFromId | 1 | terrains.getTypeByName |
getTypeAtCell | 2 | terrains.getTypeAtCell |
getDataAtCell | 2 | terrains.getTerrainData |
isAtCell | 2 | terrains.isAtCell |
isTypeAtCell | 3 | terrains.isTypeAtCell |
isCellIdTerrain | — | terrains.isCellIdTerrain |
createAtCell | 4 | world.isCellEmpty, terrains.createAt |
replaceAtCell | 4 | terrains.replaceAt |
removeAtCell | 3 | world.getCellId, terrains.isCellIdTerrain, terrains.removeAt |
damageAtCell | 3 | terrains.damageTerrain |
setHpAtCell | 3 | terrains.setTerrainHP |
collectorCollectable value of a cell.5
| method | args | engine call |
|---|---|---|
getValueFromCellId | 1 | collector.getValueFromCellId |
getValueByType | 1 | collector.getValueFromElementType |
isCellIdCollectable | 1 | collector.isCellCollectable |
isCellIdCollectableForSprite | 1 | collector.isCellCollectableForSprite |
notifyPickupAtCell | 2 | collector.notifyPickup |
worldCell ids, emptiness, activity reporting, excavation.5
| method | args | engine call |
|---|---|---|
getCellIdAtCell | 2 | world.getCellId |
isCellEmptyAtCell | 2 | world.isCellEmpty |
isTerrainAtCell | 2 | world.isTerrainAt |
reportActivityAtCell | 2 | world.reportActivityToChunk |
excavateAtCell | 5 | world.excavate |
utilsDistance, direction, angle, line between points.4
| method | args | engine call |
|---|---|---|
getDistance | — | utils.getDistance |
getDirection | — | utils.getDirection |
getAngle | — | utils.getAngle |
getCoordinatesBetweenPoints | — | utils.getCoordinatesBetweenPoints |
effectsParticles and lights raised from the worker.3
| method | args | engine call |
|---|---|---|
createEffectAtWorld | 4 | effects.createEffect |
createLightAtWorld | 3 | effects.createLight |
createParticlesAtWorld | 3 | effects.createParticles |
playerPlayer position and proximity tests.3
| method | args | engine call |
|---|---|---|
getWorldPosition | 0 | player.getPosition |
isCollidingWithCell | 2 | player.isCollidingWithCell |
isWithinRadiusOfCell | 3 | player.isWithinRadius |
eventsSubscribe to simulation events. Some require a guard.2
| method | args | engine call |
|---|---|---|
on | 3 | — |
emit | 3 | — |
fireWhether a cell burns, and setting it burning.2
| method | args | engine call |
|---|---|---|
canBurnElementAtCell | 2 | fire.canBurnElementAt |
burnElementAtCell | 2 | fire.canBurnElementAt, fire.burnElementAt |
hooksIntercept or modify engine calls inside the simulation.2
| method | args | engine call |
|---|---|---|
intercept | 3 | — |
modify | 3 | — |
patternsCircle patterns and pattern excavation.2
| method | args | engine call |
|---|---|---|
createCircle | — | patterns.createCircle |
excavateAtCell | 6 | patterns.excavate |
randomSeeded integers and floats.2
| method | args | engine call |
|---|---|---|
int | — | random.int |
float | — | random.float |
sharedRead the buffers the main entry created.2
| method | args | engine call |
|---|---|---|
buffers.require | 2 | workers.shared.get |
buffers.get | 1 | workers.shared.get |
workerThis thread's index and the total thread count.2
| method | args | engine call |
|---|---|---|
getIndex | 0 | — |
getCount | 0 | — |
mainEmit an event to the main thread. The only way out.1
| method | args | engine call |
|---|---|---|
emitEvent | 2 | — |
mapsThe active map.1
| method | args | engine call |
|---|---|---|
getActive | 0 | maps.getActive |
uiToasts. That is the whole UI surface here.1
| method | args | engine call |
|---|---|---|
toast | 2 | ui.toast |
Value catalogs
The reference tables above say what a mod can call. These are the values worth passing — collected from the same shipped code, so they regenerate with the game.
Sound ids (63)
Everything the game itself passes to its sound engine, playable via sound.play(id, options) — with calculateDistanceOptionsAtWorld for falloff and rateLimitKey so a hundred machines share one voice.
Structure ambience groups, for the ambienceGroup render field: clearingFrames, conveyors, launchers, shakers, steamTurbines.
gameConfig defaults (81)
What gameConfig.getAll() returns before anything overrides it — including the flags no options screen exposes. customMaps.showCustomMaps and mods.showSubscribedMods ship false: whole features behind a boolean. The procgen subtree is the map generator's own knobs, seed included.
gameConfigevery default, in source order81
| key | default |
|---|---|
version | "…" |
fps | 60 |
lockFps | false |
chunkSize | 40 |
cellSize | 4 |
gravity | "…" |
upflow | "…" |
snapGridCellSize | 4 |
startingResources | 0 |
debug.active | false |
debug.showUnderlyingCellsInStructures | true |
debug.drawChunks | false |
debug.drawChunkFade | 60 |
debug.drawRulers | false |
debug.showThreadLoad | true |
debug.controls | true |
debug.defaultBaseHue | 35 |
debug.brushSize | 3 |
debug.brushShape | "circle" |
debug.brushThrottle | 0 |
debug.highlightBrush | true |
debug.preventDuplicateCells | false |
debug.doNotDrawStructures | false |
debug.stopOnDebugCellUpdate | false |
debug.overrideLightSize | false |
debug.overrideTerrainShadow | false |
debug.lightSize | 700 |
debug.terrainShadowValue | 0.995 |
debug.flashlight.brightness | 2 |
debug.flashlight.size | 200 |
debug.flashlight.duration | 100 |
debug.flashlight.color | "[…]" |
debug.gameOfLife | "…" |
debug.countEvents | false |
debug.strictSafeguards | false |
debug.showLights | false |
debug.showProximityFade | true |
debug.showFilters | false |
debug.showAuthorizationZones | false |
debug.showProximityFadeSelectedOnly | false |
debug.selectedLightIndex | -1 |
debug.cellInspector | false |
debug.badDisplays | false |
debug.i18nDebug.mode | "off" |
debug.i18nDebug.pseudo.brackets | true |
debug.i18nDebug.pseudo.accentChars | true |
debug.i18nDebug.pseudo.padPercent | 0.3 |
debug.i18nDebug.randomScript.script | "chinese" |
debug.i18nDebug.randomScript.lengthMultiplier | 1 |
conveyorDefaultSpeed | 0.05 |
useMultithreading | true |
useExperimentalRenderer | false |
obstacleBreakpoint | 100 |
playerSize.width | 12 |
playerSize.height | 30 |
matrixCompression.padding | 100 |
blueprintEncoding | "binary" |
mods.showSubscribedMods | false |
customMaps.showCustomMaps | false |
procgen.useProcgenMap | true |
procgen.showGeneratedMap | false |
procgen.logPerformance | true |
procgen.logVerbosity | 1 |
procgen.params.width | 3840 |
procgen.params.height | 3840 |
procgen.params.scale | 0.005 |
procgen.params.threshold | 0.02 |
procgen.params.cellSize | 1 |
procgen.params.seed | "…" |
procgen.prefabPlacementBufferCells | 20 |
procgen.mossPatchMinDepthCells | 30 |
procgen.caveRegionLights.brightness | 1 |
procgen.caveRegionLights.size | 200 |
procgen.leftWall.enabled | false |
procgen.leftWall.startPosition | 0.2 |
procgen.leftWall.steepness | 0.01 |
procgen.leftWall.noiseAmount | 0.15 |
procgen.leftWall.entrance.enabled | false |
procgen.leftWall.entrance.width | 200 |
procgen.leftWall.entrance.height | 120 |
procgen.saveGeneratedMap | false |
window.electron (65)
The Electron preload bridge. Mod entries run in the same renderer, so all of it is reachable from a mod: openDevTools(), save export and import, the local-mods folder, Workshop subscription management.
window.electronthe preload bridge, as shipped65
| method |
|---|
window.electron.getPlatformSync() |
window.electron.getIsSteamDeckSync() |
window.electron.getPreferredSystemLanguagesSync() |
window.electron.onAppSuspend(callback) |
window.electron.onAppResume(callback) |
window.electron.diagnostics() |
window.electron.checkLicense() |
window.electron.writeGameEvent(eventName, dimensions, measurements) |
window.electron.platformPrimeAchievements(ids) |
window.electron.platformShowReauthPrompt(reason) |
window.electron.appQuit() |
window.electron.openExternalBrowser(url) |
window.electron.onXboxUserSignedOut(callback) |
window.electron.onXboxLicenseLost(callback) |
window.electron.onXboxUserSignedIn(callback) |
window.electron.save(id, name, data) |
window.electron.saveSerialized(id, name, dataJson, metadata) |
window.electron.load(id) |
window.electron.deleteSave(id) |
window.electron.loadRaw(name) |
window.electron.exportSave(id) |
window.electron.importSave(bytes) |
window.electron.getSaveFiles() |
window.electron.getSaveFolder() |
window.electron.localMods.getFolder() |
window.electron.localMods.openFolder() |
window.electron.localMods.list() |
window.electron.localMods.upload(modId) |
window.electron.getLastPlayedGameSync() |
window.electron.saveLastPlayedGame(game) |
window.electron.clearLastPlayedGame() |
window.electron.saveExistsSync(id) |
window.electron.getSettingsSync() |
window.electron.saveSettings(settings) |
window.electron.setFullscreen(shouldBeFullscreen) |
window.electron.openDevTools() |
window.electron.isFilePatchingActiveSync() |
window.electron.platform.isInitialized() |
window.electron.platform.getPlayerName() |
window.electron.platform.getPlayerId() |
window.electron.platform.getAppId() |
window.electron.platform.unlockAchievement(achievementId) |
window.electron.platform.isAchievementUnlocked(achievementId) |
window.electron.platform.clearAchievement(achievementId) |
window.electron.platform.cloudSave(fileName, data) |
window.electron.platform.cloudLoad(fileName) |
window.electron.platform.cloudFileExists(fileName) |
window.electron.platform.cloudDelete(fileName) |
window.electron.platform.cloudSync() |
window.electron.platform.workshop.subscribe(itemId) |
window.electron.platform.workshop.unsubscribe(itemId) |
window.electron.platform.workshop.installInfo(itemId) |
window.electron.platform.workshop.downloadInfo(itemId) |
window.electron.platform.workshop.getState(itemId) |
window.electron.platform.workshop.getSubscribedItems() |
window.electron.platform.workshop.getItem(itemId) |
window.electron.platform.workshop.download(itemId, highPriority) |
window.electron.platform.workshop.getSandkitMods() |
window.electron.platform.overlay.openUrl(url) |
window.electron.log(level, scope, message) |
window.electron.getSystemInfo() |
window.electron.customMaps.save(id, name, data) |
window.electron.customMaps.load(id) |
window.electron.customMaps.list() |
window.electron.customMaps.delete(id) |
apiVersion. It is the same category as sandkit.engine.api: real, useful, and free to change in any patch. Wrap every call, fail soft.Beyond api
sandkit.api is not the whole object a mod is handed. Three more members sit next to it, none of them a method, which is why none of them appears in the tables above — and why a mod cannot draw a single button without one of them.
sandkit.enums (22 / 323)
The game's own enumerations, re-exported member by member. These are the numbers the API expects: ui.update(enums.ComponentId.Hotbar), scene.getActive() === enums.Scene.MainMenu, a key state compared against enums.KeyState.Pressed. Writing the literal instead works right up until a patch renumbers it.
enums.CellType31
| member | value |
|---|---|
Empty | 0 |
Element | 1 |
Dirt | 2 |
SporeSoil | 3 |
Fog | 4 |
FogJetpackBlock | 5 |
FogWater | 6 |
FreezingIceSoil | 7 |
Divider | 8 |
Grass | 9 |
Moss | 10 |
GoldSoil | 11 |
Petal | 12 |
FogLava | 13 |
Fluxite | 14 |
Block | 15 |
SlidingBlock | 16 |
SlidingBlockLeft | 17 |
SlidingBlockRight | 18 |
ConveyorLeft | 19 |
ConveyorRight | 20 |
ShakerLeft | 21 |
ShakerRight | 22 |
Stone | 23 |
VelocitySoaker | 24 |
Ice | 25 |
Grower | 26 |
NascentWater | 27 |
SandiumSoil | 28 |
Obsidian | 29 |
Crackstone | 30 |
enums.ElementType20
| member | value |
|---|---|
Sand | 1 |
Particle | 2 |
Water | 3 |
WetSand | 4 |
Sandium | 5 |
Residue | 6 |
Gold | 7 |
Gloom | 8 |
Shake | 9 |
Steam | 10 |
Fire | 11 |
FreezingIce | 12 |
Flame | 13 |
BurntResidue | 14 |
Seed | 15 |
WetSeed | 16 |
Seedling | 17 |
Petalium | 18 |
Lava | 19 |
Basalt | 20 |
enums.MatterType8
| member | value |
|---|---|
Solid | 1 |
Liquid | 2 |
Particle | 3 |
Gas | 4 |
Static | 5 |
Slushy | 6 |
Wisp | 7 |
Powder | 8 |
enums.StructureType27
| member | value |
|---|---|
ConveyorLeft | 1 |
ConveyorRight | 2 |
ShakerLeft | 3 |
ShakerRight | 4 |
LauncherUp | 5 |
LauncherLeft | 6 |
LauncherRight | 7 |
SplitterLeft | 8 |
SplitterRight | 9 |
Dropper | 10 |
Foundation | 11 |
FoundationAngledLeft | 12 |
FoundationTriangleLeftDel | 13 |
FoundationAngledRight | 14 |
FoundationTriangleRightDel | 15 |
Collector | 16 |
FilterLeft | 17 |
FilterRight | 18 |
SlidingFoundation | 19 |
VelocitySoaker | 20 |
Grower | 21 |
SoundBox | 22 |
Pipe | 23 |
Pump | 24 |
LiquidVent | 25 |
Light | 26 |
FluxEmanator | 27 |
enums.ItemType4
| member | value |
|---|---|
Weapon | 1 |
Tool | 2 |
Consumable | 3 |
Mod | 4 |
enums.ItemId16
| member | value |
|---|---|
Shovel | 1 |
Grabber | 2 |
Demolisher | 3 |
GrapplingHook | 4 |
Vacuum | 5 |
Gun | 6 |
Copier | 7 |
RocketLauncher | 8 |
Digger | 9 |
Shotgun | 10 |
Teleporter | 11 |
Flamethrower | 12 |
PipeRemover | 13 |
Hauler | 14 |
Cryoblaster | 15 |
MegaShotgun | 16 |
enums.ProjectileType6
| member | value |
|---|---|
Bullet | 1 |
Rocket | 2 |
GrapplingHook | 3 |
Fire | 4 |
Digger | 5 |
Mod | 6 |
enums.ActionType4
| member | value |
|---|---|
Weapon | 1 |
Building | 2 |
Tool | 3 |
Mod | 4 |
enums.ActionState3
| member | value |
|---|---|
Start | 1 |
Active | 2 |
End | 3 |
enums.AbilityType4
| member | value |
|---|---|
Dig | 1 |
Shoot | 2 |
Spray | 3 |
Laser | 4 |
enums.ReloadType3
| member | value |
|---|---|
Clip | 1 |
Single | 2 |
OverTime | 3 |
enums.ComponentId28
| member | value |
|---|---|
Hotbar | 1 |
SoundBoxConfig | 2 |
Root | 4 |
Menu | 5 |
Management | 6 |
FilterConfig | 7 |
Resources | 8 |
TechTree | 9 |
Tutorial | 10 |
Loader | 11 |
Options | 12 |
ShortcutHelper | 13 |
Upgrades | 14 |
Tooltip | 15 |
Notifications | 16 |
Objectives | 17 |
DroneAdminList | 18 |
HotbarOverlays | 19 |
IntroScreen | 20 |
StoryNotifications | 21 |
FactoryProgress | 22 |
Dialogs | 23 |
GlobalOverlays | 24 |
Lexicon | 25 |
ModsScreen | 26 |
CustomMapsScreen | 27 |
CinematicPanel | 28 |
Feedback | 29 |
enums.KeyBinding27
| member | value |
|---|---|
OpenBuildMenu | "OpenBuildMenu" |
GrapplingHook | "GrapplingHook" |
Escape | "Escape" |
OpenTechTree | "OpenTechTree" |
OpenInventory | "OpenInventory" |
ReverseBuildDirection | "ReverseBuildDirection" |
Marquee | "Marquee" |
Pause | "Pause" |
Copy | "Copy" |
Paste | "Paste" |
Flip | "Flip" |
Delete | "Delete" |
PauseCamera | "PauseCamera" |
OpenUpgrades | "OpenUpgrades" |
BuildMode | "BuildMode" |
Demolish | "Demolish" |
Hover | "Hover" |
Ruler | "Ruler" |
Left | "Left" |
Right | "Right" |
Boost | "Boost" |
Descend | "Descend" |
SprintBoost | "SprintBoost" |
OverrideReplaceStructures | "OverrideReplaceStructures" |
QuickSave | "QuickSave" |
QuickLoad | "QuickLoad" |
ToggleGameHud | "ToggleGameHud" |
enums.KeyState5
| member | value |
|---|---|
Up | 1 |
Down | 2 |
Pressed | 3 |
Released | 4 |
All | 5 |
enums.BuildMode2
| member | value |
|---|---|
Linear | 1 |
Rectangular | 2 |
enums.BuildingClearance4
| member | value |
|---|---|
Available | 1 |
FullyBlocked | 2 |
PartiallyBlocked | 3 |
CanBeReplaced | 4 |
enums.AuthorizationType6
| member | value |
|---|---|
NoJetpack | 1 |
NoGrab | 2 |
NoBuild | 3 |
NoTool | 4 |
NoExcavation | 5 |
NoToolExceptFlamethrower | 6 |
enums.DroneType2
| member | value |
|---|---|
Digger | 1 |
Hauler | 2 |
enums.WorldItemType4
| member | value |
|---|---|
Artifact | 1 |
GlyphKey | 2 |
Stratacore | 3 |
Orb | 4 |
enums.Scene4
| member | value |
|---|---|
MainMenu | 1 |
Intro | 2 |
Deploy | 3 |
Game | 4 |
enums.Tech110
| member | value |
|---|---|
Shaker | 1 |
Conveyors | 2 |
Guns1 | 3 |
Filters1 | 4 |
Flamethrower | 5 |
Gun | 6 |
KineticPress | 7 |
Guns2 | 8 |
Drones1 | 9 |
Upgrading2 | 10 |
Filters2 | 11 |
Upgrading3 | 12 |
Upgrading4 | 13 |
Upgrading5 | 14 |
Upgrading6 | 15 |
Upgrading7 | 16 |
Upgrading8 | 17 |
Upgrading9 | 18 |
Upgrading10 | 19 |
PlanterBox | 20 |
Thermo | 21 |
Rocket | 22 |
Pipes | 23 |
StaticLights | 24 |
Drones2 | 25 |
Smelter | 26 |
Tools4 | 27 |
Guns3 | 28 |
Pipes2 | 29 |
ConveyorsMk2 | 30 |
Lights2 | 31 |
Refining6 | 32 |
Refining7 | 33 |
Guns4 | 34 |
Guns5 | 35 |
Tools5 | 36 |
Tools6 | 37 |
Filters3 | 38 |
Filters4 | 39 |
Pipes3 | 40 |
Pipes4 | 41 |
Logistics3 | 42 |
Logistics4 | 43 |
Lights3 | 44 |
Lights4 | 45 |
Drones3 | 46 |
Drones4 | 47 |
Alien | 48 |
Electricity | 49 |
AlienCore | 50 |
Emanators1 | 51 |
AlienPlasmaConduits | 52 |
AlienQuantumMatrix | 53 |
AlienPlasmaCore | 54 |
AlienVoidEngine | 55 |
FlareGun | 56 |
Sweeper | 57 |
Utilities3 | 58 |
Cryoblaster | 59 |
Vacuum | 60 |
Utilities6 | 61 |
Utilities7 | 62 |
Filters | 63 |
AdvancedFilters | 64 |
Infrastructure3 | 65 |
Decorations1 | 66 |
Decorations2 | 67 |
Decorations3 | 68 |
Blocks1 | 69 |
Drill | 70 |
SteamTurbine | 71 |
Electricity3 | 72 |
Electricity4 | 73 |
Logic1 | 74 |
Logic2 | 75 |
Logic3 | 76 |
Logic4 | 77 |
Various1 | 78 |
Various2 | 79 |
Various3 | 80 |
Locator | 81 |
QuantumPortal | 82 |
VoidRift | 83 |
Blink | 84 |
Recall | 85 |
ImplosionGun | 86 |
Refining8 | 87 |
Tools7 | 88 |
Diggers | 89 |
Haulers | 90 |
Map | 91 |
ColoringTool | 92 |
SignalGate | 93 |
GrapplingHook | 94 |
GlassFoundation | 95 |
PrecisionTools | 96 |
SignalDevices | 97 |
SignalControls | 98 |
LogicGates | 99 |
RetroConsole | 100 |
WallTool | 101 |
Corraller | 102 |
PlainFoundation | 103 |
ClearingFrame | 104 |
Heatmap | 105 |
MiningLaser | 106 |
GoldBattery | 107 |
Hover | 108 |
SprintBoost | 109 |
CritterFence | 110 |
enums.TechStatus5
| member | value |
|---|---|
Available | 0 |
Visible | 1 |
Researched | 2 |
Unknown | 3 |
Hidden | 4 |
sandkit.react (18.3.1)
A frozen copy of the game's own React. Mod components render inside the game's tree, so hooks work normally — but they have to come from here, because a second copy of React in one tree does not.
ui.inject and ui.overlays.register take components built with it; an entry cannot use JSX, so it is react.createElement by hand or a small h() helper.sandkit.state (424 / 1362)
The live game state, and the same object as sandkit.engine.state. It is where mods go when the API has no getter — the energy buffer, the open windows, the resource counts. Nothing about its shape is in the shipped code, so this table is measured from a running game: names, types and sizes only, never values.
Three levels deep on this page. The dump goes to 4 and holds 1362 paths in all — data/state.json has the rest. Branches keyed by id rather than shaped — whichever mods are installed, whichever tech a save has — are kept as a node with their children dropped, since walking them describes one machine rather than the game: state.sandkit (1038), state.session.lexicon.entriesById (337), state.store.mods (120), state.store.player.tech (71), state.shared.mods (19), state.session.settings.externalModSettings (14).
state.environmentbuild and platform4
| path | type | size |
|---|---|---|
state.environment | object | 2 |
state.environment.context | number | |
state.environment.multithreading | object | 1 |
state.environment.multithreading.simulation | object | 16 |
state.sandkitthe mod layer itself — reflects whichever mods are installed1
| path | type | size |
|---|---|---|
state.sandkit | object | 7 |
state.sessionthis run only: input, windows, camera, rendering, settings193
| path | type | size |
|---|---|---|
state.session | object | 51 |
state.session.action | object | 3 |
state.session.action.customData | null | |
state.session.action.point | object | 2 |
state.session.action.state | object | 3 |
state.session.actionLocked | boolean | |
state.session.ambience | object | 5 |
state.session.ambience.clearingFrames | object | 2 |
state.session.ambience.conveyors | object | 2 |
state.session.ambience.launchers | object | 2 |
state.session.ambience.shakers | object | 2 |
state.session.ambience.steamTurbines | object | 2 |
state.session.animations | object | 1 |
state.session.animations.player | object | 3 |
state.session.building | object | 6 |
state.session.building.activeStructureType | null | |
state.session.building.amountOfTiles | number | |
state.session.building.ignoreAngleLock | boolean | |
state.session.building.lockedAngle | null | |
state.session.building.placing | boolean | |
state.session.building.start | object | 2 |
state.session.buttons | object | 1 |
state.session.buttons.pressed | boolean | |
state.session.cache | object | 2 |
state.session.cache.pipes | object | 4 |
state.session.cache.structures | object | 4 |
state.session.camera | object | 2 |
state.session.camera.x | number | |
state.session.camera.y | number | |
state.session.cheat | object | 1 |
state.session.cheat.bypassCosts | boolean | |
state.session.cinematic | undefined | |
state.session.colors | object | 1 |
state.session.colors.scheme | object | 2 |
state.session.construction | object | 3 |
state.session.construction.demolisherActive | boolean | |
state.session.construction.marqueeActive | boolean | |
state.session.construction.rulerActive | boolean | |
state.session.debug | object | 1 |
state.session.debug.brush | object | 4 |
state.session.effects | array | 0 |
state.session.explosions | array | 0 |
state.session.externalMods | object | 6 |
state.session.externalMods.diagnostics | array of object | 3 |
state.session.externalMods.missingSavedMods | array of string | 3 |
state.session.externalMods.orderedMods | array of object | 14 |
state.session.externalMods.statuses | array of object | 14 |
state.session.externalMods.workerEntries | array of object | 1 |
state.session.externalMods.workerResults | array of object | 8 |
state.session.factoryProcessRates | Float64Array | 4 |
state.session.input | object | 9 |
state.session.input.action | object | 2 |
state.session.input.bindingStates | object | 0 |
state.session.input.currentLastKey | string | |
state.session.input.currentLastKeyCode | string | |
state.session.input.deckCursor | object | 5 |
state.session.input.deckLastManagementTab | string | |
state.session.input.keys | object | 57 |
state.session.input.mode | string | |
state.session.input.mouse | object | 10 |
state.session.lerpCamera | boolean | |
state.session.lexicon | object | 3 |
state.session.lexicon.compiled | boolean | |
state.session.lexicon.entries | array of object | 337 |
state.session.lexicon.entriesById | object | 337 |
state.session.lightZones | array of array | 150 |
state.session.lights | array of null | 100 |
state.session.mainSensorCache | object | 1 |
state.session.mainSensorCache.cache | object | 1 |
state.session.mods | object | 1 |
state.session.mods.signals | object | 13 |
state.session.monitor | object | 1 |
state.session.monitor.threads | array of object | 8 |
state.session.movementSpeedMultiplier | number | |
state.session.music | object | 2 |
state.session.music.ambient | object | 1 |
state.session.music.engine | object | 30 |
state.session.nextTickCallbacks | array | 0 |
state.session.notifications | object | 2 |
state.session.notifications.story | array | 0 |
state.session.notifications.toast | string | |
state.session.overrideCamera | boolean | |
state.session.paused | boolean | |
state.session.platform | string | |
state.session.prefabWorldItemCache | object | 1 |
state.session.prefabWorldItemCache.cache | object | 10 |
state.session.reconMode | boolean | |
state.session.rendering | object | 24 |
state.session.rendering.backgroundChannel | object | 2 |
state.session.rendering.canvas | object | 0 |
state.session.rendering.channel | object | 2 |
state.session.rendering.foregroundChannel | object | 2 |
state.session.rendering.foregroundOffscreenCanvas | object | 0 |
state.session.rendering.foregroundOffscreenContext | object | 0 |
state.session.rendering.images | object | 102 |
state.session.rendering.obstacleCanvas | object | 0 |
state.session.rendering.obstacleChannel | object | 2 |
state.session.rendering.obstacleContext | object | 0 |
state.session.rendering.obstacleOffscreenCanvas | object | 0 |
state.session.rendering.obstacleOffscreenContext | object | 0 |
state.session.rendering.offscreenCanvas | object | 0 |
state.session.rendering.offscreenContext | object | 0 |
state.session.rendering.offscreenOutlineCanvas | object | 0 |
state.session.rendering.offscreenOutlineContext | object | 0 |
state.session.rendering.overlayCanvas | object | 0 |
state.session.rendering.overlayContext | object | 0 |
state.session.rendering.pixi | object | 43 |
state.session.rendering.worldCanvas | object | 0 |
state.session.rendering.worldChannel | object | 2 |
state.session.rendering.worldContext | object | 0 |
state.session.rendering.worldOffscreenCanvas | object | 0 |
state.session.rendering.worldOffscreenContext | object | 0 |
state.session.resolution | object | 2 |
state.session.resolution.height | number | |
state.session.resolution.width | number | |
state.session.runtime | string | |
state.session.saving | boolean | |
state.session.scale | number | |
state.session.settings | object | 23 |
state.session.settings.autosaveInterval | number | |
state.session.settings.cursorScale | number | |
state.session.settings.disableBackgroundShader | boolean | |
state.session.settings.disableShadows | boolean | |
state.session.settings.externalModSettings | object | 14 |
state.session.settings.focusLoss | object | 3 |
state.session.settings.frameRateCap | number | |
state.session.settings.hardMode | boolean | |
state.session.settings.hotbarCount | number | |
state.session.settings.hybridScheduling | boolean | |
state.session.settings.keyBindings | object | 66 |
state.session.settings.locale | string | |
state.session.settings.lockHotbar | boolean | |
state.session.settings.lockSkyColor | boolean | |
state.session.settings.pipesModeView | boolean | |
state.session.settings.schedulingMode | number | |
state.session.settings.settingsVersion | number | |
state.session.settings.showFps | boolean | |
state.session.settings.sound | object | 3 |
state.session.settings.uiScale | number | |
state.session.settings.upgradesHideMax | boolean | |
state.session.settings.videoZoom | number | |
state.session.settings.windowMode | string | |
state.session.soundBox | object | 1 |
state.session.soundBox.selectedNote | object | 2 |
state.session.soundEngine | object | 8 |
state.session.soundEngine.audioContext | object | 0 |
state.session.soundEngine.gate | object | 1 |
state.session.soundEngine.globalVolume | number | |
state.session.soundEngine.limiter | object | 0 |
state.session.soundEngine.masterGain | object | 0 |
state.session.soundEngine.muted | boolean | |
state.session.soundEngine.outputLimiter | object | 0 |
state.session.soundEngine.sfx | object | 106 |
state.session.sprintBoost | object | 2 |
state.session.sprintBoost.meter | number | |
state.session.sprintBoost.recharging | boolean | |
state.session.teleportZoneCache | object | 1 |
state.session.teleportZoneCache.cache | object | 270 |
state.session.timestep | object | 2 |
state.session.timestep.frameDuration | number | |
state.session.timestep.lastTime | number | |
state.session.triggers | array of object | 26 |
state.session.ui | object | 9 |
state.session.ui.dialogs | array | 0 |
state.session.ui.discoveryPopups | array | 0 |
state.session.ui.hudHidden | boolean | |
state.session.ui.introScreen | object | 1 |
state.session.ui.listeners | object | 24 |
state.session.ui.nextListenerId | number | |
state.session.ui.overlays | object | 2 |
state.session.ui.tooltip | object | 2 |
state.session.ui.visible | boolean | |
state.session.view | object | 4 |
state.session.view.maxZoom | number | |
state.session.view.minZoom | number | |
state.session.view.zoom | number | |
state.session.view.zoomDirty | boolean | |
state.session.visualParticles | array of object | 94 |
state.session.windows | object | 13 |
state.session.windows.blueprints | object | 1 |
state.session.windows.building | object | 3 |
state.session.windows.conservatory | object | 1 |
state.session.windows.customMapsScreen | object | 1 |
state.session.windows.feedback | object | 1 |
state.session.windows.inventory | object | 1 |
state.session.windows.lexicon | object | 1 |
state.session.windows.loader | object | 1 |
state.session.windows.menu | object | 1 |
state.session.windows.modsScreen | object | 1 |
state.session.windows.options | object | 1 |
state.session.windows.techTree | object | 2 |
state.session.windows.upgrades | object | 1 |
state.session.zoomLevel | number |
state.sharedSharedArrayBuffers the simulation threads write into83
| path | type | size |
|---|---|---|
state.shared | object | 33 |
state.shared.actionState | Uint8Array | 3 |
state.shared.authorization | object | 3 |
state.shared.authorization.data | Uint8Array | 14745600 |
state.shared.authorization.height | number | |
state.shared.authorization.width | number | |
state.shared.collectorGoldCount | Uint8Array | 921600 |
state.shared.conveyorBeltsAnimationIndex | Uint8Array | 8 |
state.shared.debug | object | 1 |
state.shared.debug.cellPos | Uint16Array | 2 |
state.shared.energy | Uint32Array | 1 |
state.shared.energyBatteryDirty | Uint8Array | 921600 |
state.shared.energyChange | Uint32Array | 4 |
state.shared.gold | Uint32Array | 1 |
state.shared.goldChange | Uint32Array | 4 |
state.shared.hybridScheduling | Uint8Array | 1 |
state.shared.listenerPos | Float32Array | 2 |
state.shared.managerPerformance | Float32Array | 23 |
state.shared.mapData | object | 3 |
state.shared.mapData.data | Uint8Array | 58982400 |
state.shared.mapData.height | number | |
state.shared.mapData.width | number | |
state.shared.mods | object | 19 |
state.shared.mouse | object | 1 |
state.shared.mouse.worldPosition | Uint16Array | 2 |
state.shared.mutationSync | Int32Array | 1 |
state.shared.naturalAmbience | Float32Array | 8 |
state.shared.playerPos | Float32Array | 2 |
state.shared.productionPoints | Uint32Array | 1 |
state.shared.reservoir | Uint16Array | 1 |
state.shared.schedulingMode | Uint8Array | 1 |
state.shared.shadowMap | object | 3 |
state.shared.shadowMap.data | Uint8Array | 14745600 |
state.shared.shadowMap.height | number | |
state.shared.shadowMap.width | number | |
state.shared.sim | object | 20 |
state.shared.sim.cellIds | Uint32Array | 14745600 |
state.shared.sim.chunkHeight | number | |
state.shared.sim.chunkShouldUpdate | Uint8Array | 9216 |
state.shared.sim.chunkShouldUpdateNext | Uint8Array | 9216 |
state.shared.sim.chunkSize | number | |
state.shared.sim.chunkWidth | number | |
state.shared.sim.damagedGround | object | 2 |
state.shared.sim.elementCapacity | number | |
state.shared.sim.elementData | object | 25 |
state.shared.sim.elementSlabsExhausted | Uint32Array | 1 |
state.shared.sim.freeDamagedIdCount | Uint32Array | 1 |
state.shared.sim.freeDamagedIds | Uint32Array | 999000 |
state.shared.sim.height | number | |
state.shared.sim.idStats | Uint32Array | 420 |
state.shared.sim.liveElementCount | Uint32Array | 1 |
state.shared.sim.nextDamagedId | Uint32Array | 1 |
state.shared.sim.nextElementSlab | Uint32Array | 1 |
state.shared.sim.overflowPool | Uint32Array | 100001 |
state.shared.sim.terrainType | Uint8Array | 1001 |
state.shared.sim.width | number | |
state.shared.wallData | object | 4 |
state.shared.wallData.data | Uint8Array | 14745600 |
state.shared.wallData.height | number | |
state.shared.wallData.paletteData | Uint8Array | 1020 |
state.shared.wallData.width | number | |
state.shared.waterPresenceZones | Uint8Array | 14400 |
state.shared.waterPresenceZonesHeight | number | |
state.shared.waterPresenceZonesWidth | number | |
state.shared.workQueue | object | 14 |
state.shared.workQueue.chunkCount | number | |
state.shared.workQueue.chunkDone | Uint8Array | 9216 |
state.shared.workQueue.chunkHeight | number | |
state.shared.workQueue.chunkWidth | number | |
state.shared.workQueue.columnCosts | Float32Array | 768 |
state.shared.workQueue.columnDone | Uint8Array | 96 |
state.shared.workQueue.counters | Uint32Array | 4 |
state.shared.workQueue.effectiveMode | Uint8Array | 1 |
state.shared.workQueue.evenTasks | Uint32Array | 9216 |
state.shared.workQueue.hotColumns | Uint8Array | 96 |
state.shared.workQueue.hotRemaining | Int32Array | 96 |
state.shared.workQueue.oddTasks | Uint32Array | 9216 |
state.shared.workQueue.taskCounts | Uint32Array | 2 |
state.shared.workQueue.workerCount | number | |
state.shared.workerCompletion | Float32Array | 16 |
state.shared.workerDetailEnabled | Uint8Array | 1 |
state.shared.workerDetailPerformance | Float32Array | 104 |
state.shared.workerPerformance | Float32Array | 32 |
state.storesaved with the world: resources, structures, progression143
| path | type | size |
|---|---|---|
state.store | object | 34 |
state.store.achievements | object | 14 |
state.store.achievements.ANCIENT_TECHNOLOGY | boolean | |
state.store.achievements.AURA_EXTRACTOR | boolean | |
state.store.achievements.CONSERVATORY_CURATOR | boolean | |
state.store.achievements.CRITTER_CATCHER | boolean | |
state.store.achievements.FINAL_SCHEMATIC | boolean | |
state.store.achievements.FIRST_BLOOM | boolean | |
state.store.achievements.FIRST_SPARK | boolean | |
state.store.achievements.FLUX_IT_UP | boolean | |
state.store.achievements.FULL_CIRCLE | boolean | |
state.store.achievements.GOLD_IN_THE_BIN | boolean | |
state.store.achievements.PROMOTION_PROTOCOL | boolean | |
state.store.achievements.QUARTERMASTER | boolean | |
state.store.achievements.STRATACORE_SECURED | boolean | |
state.store.achievements.VOID_TOUCHED | boolean | |
state.store.conservatory | object | 1 |
state.store.conservatory.tickets | number | |
state.store.createdVersion | string | |
state.store.creatures | object | 5 |
state.store.creatures.eyes | object | 2 |
state.store.creatures.lumling | object | 2 |
state.store.creatures.resinWeaver | object | 2 |
state.store.creatures.shinelet | object | 2 |
state.store.creatures.voidgrazer | object | 2 |
state.store.discoveries | object | 2 |
state.store.discoveries.elements | array of number | 45 |
state.store.discoveries.terrains | array of number | 21 |
state.store.drones | array of object | 1 |
state.store.factoryLevelCap | null | |
state.store.gloom | object | 1 |
state.store.gloom.emitterPositions | array | 0 |
state.store.hints | object | 6 |
state.store.hints.angleLockShown | boolean | |
state.store.hints.conservatoryIntroSeen | boolean | |
state.store.hints.freezingIceSoilFire | boolean | |
state.store.hints.hoverIntroToastSeen | boolean | |
state.store.hints.nonDestructible | boolean | |
state.store.hints.upgradesIntroSeen | boolean | |
state.store.integrity | object | 2 |
state.store.integrity.cheatsUsed | boolean | |
state.store.integrity.modsUsed | boolean | |
state.store.lockedTechs | object | 2 |
state.store.lockedTechs.aurixiteCrystallizer | boolean | |
state.store.lockedTechs.voidOrb | boolean | |
state.store.machineryEngine | object | 1 |
state.store.machineryEngine.runLaunchers | boolean | |
state.store.meta | object | 8 |
state.store.meta.autosaveSlot | number | |
state.store.meta.elementCapacity | number | |
state.store.meta.nextId | object | 3 |
state.store.meta.seed | string | |
state.store.meta.tick | number | |
state.store.meta.time | number | |
state.store.meta.worldId | string | |
state.store.meta.worldName | string | |
state.store.mods | object | 34 |
state.store.objectives | object | 1 |
state.store.objectives.active | array | 0 |
state.store.options | object | 6 |
state.store.options.buildMode | number | |
state.store.options.buildModeIndices | object | 5 |
state.store.options.defaultFilter | object | 2 |
state.store.options.sandkitPlacementConfig | object | 1 |
state.store.options.shortcutHelperVisible | boolean | |
state.store.options.techTreePan | object | 2 |
state.store.owner | undefined | |
state.store.pipes | array of object | 413 |
state.store.player | object | 17 |
state.store.player.action | object | 2 |
state.store.player.buildings | array of number | 61 |
state.store.player.cooldowns | object | 3 |
state.store.player.grapplingHook | boolean | |
state.store.player.height | number | |
state.store.player.hotbar | object | 3 |
state.store.player.inventory | array of object | 32 |
state.store.player.isHovering | boolean | |
state.store.player.onGround | boolean | |
state.store.player.speedCapOverdrive | object | 2 |
state.store.player.tech | object | 71 |
state.store.player.threshold | object | 2 |
state.store.player.velocity | object | 2 |
state.store.player.weaponsMeta | object | 1 |
state.store.player.width | number | |
state.store.player.x | number | |
state.store.player.y | number | |
state.store.productionPoints | number | |
state.store.progression | object | 2 |
state.store.progression.dungeons | object | 1 |
state.store.progression.upgradesUnlocked | boolean | |
state.store.projectiles | array | 0 |
state.store.pumpsCache | array of object | 51 |
state.store.queue | array | 0 |
state.store.resources | object | 4 |
state.store.resources.artifacts | object | 2 |
state.store.resources.energy | number | |
state.store.resources.fluxite | number | |
state.store.resources.gold | number | |
state.store.scene | object | 3 |
state.store.scene.active | number | |
state.store.scene.start | number | |
state.store.scene.triggers | array of object | 3 |
state.store.stratacores | array of string | 1 |
state.store.structures | array of object | 35956 |
state.store.tutorial | object | 4 |
state.store.tutorial.active | boolean | |
state.store.tutorial.currentStep | number | |
state.store.tutorial.data | object | 0 |
state.store.tutorial.transitioningToNextStep | boolean | |
state.store.upgrades | object | 16 |
state.store.upgrades.cryoblaster | object | 1 |
state.store.upgrades.digger | object | 4 |
state.store.upgrades.drill | object | 1 |
state.store.upgrades.flamethrower | object | 2 |
state.store.upgrades.grabber | object | 3 |
state.store.upgrades.gun | object | 5 |
state.store.upgrades.hauler | object | 2 |
state.store.upgrades.implosionGun | object | 1 |
state.store.upgrades.jetpack | object | 2 |
state.store.upgrades.laser | object | 3 |
state.store.upgrades.locator | object | 1 |
state.store.upgrades.rocketLauncher | object | 4 |
state.store.upgrades.shovel | object | 3 |
state.store.upgrades.sweeper | object | 3 |
state.store.upgrades.thruster | object | 1 |
state.store.upgrades.vacuum | object | 2 |
state.store.version | string | |
state.store.viability | object | 3 |
state.store.viability.goldSpent | number | |
state.store.viability.level | number | |
state.store.viability.peakEnergy | number | |
state.store.world | object | 10 |
state.store.world.deferredChunkReports | array | 0 |
state.store.world.fixtures | array of object | 70 |
state.store.world.groundHorizon | array of number | 3840 |
state.store.world.horizon | array of number | 3840 |
state.store.world.lights | array of object | 561 |
state.store.world.matrixTraverseDirection | number | |
state.store.world.sensors | array of object | 1 |
state.store.world.size | object | 2 |
state.store.world.teleportZones | array of object | 306 |
state.store.world.updatedElementIndices | array | 0 |
state.store.worldItems | array of object | 1 |
apiVersion, and not even stable within one. This is a description of one world on one build. Read from it and a patch makes a mod wrong; write to it and a patch makes a save wrong. Every access wrapped, every read checked.Traps
Collected the expensive way, across eight mods.
Entries are scripts, not modules
An import or export throws Unexpected token 'export' and the entry silently never runs. Nothing in the UI says so.
Write scene gating as a hide-list
Injected UI sits in the global overlay slot, draws above the main menu, and stays mounted after leaving a world. Suppress on scenes you positively recognise as menus — a wrong guess in a show-list blanks your UI everywhere.
Never wrap registration in a quiet helper
A silent try/catch around energy.registerType can hide a total failure. Registration cannot be retried, so it has to fail loudly.
Validate before launching the game
workshop-mods.js and local-mod-publisher.js can be pulled out of app.asar and called directly: loadCandidate for the manifest and entries, validatePreview for the image, and the same new Function wrapper for syntax. Catches everything but runtime behaviour, with no restart.
Effect callbacks differ per entry point
energy.addAtCell reads onPathCharged but never calls onSourceAdded — that one only fires from the batch API, which mods do not get. It returns the amount the network accepted, which is the right thing to gate feedback on.
A mod appears in Options only if it has settings
No non-empty configSchema, no entry in the mods screen — and the game has no built-in way to disable a mod without unsubscribing, so an enabled field is worth making a convention.