THE MAP MAKER’S GUIDE · MAPMODS

A little life.
On every mountain.

Write a behaviour once, let several maps use it, and distribute its resources separately. Start with a small Lua exercise, then explore the animated foxes that live in Veymont.

Tools & game loader · 1.20.6+Two downloadable source projectsEnglish · 13 October 2026
A fox in the snowy Veymont forest, captured in the current game through UE4SS.
Forest foxes 0.4.0 on Veymont 0.7.6, with Custom Map 1.20.7. A new engine capture requested through UE4SS in an offscreen game session. Model: npbehunin, CC BY 4.0; adapted by Albatar (Élie Millon).

STEP 01

Two projects, one experience

A mapmod is a custom-mod.json project with runtime: "map-api". The loader runs it inside a dependent map’s session. The mod owns its scripts and assets; the map session owns the lifetime of its callbacks, timers, actors and saves.

You need Python 3.10+, pipx and matching tools/game loader 1.20.6 or later. Install Lua 5.4 with luac for syntax checks. Authoring needs no running game; Play needs Ski-E-O! or its Steam demo on Windows or Linux/Proton.

In the workshop, Help → Mapmod tutorial opens this guide (editor 1.20.8 or later).

pipx install --force --index-url https://skieomap.emillon.fr/downloads/pypi/ skieo-custom-map
skieomod --version
skieomod setup

If setup is already complete, close the game and use skieomod update instead. If the public feed is older than the required version, install the matching prepared wheel and use skieomod update --archive CustomMap-Mod-VERSION.zip.

Practice: Mountain life

A small mapmod with a configurable message and a saved field-report counter, plus a starter map that depends on it. This exercise writes to the log; it does not create animals. Extract both folders and work from their shared parent directory.

Recreate the projects in empty folders
skieomod create mapmod --output mountain-life --id tutorial.mountain-life --name "Mountain life" --author "Your name"
skieomod create map --output practice-mountain --id tutorial.practice-mountain --name "Practice mountain" --author "Your name"

create mod creates a startup UE4SS mod. Choose create mapmod for this map-scoped workflow. The template creates commented scripts and local authoring guides.

Before publishing your own derivative, replace tutorial.mountain-life with a stable namespaced ID such as yourname.mountain-life. Update dependency and extension keys wherever they refer to it.

STEP 02

Tell the loader what belongs to your mod

Open mountain-life/custom-mod.json. This complete minimal manifest selects Lua API 1.4 and gives the script access to its own welcome text. The download also declares a licence file.

{
  "format": {"name": "skieo-mod", "version": "1.0.0"},
  "id": "tutorial.mountain-life",
  "metadata": {
    "name": "Mountain life", "version": "0.1.0",
    "authors": ["Your name"],
    "description": "Configurable field reports for a living mountain.",
    "language": "en", "license": "MIT"
  },
  "runtime": "map-api",
  "dependencies": [],
  "script": {
    "language": "lua", "api": "1.4", "entry": "Scripts/main.lua",
    "permissions": ["gameplay"]
  },
  "files": ["Scripts/main.lua", "Data/welcome.txt"],
  "content": {"assets": ["Data/welcome.txt"]}
}
Three versions, three purposes
FieldMeaning
format.versionThe package format.
script.apiThe Lua API used by the script.
metadata.versionYour mod’s release version. Increase this when you distribute a change.

files lists everything packaged, including the entry point. content.assets allows the script to read selected resources. Extra data, textures and Lua modules must appear in both lists. Paths are relative, use / and must match exact spelling.

Create Data/welcome.txt containing:

The mountain is alive: listen for wildlife between the trees.

Only declared content enters the runtime archive. Authoring guides, editable model sources and the preview map can stay in the source project.

STEP 03

Wait for the map, then register your behaviour

Replace mountain-life/Scripts/main.lua with this entry point, also available as a downloadable Lua file. The loader calls the returned function once per map session.

return function(api)
    local options = api:mapmod_options()
    if options.enabled == false then return end
    local label = type(options.label) == 'string' and options.label:sub(1, 80)
        or 'Mountain life'
    local welcome = api:read_asset('Data/welcome.txt'):gsub('%s+$', '')

    api:on('map.ready', function()
        local state = api:mod_state({reports = 0})
        api:log(label .. ': ' .. welcome)
        api:log('Saved field reports: ' .. state.reports)

        -- Timers follow the map session; explicitly respect the game's pause.
        api:every(30000, function()
            local environment = api:environment()
            if environment.paused or environment.ready == false then return end
            state.reports = state.reports + 1
            api:log(string.format('%s: field report %d, local hour %.1f',
                label, state.reports, environment.hour or 0))
        end)
    end)
end

map.ready runs after terrain/station initialization, including save restoration. api:every uses real milliseconds; this callback skips reports while the game is paused or not ready. Messages appear in UE4SS.log, in the installed UE4SS directory; they do not open a player-facing notification.

api:mod_state stores JSON data under this mod’s ID in the map’s native save. Loading that save restores the counter. Starting a new game begins at zero. Keep functions and actor handles out of saved state; fill missing fields when you change its schema. Reserve the map’s main api:state for map data.

The loader cancels these timers and callbacks on unload. Register map.unloading for additional cleanup your own resources need, and use the API for session work.

STEP 04

Let a map request your mod

In practice-mountain/custom-map.json, replace the empty dependencies array and add this top-level extensions object. These are fragments to merge; preserve the map’s terrain, scripts, build settings and other dependencies.

"dependencies": [
  {"id": "tutorial.mountain-life", "version": "=0.1.0"}
],
"extensions": {
  "tutorial.mountain-life": {
    "enabled": true,
    "label": "Practice mountain"
  }
}

Dependency scripts register before the map’s script. api:mapmod_options() gives the mod a copy of the extension named after its ID. Each map can set a different label or disable reports. Validate your options and provide defaults because a map may omit its extension.

The dependency connects the packages. Keep the mod’s scripts and assets in its .skieomod; do not copy them into the map or declare them in the map’s content.assets.

STEP 05

Build separately, test together

Run these commands from the parent of both project folders:

skieomod build mountain-life --check-scripts
skieomod build practice-mountain --dependency mountain-life --check-scripts
skieomod test practice-mountain --dependency mountain-life --check-dependencies --check-scripts
skieomod run practice-mountain --dependency mountain-life

Remove --check-scripts if luac is unavailable. Build validates and packages files. Test validates the package and dependency closure; runtime behaviour still needs a game session.

Checkpoint: the mod’s bin/ contains tutorial.mountain-life-0.1.0.skieomod, and the map’s contains tutorial.practice-mountain-0.1.0.skieomap. The map archive has no Data/welcome.txt. The game log prints the welcome, Saved field reports: 0, then a first field report after roughly 30 unpaused seconds.

  1. Pause for more than 30 seconds: no new report should be counted while paused.
  2. Save, leave and reload that save: the counter resumes.
  3. Set enabled to false, rebuild and start a new session: no reports are registered.

--dependency accepts a local mod project or archive and can be repeated. Build/Test dependency checks are offline. run installs the packages separately; it can acquire missing published mapmods from the project Featured catalogue.

Test just the mod
skieomod run mountain-life

The CLI generates a private test map in mountain-life/obj/mapmod-preview, with an exact dependency on this mod. Its default options use the “Mountain life” label. Distribute the mod, rather than this generated preview.

STEP 06

Put a fox on your mountain

Forest foxes · source project 0.4.0

The real example: behaviour, gaze, baked animations, paw contacts, textures, source rig and converter. It includes credits and licences; the original anatomy download stays external.

Extract foxes/ beside the practice map. Add {"id":"community.foxes","version":"=0.4.0"} to the map’s dependency array, keeping Mountain life if you want both. Merge this object into its existing extensions:

"community.foxes": {
  "count": 1,
  "seed": 7521,
  "habitats": [{
    "forest": {"x": -6000, "y": 0, "radius": 1800},
    "meadow": {"x": 6000, "y": 0, "radius": 1800},
    "crossing": [{"x": -2500, "y": 0}, {"x": 2500, "y": 0}]
  }]
}
skieomod build foxes --check-scripts
skieomod run practice-mountain --dependency mountain-life --dependency foxes

These coordinates and radii are centimetres, unlike the vegetation JSON’s metres. This example fits the starter terrain. On your map, choose forest and meadow habitat away from busy lifts, buildings and cliffs. Put the first crossing bank on the forest side, the second on the meadow side, both outside a piste. A forest circle does not create trees or a piste.

Checkpoint: press F7 to centre on a fox; press it again to visit the next one. Click the fox for its discovery panel. Check snow, slopes, running, nighttime and pause. Start with one animal, then spread habitats across the map.

STEP 07

Learn from the animated example

Where to make a change
SourceResponsibility
Scripts/behaviour.luaGoals, solitary territories, hunting, fear, play, dispersal and gait timing.
Scripts/gaze.luaNearby eye targets and smooth return to neutral.
Scripts/main.luaAPI registration, saved checkpoints, rendering, F7 and selection.
Assets/fox.skanMesh sections and baked vertex-animation clips.
Assets/fox-ground.jsonPaw soles, leg anchors and gait/contact data.
Assets/fox-eyes.jsonEyeball groups and aiming limits.
tools/prepare_fox.pySource geometry conversion, weighted rig, poses, snow textures and runtime rig generation.

The original Fox Anatomy model has anatomical display meshes, but no usable animation armature or clips. The converter keeps skin and eyeballs and adds a sixteen-joint weighted rig with hips, spine, legs, head and a segmented tail. Pupils are texture details; gaze rotates the separate eyeball geometry.

Refine the fox’s baked poses

Obtain the credited original Collada ZIP. Inside the downloaded foxes/ folder, run:

python3 -m pip install numpy Pillow
python3 tools/prepare_fox.py /path/to/fox-anatomy.zip --output Assets
skieomod build . --check-scripts

Edit the converter’s rig and pose functions, then regenerate. Assets/fox-rig.json records pivots, weights and source SHA-256. This converter expects this particular model; another species needs its own geometry conversion, weights, clips and eye/contact groups. It is not a generic FBX/GLB importer. SKAN uses baked poses rather than an Unreal skeletal animation asset; no Unreal Editor is required.

main.lua calls api:spawn_animated, simulates behaviour at 10 Hz and interpolates nearby rendering at 30 Hz through api:on_frame. Distant animals cost less work. Clip transitions crossfade over 0.16 seconds, walk/trot phase follows travelled distance, and jumps follow ballistic flight. Rodents stay invisible under the snow.

Ground support uses the model’s paw soles and fresh/packed snow. Contacts stamp paw-shaped prints and play local snow crunches. The fox stays upright on sideways slopes. Tracks use a bounded visual fallback: this demo has no reflected method for writing the skiers’ private snow-track pages. Heavy snowfall builds a soft coat; shaking removes it. Movement, transitions, gaze, particles and contact sound stop during pause.

For your own animal, declare the SKAN, textures and rigs in both mod lists; wait for map.ready and environment().ready; measure elapsed seconds; separate AI from rendering; interpolate; and periodically checkpoint JSON behaviour state. Frame callbacks do not automatically serialize saves. Use handle:inspect for a discovery panel and the loader’s F7 API for debug focus. The API reference provides signatures and limits.

Code is MIT. The model and derived textures are CC BY 4.0: preserve CREDITS.txt, npbehunin’s attribution, source/licence links and a description of your changes.

STEP 08

Release the mod, then the maps

Distribute the map and each required mod separately. Sending a ZIP does not make it automatically discoverable: the distribution catalogue must advertise its ID, version and verified download.

Prepare each article independently

skieomod publish mountain-life --prepare

This stages unpacked content and a publication request in bin/workshop/; it does not upload. Replace preview.png and review the private article settings. Actual publication needs a configured native Steamworks client, which is not included.

Once the mod has a real Workshop article, add its decimal-string workshop_id to the map’s dependency before preparing that map. Steam serves the current article content and does not promise arbitrary older semantic releases. See the Workshop contract.

Keep the package ID and increase metadata.version for each update. Rebuild the mod, update dependent maps and test the full set. =0.1.0 pins a tested version; ^0.1.0 allows compatible 0.1.x releases. Published immutable archives stay unchanged. Changed save schemas need migration or explicit defaults.

When something does not work

No message or animal appears

Check runtime: "map-api", the returned registration function, the map’s dependency, the loader version and UE4SS.log. Mountain life is a log-only exercise; animals require the fox dependency and habitat.

Reading an asset is rejected

List the exact path in both files and content.assets. Lua modules are assets too. Keep paths relative with /.

The dependency cannot be resolved

Check matching IDs and versions. Supply --dependency for local unpublished mods. Remote Play requires a published compatible catalogue entry, not merely a link to an archive.

The edited script is not running

Rebuild and start or reload a map session. Editing a source file does not replace a running Lua closure.

Foxes are hard to find or move in pause

Use F7 and begin with one fox. Check centimetres and terrain bounds. Custom behaviour must check environment().paused before simulation, animation, particles and audio.

The saved counter resets

Use api:mod_state, preserve restored values and checkpoint changes outside frame callbacks. A new game intentionally begins with fresh state.

Workshop preparation rejects the dependent map

Required Workshop dependencies need real workshop_id values. Local/Featured dependencies alone do not identify a Steam article.