// 06 MODDING VERSION 2026-10-17 · 01 ON FILE
Modding guide
The living technical guide, published as the team writes it. Each section says what exists today; planned work is listed near the end. Early prototype: expect things to move.
A living guide to how ColonySim is put together and where to hook in. It's updated as systems land, so each section says what exists today. Planned work is listed at the end.
- Engine: Unreal Engine 5.8.3, C++ module
ColonySim(Source/ColonySim/) - Video memory:
Config/DefaultScalability.inicaps texture streaming at the Epic and Cinematic levels (r.Streaming.LimitPoolSizeToVRAM=1,r.Streaming.PoolSize=1500). The engine leaves those levels unlimited, and the editor plus a Standalone Game launched from it each keep a pool, which overflowed 8 GB cards (driver resets,DXGI_ERROR_DEVICE_REMOVED). The game itself peaks around 1.4 GB in the Character Designer. - Status: early prototype. Every file in
Content/Datais data-driven, and a mod can add or override its rows fromMods/<mod>/Data/. That covers terrains (section 2), rocks (section 3), plants (section 4), colonist bodies, ancestries, appearance options, skin tones, life stages, body parts, damage types and face references (section 6), jobs, needs, whims, work lists and typed-order words (section 8), social events and interactions (section 9), species words and fauna (section 10), and the planet and biomes (section 11). Mods, the in-game data editor, the Unreal Editor Mod Kit, the modding API and the multiplayer-ready design are in section 12. Everything else still means changing source or editing exposed properties in the editor.
1. World scale
Everything is measured in voxels. The constants live in Source/ColonySim/VoxelTypes.h:
| Constant | Value | Meaning |
|---|---|---|
Voxel::BlockSize | 25 cm | Edge length of one voxel |
Voxel::AgentRadius | 1 | Voxels either side of a colonist's center column, so a footprint is 3 voxels wide |
Voxel::AgentHeight | 8 | Clear voxels of headroom a colonist needs |
Voxel::AgentMaxStepUp | 1 | Highest ledge a colonist can step onto |
Voxel::AgentMaxDrop | 3 | Furthest a colonist will drop down |
The colonist's capsule size and the pathfinder both read these constants, so change them here and nowhere else.
2. Terrain types
Every voxel holds one terrain type. Terrain types are rows in a CSV file, not code:
- Base game:
Content/Data/Terrains.csv - Mods:
Mods/<YourMod>/Data/Terrains.csv
At startup the base file loads first, then each mod folder in alphabetical order. A mod row with a new name adds a terrain. A row whose name matches an existing terrain replaces that terrain's values. The log reports LogTerrain: Loaded N terrain types, plus a warning for each row it can't parse.
Columns
| Column | Meaning |
|---|---|
Name | Unique id, e.g. RichSoil. World generation and saves refer to terrain by this name |
DisplayName | Name shown to players |
Category | Natural, Stone, Water, Road, Floor, Special, or Lava |
bSolid | True = fills its voxel: walkable, collidable, part of the terrain mesh. Water is False |
MoveSpeed | Walk speed multiplier for colonists standing on it. 0 = impassable |
Fertility | Crop growth multiplier. 0 = can't farm (not used by any system yet) |
Hardness | Relative work to dig it out (not used yet) |
bSupportsHeavyStructures | Can carry heavy buildings such as concrete walls (not used yet) |
bIndestructible | True = can't be dug or removed (the Mantle) |
bHidden | True = solid but never drawn by the terrain mesher, because something else renders it (RockCore, inside rock meshes) |
TopColor | Average color on upward-facing surfaces, sRGB, as "(R=0.24,G=0.28,B=0.05,A=1.00)" |
SideColor | Average color on slopes and cliffs, sRGB |
TopLayer | Texture layer used on top surfaces (0–7, table below) |
SideLayer | Texture layer used on slopes and cliffs |
Texture layers (built by Scripts/build_terrain_material.py):
| Layer | Look | Texture | Color mode |
|---|---|---|---|
| 0 | Grass | Pack_Bonus Grass_2 | Painted |
| 1 | Dirt | Pack_Bonus Stone_2 (35% detail) | Recolored |
| 2 | Sand | Pack_Bonus Stone_2 (20% detail) | Recolored |
| 3 | Stone | Pack_Bonus Stone_1 | Painted |
| 4 | Gravel | Pack_Bonus Stone_3 | Painted |
| 5 | Mud | Pack_Bonus Stone_2 (30% detail) | Recolored |
| 6 | Ice | Pack_Bonus Stone_1 (25% detail) | Recolored |
| 7 | Planks and tiles | Pack_Bonus Wooden_Floor_1 | Painted |
- Painted layers keep the texture's hand-painted colors, shifted so the surface averages to the terrain's
TopColororSideColor. - Recolored layers use only the texture's light and dark detail. All color comes from the CSV.
Either way, the CSV color is the color you'll see on average.
Example mod (Mods/RedDesert/Data/Terrains.csv) that adds a red sand and makes vanilla mud less sticky:
Name,DisplayName,Category,bSolid,MoveSpeed,Fertility,Hardness,bSupportsHeavyStructures,TopColor,SideColor,TopLayer,SideLayer
RedSand,Red Sand,Natural,True,0.76,0.05,0.8,False,"(R=0.62,G=0.30,B=0.18,A=1.00)","(R=0.55,G=0.26,B=0.16,A=1.00)",2,2
Mud,Mud,Natural,True,0.65,0.00,0.8,False,"(R=0.24,G=0.18,B=0.12,A=1.00)","(R=0.22,G=0.16,B=0.11,A=1.00)",5,5
Nothing places a new terrain in the world yet; world-generation hooks are planned. Overrides of existing terrains take effect right away.
Limits: 255 terrain types in total, including mods. Terrain ids (the uint8 stored per voxel) are assigned in load order, so never save ids; save names.
Launch terrain set (63 types): 11 natural soils and ground types, 5 rock types, 6 water types, 8 flow states (FlowingWater1..7 and FallingWater, written only by the water simulation, see "Flowing water" below), 2 road surfaces, 13 constructed floors, 16 special floors, the Mantle, and RockCore (the hidden voxels inside rock meshes, section 3).
3. World generation and meshing
AVoxelWorld (VoxelWorld.h/.cpp) owns the voxel grid and its meshes.
Editable properties (place a VoxelWorld actor in your level and edit it in the Details panel; the game mode uses a placed world instead of spawning its own):
| Property | Default | Notes |
|---|---|---|
SizeX, SizeY | 256 | District footprint in voxels (256 = 64 m). Set per game from the new game screen (64–1024) |
SizeZ | 192 | District depth in voxels, mantle included (192 = 48 m) |
MantleThickness | 4 | Voxels of indestructible Mantle at the bottom |
CaveAmount | 1.0 | 0 = solid underground, 1 = default, 2 = very hollow |
WaterTableOffset | -3 | Water table relative to the district's average ground, in voxels. Lower = fewer, smaller lakes. The district's biome adds to it (section 11) |
HeightScale | 1.0 | Ground height variation; set from the district's hilliness (0.55 flat to 2.2 mountains) |
SurfaceTerrainSwap | empty | Surface terrain substitutions by name, from the district's biome (e.g. Soil=Sand) |
WaterMaterial | /Game/Terrain/M_Water | Water surface material; leave empty for the default |
ChunkSize | 32 | Voxels per chunk side; one mesh per chunk |
VisibleDepth | 16 | Voxels below a chunk's lowest surface that get meshed; deeper ground is meshed on demand |
CutawayDepth | 40 | Voxels below the cutaway slice that the cutaway view meshes |
Seed | 1337 | Same seed, same world |
Material | /Game/Terrain/M_Terrain | Leave empty to use the default terrain material |
Generation stages (AVoxelWorld::Generate, GenerateShape). The approach follows Minecraft's 3D density terrain; see Docs/Research/MinecraftTerrainAndWater.md.
- Shape. A 3D density function decides solid (above 0) or air for every voxel:
- Ground: base height per column from three Perlin noise layers (scales 90, 35 and 12 voxels, averaging 62% of
SizeZ). Density falls off with height above it. A 3D noise (scale 28) nudges it so the surface can overhang. - Caverns ("cheese"): a vertically squashed 3D noise (scale 52) opens wide pockets where it dips. They're kept closed within 16 voxels of the surface.
- Tunnels ("spaghetti"): two 3D noises (scale 40). Where both are near zero they trace winding tubes, wide enough for a colonist.
- Entrances: tunnels break the surface only where a 2D entrance noise allows it, and never within 24 voxels of the spawn.
- Pillars: a noise that's tall in Z and thin in X/Y puts stone columns back inside caverns.
- Floor: caves stay out of the 6 voxels above the mantle.
- Sampling: density is sampled every 4 voxels and interpolated in between.
- Threads: shape sampling, interpolation and the per-column stages (surface rules, lakes, aquifers, depth classes) run in parallel, one slice or row of columns per task. Results are identical to single-threaded runs.
- Timing: a whole 256×256 district generates in about 1.3 s; 975×975 takes about 6 s.
- Ground: base height per column from three Perlin noise layers (scales 90, 35 and 12 voxels, averaging 62% of
- Surface rules. Each column's topmost solid run gets a surface terrain in its top 4 voxels, picked from its height relative to the water table, moisture, patch noise and steepness:
- Cliffs (a 3+ voxel drop to a neighbor): bare rock, above and below water
- Under the lake:
Sandin the shallows (less than 3 voxels deep),Muddeeper - Shore (1–2 voxels above water):
Marsh(very wet),MarshySoil(wet) orSand - 3–5 voxels above water:
Mud(wet),Sand(dry) orSoil - 16+ above the district average:
StonySoilwithGravelpatches - Elsewhere:
Soil, withRichSoilwhere it's wet,LichenSoilwhere it's dry, andGravelpatches Everything else is rock: - Regional rock (
Granite,Slate,Limestone,SandstoneorMarble) from a large-scale noise. - Deep
Slatebelow about a third of the way down. Mantlein the bottomMantleThicknessvoxels. Cave floors, walls and overhang undersides are rock.
- Water (placed at generation as still sources; it flows once something opens a way, see "Flowing water" below):
- Lakes: every column's air between its ground and the water table fills with water. The table sits
WaterTableOffsetvoxels from the district's average ground (-3 by default). - Flooded caves (aquifers): the underground is divided into cells of 64×64×48 voxels, each with a random center that is dry or flooded to its own level:
- Caves under or beside lakes fill to the lake level.
- Deeper caves flood more often.
- Each cave voxel follows its nearest center.
- Where two neighboring centers disagree (different levels, or wet against dry), the voxel becomes a thin rock barrier, so no pool sits against air it would spill into.
- Depth classes: each vertical run of water up to 3 voxels (75 cm) deep becomes
ShallowWater, which colonists wade at 30% speed. Deeper runs becomeDeepWater, which is impassable. Streams, rivers and oceans need flow and aren't generated yet.
- Lakes: every column's air between its ground and the water table fills with water. The table sits
- Nothing floats: a flood fill through solid voxels from the mantle removes every piece of rock that isn't connected to it. Overhang noise and cave carving occasionally leave such pieces; about 800 voxels per district at the defaults.
- Rocks (meshes, see "Rocks" below): surface boulders, pebbles, stalagmites on cave floors, stalactites on cave ceilings, and boulders buried inside the ground.
- Plants last (section 4), never in water or on rocks. A 12-voxel radius around the world center stays clear of boulders and of plants that block movement, for the spawn. Colonists never spawn in water.
Log lines report:
LogVoxel: Generated ... caves are N% of the underground: generation time and how hollow the underground came out (about 27% at the defaults).LogVoxel: Water table at Z=...: lake, flooded-cave and barrier voxel counts.LogVoxel: Removed N floating voxels.LogRocks: Placed rocks: ...: counts per placement and total core voxels.
Rocks (RockLayerComponent.h/.cpp). Rocks are static meshes, listed in Content/Data/Rocks.csv (generated by Scripts/gen_rocks_csv.py; mods add or override rows in Mods/<Mod>/Data/Rocks.csv):
| Column | Meaning |
|---|---|
Name | Unique id |
Mesh | Static mesh path, e.g. /Game/RockEnv_Pack/Meshes/Rocks/SM_Rock_1.SM_Rock_1 |
Placement | Surface (boulders), Pebble (decoration), CaveFloor (stalagmites), CaveCeiling (stalactites, mesh flipped), Buried |
MinScale, MaxScale | Random uniform scale range |
Weight | Relative chance among rocks with the same placement |
Sink | Fraction of the rock's height sunk into the ground (or ceiling) |
How a rock occupies the voxel grid. A rock is bigger to the eye than to the terrain:
- Core voxels: every voxel whose 8 corners are all inside the mesh's convex collision hull becomes
RockCoreterrain. It's solid for pathing, support, collapse and digging, butbHidden, so the terrain mesher never draws it; the rock mesh shows instead. - Blocked voxels: every voxel whose center is inside the hull is marked blocked. The pathfinder treats it like a wall, so colonists don't plan routes through the visible rock.
- Pawn collision: colonists collide with the mesh's own (convex) collision, its outer shape.
- Pebbles have no core voxels and no collision.
Placement rules: a rock is rejected if it would overlap another rock, water or the mantle, or if it has nothing solid to rest on (or hang from). Buried rocks must sit completely inside solid ground; they appear when you dig to them or slice through them in the cutaway view.
Rocks at runtime:
- Dig tool: used on a rock, or on its core voxels from above, it mines the whole rock. The core voxels become air, and anything that rested on it gets the collapse check.
- Collapse: a collapse that drops a rock's core voxels moves the rock mesh with them.
- Cutaway view: rocks sitting above the slice are hidden.
- Hover readout: shows the rock's name.
Water rendering. Water voxels aren't part of the terrain mesh. Each chunk has a separate water surface mesh:
- One quad sits just below the top of every water voxel that has open air above it, at its flow height: sources and falling water fill the voxel, flowing water
amount / 8of it. Its corners slope like Minecraft's (calculateAverageHeight): each corner is the average of the up to four water cells around it (full ones weighted 10, open air counts as empty, banks are left out, and water above any of them makes the corner full), so flow slopes from its source to its edge while still lakes stay flat. Colours blend the same way: each corner mixes the water around it (a 4x4 tent), so shallow water fades into deep water over a couple of voxels instead of stepping along voxel squares. - Side faces are drawn where water meets open air (waterfalls, the front of a flow) or lower water (the step between a source and the flow it feeds).
- Quads extend half a voxel under neighboring banks, so the smooth terrain hides the edges.
- Vertex color is the water terrain's
TopColor, and alpha sets opacity: shallow 0.7, deep 0.9. - The material is
/Game/Terrain/M_Water, built byScripts/build_water_material.py. It's translucent, and itsShoreFadeparameter (8 cm) softens the waterline without hiding shallow water. If the asset doesn't exist, water draws as an opaque blue placeholder. - Water has no collision. The cursor lands on the bed, and the hover readout reports the water and its depth.
- In the cutaway view, water reaching the slice is capped there, so flooded caves show up.
Water and colonists:
- A colonist can stand in water when there's solid ground right under its feet.
- The water's
MoveSpeedreplaces the ground's. DeepWater(0) blocks pathing.- Flowing water (
FlowingWater1..7) is waded at 45%, falling water at 30%.
Flowing water (ColonyWaterSubsystem.h/.cpp, UColonyWaterSubsystem) behaves like Minecraft's (Java Edition FlowingFluid / WaterFluid, and the Minecraft wiki's Water and Fluid pages):
- Levels: a source is full water: every water voxel from generation, and any new source. It never drains. Flowing water has an amount from 7 (next to a source) down to 1. Each voxel sideways loses
DropOff(1), so on flat ground water reaches 7 voxels, in a diamond. Falling water has water right above it: it's full height, and when it lands it spreads at 7. - Updates run every
TickSeconds(0.25 game seconds, Minecraft's 5 ticks), so they follow the game speed and stop while paused. Only cells next to a change update: digging, building, collapses and mined rocks wake the cells around them through the event bus (UColonyEvents::OnWorldEdited), and every water change wakes its neighbours for the next step. Generated lakes and caves sit still until something opens them. - Each update, in Minecraft's order:
- The cell's amount is recomputed. Water above makes it falling. Two or more horizontal sources, with solid ground or a source below, make it a new source (infinite water,
InfiniteSources). Otherwise it's the highest horizontal neighbour's amount minus one, and dry at 0, so a flow dries up back toward a removed source. - Down first: it flows into air or flowing water below, as falling water. Downward flow has no limit.
- Sideways: a source, or water that can't fall, spreads at amount - 1. It goes only toward the nearest drop that sideways flow could reach within
SlopeDistance(4) voxels; with no drop in reach, every open side.
- The cell's amount is recomputed. Water above makes it falling. Two or more horizontal sources, with solid ground or a source below, make it a new source (infinite water,
- Water isn't conserved, as in Minecraft: a lake drains into a hole you dig without getting lower.
- Flow isn't a world edit (
AVoxelWorld::SetWaterVoxelrebuilds only the fluid surfaces and records nothing). TheWatersave section keeps every cell the flow has written that still holds water or lava, with its terrain id, so a save, or a join, resumes the flow exactly where it was; those cells wake on load. Sources a player places, and lava hardened into stone, are ordinary world edits. Flow follows fixed rules, so every machine runs it; only the host (ColonyNet::HasSimAuthority) hardens lava, removes plants, pushes colonists and burns them. Water writes bumpAVoxelWorld::GetFluidVersion, not the terrain version (they never change where colonists can stand); lava writes bump the terrain version. - Budget: one step per frame at most, and each step stops after
MaxStepMs(2 ms) of game-thread time (and atMaxUpdatesPerTickcells); the rest wait for the next step, so a big flood spreads more slowly instead of costing frames. - Plants: flowing or falling water entering a voxel washes away the plant standing in it when its category is in
WashAway(Wild;Decorativeby default) and it doesn't block movement. Trees, crops and stumps stay, so a stream never takes a field. Removals are world edits, like any other. - Colonists: standing in flowing water, a colonist drifts along it at
PushSpeed(40 cm/s), toward lower water and drops (Minecraft'sgetFlow), with a sweep so it never passes through a bank. Possessed and downed colonists aren't pushed: a downed one lies still where it fell. Data/Water.csvKey,Value:TickSeconds,SlopeDistance,DropOff,InfiniteSources,MaxUpdatesPerTick(20000),MaxStepMs(2),WashAway,PushSpeed. Mods override any value.- Lava (
Lava,FlowingLava1..7,FallingLavain Terrains.csv, categoryLava) flows by the same rules with Minecraft's Overworld numbers (LavaFluid): a step every 1.5 s,DropOff2 (a source reaches 3 voxels: amounts 6, 4, 2),SlopeDistance2, and no infinite sources. It's drawn with the water surfaces, opaque, and it's never walkable (VoxelPathfinder); colonists don't spawn in it.- Lava meets water (
LiquidBlock.shouldSpreadLiquid,LavaFluid.spreadTo): water beside or above lava hardens a lava source intoObsidianand flowing or falling lava intoCobblestone, on the next step. Lava falling onto water turns that water intoStone. Lava never flows into water, and water never flows into lava. So a stream poured onto a lava pool crusts it over, and lava running into a lake makes a stone shelf. The three blocks are ordinary solid terrain (world edits, so saves and multiplayer carry them). - Burns: lava entering a voxel burns the plant in it (
BurnAway, every category by default, trees too:BurnBlocking). A colonist standing in lava takesBurnDamage(10)Burndamage everyBurnInterval(0.5 s), like Minecraft'slavaHurt, downed or not. Data/Lava.csvKey,Value:TickSeconds,SlopeDistance,DropOff,InfiniteSources,BurnAway,BurnBlocking,BurnDamage,BurnInterval,CaveLavaChance. For Nether-style fast lava, use 0.5, 4 and 1.- Generation (
AVoxelWorld::GenerateTerrainstage 3d; districts with these land-feature tags, from the seed):LavaLake: a crater pool 6-9 voxels across and up to 3 deep, with a rock bank;LavaFlow: 1-3 sources on the highest ground found, which start pouring downhill when the game starts;LavaCaves:CaveLavaChance(30%) of deep flooded cave pockets (more than 24 below the surface) hold lava, walled off from water pockets: about 10% of the caves. Surface lava stays at least 64 voxels from the spawn point and away from water. Plants and rocks never spawn in it.-ColonyLavaturns all three on for any map, for testing.
- Placing lava by hand: the dev panel's Paint terrain (the top voxel becomes a lava pool) or Raise (a source on top) with
Lava.
- Lava meets water (
- Console:
colony.Water.Enabled 0freezes water and lava where they are;colony.Water.Statslogs pending cells, updates so far, lava cells hardened, cells in the save and the last step's time. - Test hook:
-ColonyDevTest -ColonyWaterTestbuilds a floating 15×15 stone basin and checks the rules, logging PASS or FAIL for each:- one source spreads exactly 4, 8, 12 ... 28 cells of amounts 7 to 1;
- without its source the flow dries up;
- a gap between two sources becomes a source;
- flowing water washes away a small wild plant;
- water falls through a hole in the floor;
- flow heads toward the hole;
- in the emptied basin, a lava source spreads 4, 8 and 12 cells of amounts 6, 4 and 2, and burns a tree;
- a gap between two lava sources stays flowing;
- water beside the lava source makes obsidian, beside flowing lava cobblestone;
- lava falling onto water makes stone. It then quits; add
-ColonyWaterTestShotsto keep it running, point the camera at the basin and screenshot it.
- Not yet:
- Lava glowing (it's lit like any surface), setting fire to things near it, and the hiss and steam where it hardens.
- Washed-away crops dropping their yield (crops don't wash away for now).
The mantle is indestructible: the Mantle terrain has bIndestructible = True. AVoxelWorld::CanDig refuses it, and so does the dev panel's Dig tool. Below Z = 0 the world reports mantle, so nothing ever meshes under it.
Cutaway view. SetCutawayZ(Z) shows the district sliced at height Z, so caves can be seen from above.
- Controls: PageDown and PageUp move the slice by 4 voxels, Home clears it, or use the dev panel.
- It's display only. The real terrain keeps its collision and pathing; a second set of display meshes shows the slice and answers cursor traces, so hovering and the dev tools act on what you see.
- Plants and colonists above the slice are hidden.
Runtime edits: AVoxelWorld::SetTerrain(X, Y, Z, TerrainId) changes one voxel and rebuilds every chunk within 2 voxels of it, because that's how far one voxel can affect the smooth surface. Look up ids with GetTerrains().FindId("Granite").
- Batches: call
SetTerrain(..., false)for each voxel, then rebuild the region once. RebuildRegion(X0, Y0, X1, Y1)rebuilds the touched chunks right away, one after another.MarkRegionDirty(X0, Y0, X1, Y1)only marks them dirty (same 2-voxel margin). Next frame,FlushDirtyChunks()rebuilds every dirty chunk together: terrain (and cutaway) geometry for all of them in oneParallelFor, then water the same way. Several digs in one frame rebuild each chunk once. Digging uses this. The flush schedules itself (a next-tick timer); callFlushDirtyChunks()yourself if you need the mesh at once.- Every write keeps the column heightmap (
GetSurfaceZ) up to date, so it's always current, even before the rebuild.
Collapse. When voxels are dug out, AVoxelWorld::CollapseUnsupported(DugCells, Min, Max) checks every solid piece touching them:
- The check: a depth-first search through solid neighbors that tries "down" first. A piece that reaches the mantle is supported.
- When it fails: a piece that can't reach the mantle drops straight down as one rigid piece, by the largest distance it can fall before any part lands. Plants it lands on or leaves behind are removed.
- Limit: pieces bigger than 60,000 voxels are assumed supported for now. Real structural rules (span, load, the design doc's pillar support radius) come later.
- Callers: the Dig tool calls it automatically, including after mining a rock.
- Bounds:
MinandMaxgrow to cover everything that moved, so rebuild that region afterwards.
Meshing: only what can be seen. From the top-down camera, the underground is never visible, so it isn't meshed:
- Each chunk meshes from its lowest surface (over its columns and a 2-voxel margin) minus
VisibleDepth, up to the sky. Cliffs, overhangs and cave mouths near the surface are included. - Deeper ground is meshed on demand by
EnsureMeshed(Cells), which extends the affected chunks 12 voxels below the deepest cell. It's called for every new colonist path, for spawn cells, and twice a second for the cell under each colonist (falling, digging, possessed), so collision is always there before anyone stands on it. Digging lowers the surface, so dug shafts are meshed automatically. - The cutaway view meshes
CutawayDepthvoxels below its slice. - Water surfaces follow the same ranges.
- Chunk geometry is computed in parallel (
BuildChunkMeshDatais read-only) and applied to the mesh components on the game thread. - Column heightmap:
SurfaceZholds each column's first air voxel above its highest solid one. It's filled at the end ofGenerateTerrainand kept current bySetTerrainand collapses.GetSurfaceZreads it, so finding a chunk's lowest surface no longer scans every column from the sky down. - Chunks near the center cook their collision immediately, because the first colonist spawns there; the rest cook in the background.
- Timing: meshing a 256×256 district takes about 0.2 s (it was 3.4 s meshing everything); 975×975 takes about 2 s. Those figures predate the per-chunk caches described under "Meshing (smooth terrain)", which made one chunk about 3× faster in a standalone test (5.6 ms down to 1.8 ms, one thread).
Threading (loading screen). A new game builds the world in steps so the voxel work stays off the game thread and the loading screen keeps drawing. Generate() and BuildAllChunks() still run all the steps at once (used by RegenerateWorld).
| Step | Thread | What it does |
|---|---|---|
PrepareGeneration() | game | Loads terrain, rock and plant definitions (data tables, asset references) |
GenerateTerrain() | worker | Stages 1–4: shape, surface rules, water, floating rock removal (each stage is itself ParallelFor) |
GenerateFeatures() | game | Stages 5–6: rocks and plants (they create components and instances) |
BeginBuildAllChunks() | game | Loads materials and creates the chunk and water mesh components |
ComputeAllChunkData() | worker | Every chunk's terrain and water geometry, in parallel |
ApplyBuiltChunks(Max) | game | Hands up to Max computed chunks per call to their components, then builds rock and plant visuals |
GetStepProgress() reports the running step's progress (0..1, thread-safe). While a worker step runs, nothing else may read or write the voxel grid. A mod that adds a generation stage must therefore put pure voxel work in GenerateTerrain and anything that touches actors or components in GenerateFeatures.
- Timing (975×975): terrain 4.8 s, features 1.0 s, meshing 0.7 s, applying 1.6 s, warm-up 0.5 s, 9.4 s in all. At 256×256 it's 4.0 s in all.
Meshing (smooth terrain). Voxels are game data, not visuals. The surface is built with surface nets in AVoxelWorld::BuildChunkMeshData:
- Each voxel center is a sample. Its density blends the voxel's own solidity (weight 0.6) with its 26 neighbors (weight 0.4). Solid voxels always score at least 0.6 and air at most 0.4, so the surface at 0.5 follows the grid exactly. Only vertex positions are smoothed.
- Flat ground lands at exactly the voxel-top height, so what you see matches where the pathfinder puts colonists.
- Each vertex blends the surrounding solid terrains: vertex color carries their average color, and UV channels 0–3 carry the weights of the 8 texture layers. Top and side looks crossfade by steepness (normal Z from 0.35 to 0.75).
- A single isolated voxel renders as a small rounded lump. One-voxel steps render as ramps of about 45°, which is why colonists have
WalkableFloorAngleset to 55°. - Per-chunk caches: before meshing, a chunk copies the solidity of a box of voxels around it into a byte array (the chunk, 2 voxels of margin on each side, and up to 16 layers above for the empty-layer skip). The edge loop and the vertices read only that array. Each voxel's density is computed once, on first use, into a float array over the same box, and each cell's vertex index goes into a flat array instead of a map. The output is unchanged: the same vertices and triangles in the same order.
To change how rounded the terrain looks, change the 0.6/0.4 weights in DensityAt (in BuildChunkMeshData). Keep the voxel's own weight above 0.5, or single voxels and one-voxel walls will disappear from the mesh while staying solid for pathing.
Terrain material. Scripts/build_terrain_material.py generates /Game/Terrain/M_Terrain. Each layer is sampled from 3 world-space directions, so cliffs aren't stretched. To change a layer's texture, scale or detail strength, edit the LAYERS table at the top of the script and run it in the editor console: py "<project>/Scripts/build_terrain_material.py"
Coordinate helpers: WorldToBlock(FVector) and BlockToWorldFeet(FIntVector). The second returns the bottom center of a cell, which is where a colonist's feet go.
4. Plants
Plants are rows in a CSV, loaded the same way as terrain:
- Base game:
Content/Data/Plants.csv(generated byScripts/gen_plants_csv.py; edit the script's table and rerun it, or edit the CSV directly) - Mods:
Mods/<YourMod>/Data/Plants.csv. A new name adds a plant, and a matching name replaces it.
The log reports LogPlants: Loaded N plant types and Generated N wild plants.
Columns
| Column | Meaning |
|---|---|
Name | Unique id, e.g. Oak |
DisplayName | Name shown to players |
Category | Tree, Wild, Crop, Decorative, or Stump (stumps, pods and other plant-related objects) |
Shape | Placeholder model: RoundTree, ConeTree, PalmTree, Cactus, Mushroom, Bush, Grass, Crop, Flower, Moss, Vine, Stump, Pod |
HeightCm, WidthCm | Full-grown placeholder size |
Footprint | Growing space: 1 = one voxel, 3 = a 3×3 area. Wide plants use 3 so they don't overlap |
bBlocksMovement | True = the plant's center voxel can't be walked through (trunks, stumps, cacti) |
MoveSpeed | Walk speed multiplier while passing through it (brambles 0.5, tall grass 0.85). Ignored if it blocks |
Color, StemColor | Foliage and trunk/stem colors, sRGB |
Habitat | Terrain names it grows on in the wild, e.g. "(Soil,RichSoil)" |
WildDensity | Chance per suitable ground column that world generation places one. 0 = never wild (crops, decoratives, special trees) |
Rules
- One plant per ground column. A 3×3 plant claims all 9 columns, and they must be level.
- Only the center (trunk) voxel blocks movement, for the plant's height. Colonists path around trunks. Undergrowth multiplies the terrain's walk speed.
- Digging out a plant's ground or burying it with a raised voxel removes the plant.
- World generation places 3×3 plants first, then the rest, each rolling
WildDensity×WildDensityScaleper column.WildDensityScale(default 0.5) is a property of the VoxelWorld'sPlantscomponent, the overall "how lush" knob. - A plant placed or removed rebuilds its type's instanced meshes once, at the end of the frame, however many plants of that type changed (a brush, a flood, a load).
colony.Plants.BatchRebuilds 0rebuilds on every change instead (for comparison). - Plants are built from the plant kit (section 10, "Plant and creature models"): a stalk, crown and accents for trees, one part for other plants, picked per species. One instanced mesh per plant type and part. Without the kit (or for stumps) they fall back to engine basic shapes colored through the material's
Colorparameter.
Launch plant set (85): 26 trees, 37 wild plants, 15 crops (the cocoa crop is the cocoa tree), 3 decoratives (bonsai is in trees), and 4 plant-related objects (gardener pod and chopped, burned and smashed stumps).
Nothing grows, gets harvested or gets planted yet. Plants are placement, rendering and pathing only.
Each playthrough, trees, wild plants and decoratives also get a generated species look and name over these rows (section 10). The rows are the plants' functions; DisplayName, Shape, Color, StemColor and the sizes shown in game come from the species.
5. Pathfinding
FVoxelPathfinder (VoxelPathfinder.h/.cpp) runs A* directly on the voxel grid. It doesn't use Unreal's navmesh.
- A cell is standable when the voxel under it is solid terrain with
MoveSpeedabove 0, the center column hasAgentHeightclear voxels, the rest of the 3×3 footprint is clear above step height (rock-blocked voxels count as walls), and no plant trunk sits anywhere in the footprint. - Moves go in 8 directions, with no corner cutting. Each move can step up by
AgentMaxStepUpor drop byAgentMaxDrop. - Cost is (1 per straight step, √2 per diagonal, plus 0.5 per voxel of height change) divided by
GroundSpeed: the destination terrain'sMoveSpeedtimes any undergrowth'sMoveSpeed. Colonists route around mud, marsh and brambles when the detour is cheaper. FindNearestStandablesnaps a clicked or requested point to the closest valid cell.- Closed areas: when a search runs out of cells before its expansion cap, every cell it reached is one closed area that doesn't hold the goal. That area is remembered, so later searches from inside it to outside fail at once instead of flooding it again. The memory resets every frame and whenever the terrain changes (
AVoxelWorld::GetTerrainVersion).-ColonyNoAreaCacheturns it off, to compare results. - Other colonists (
SetHeldCells): a caller can hand the pathfinder the cells other colonists hold. A colonist standing still holds its feet cell (cost 8); a walking one holds the cell it's headed to (cost 6). Stepping within 1 cell of a held cell adds that cost, within 2 cells half of it (two 3-wide bodies overlap closer than 3 cells). The goal cell itself is never charged. Reachability doesn't change, only the route.FindNearestFreeisFindNearestStandablethat skips cells within 2 of a held cell.
- Straight walking (
Straighten,CanWalkStraight): the A* cell path is cut down to its turning points. From each kept point it jumps to the farthest later cell, up to 24 cells on, that a straight walk reaches: the line is sampled every quarter cell, each new cell must be a valid A* step from the last (same step-up and drop, no corner cutting), no cell may be slower than the cells it skips (so shortcuts never cross the mud the route avoided), and none may be held by another colonist. Three failed tries in a row end the search for that point. - No navmesh, on purpose: the terrain changes as colonists dig, so a navmesh would need rebuilding all the time. The AdaptivePathfindingSystem plugin (Recast and DetourCrowd based) was tried and removed for this reason.
Ways to extend it: doors and zones, bigger or smaller agents. Each of these means changing IsStandable and StepTarget.
6. Colonists
AColonistCharacter (ColonistCharacter.h/.cpp) is a Character driven by an AI controller.
MoveToLocation(FVector)pathfinds to that location and walks there, showing the route as a yellow debug line.- The path avoids other colonists (
GetHeldCells, see section 5), and the goal moves to a free spot within 2 cells if another colonist holds it (task reach is 1-2 cells, so it never goes further). If there's none, it uses the nearest standable cell as before. - The colonist walks straight lines between the path's turning points (
Straighten), not cell centre to cell centre. Movement was never locked to voxels: the character moves freely and the grid only plans the route. - Steering around colonists on the move: Unreal's RVO avoidance on the character movement (
bUseRVOAvoidance, consideration radius 14 blocks, weight 0.5; needs no navmesh), plus a push away from colonists closer than two body widths (SeparationFrom). - "Who is near?" checks (
SeparationFrom,IsColonistAhead,GetHeldCells, animals fleeing) readUColonyCharacterIndex(ColonyCharacterIndex.h): every character's position, gathered once per frame into a 4 m grid (ForEachNear(Location, Radius, Visit),GetAll()), instead of walking all actors each time. New code with the same question should use it too. - If the colonist makes no progress for 1.5 seconds, it finds a new path, up to 3 times. When another colonist is in the way (
IsColonistAhead: within two body widths ahead), it repaths after 0.6 seconds instead, around where that colonist now stands. - Check:
-ColonyDevTest -ColonyJobTestmined 20 voxels with 4 designations left after this change (earlier runs: 19 or 20).
- The path avoids other colonists (
- Editable properties:
BodyColorandBaseWalkSpeed(300 cm/s on ground with speed 1.0). The name is the identity below. - Walk speed is
BaseWalkSpeed× the ground speed underfoot (terrain × undergrowth), updated every frame. - Movement: steps up 1 voxel, and walks slopes up to 55° (the smooth terrain turns voxel steps into ramps).
- The body is a MetaHuman (see "Colonist bodies and animation" below). If a body's meshes can't be loaded, the colonist falls back to placeholder shapes.
Names, id and xenotype (ColonyIdentity.h)
- Every character (
AColonistCharacter::Identity, anFColonyIdentity) has:- a nickname: required, never empty. It's the name the game shows (HUD lists, portraits, logs, typed orders).
- optional first and last names. The full form, "First 'Nickname' Last", shows where there's room: the selected-colonist card, the portrait tooltip and the designer heading (
GetFormalName()). - a numeric id (
GetCharacterId()): the colony id fromUColonyEntityComponent. The host gives it; it's never 0, never reused in a game (dead characters keep theirs) and never edited, and saves keep the counter. - a xenotype (
GetXenotype()/SetXenotype(), aData/Xenotypes.csvrow). Colonists areHuman, zombiesZombie.
ColonistNames.csvName,FirstName,LastName,Nickname,Tone,Humor(about 14,000 rows). A row isn't a set:- every distinct first name, last name and nickname goes into its own pool (empty values skipped);
- each generated character rolls one from each pool, so names come out as random mixes;
- names are rolled regardless of sex;
- nicknames avoid ones already in use when they can;
- Tone and Humor aren't used yet.
- Mods add rows, or replace one by
Name, withMods/<Mod>/Data/ColonistNames.csv.
- Who rolls the names:
AColonyGameMode::SpawnColonistAtrolls them, seeded like the rest of the colonist, so a seed gives the same names.- Anything spawned without a nickname gets one at BeginPlay.
Xenotypes.csvName,Label,Description,bColonyMember,Faction,SkinHue,SkinSaturation,SkinValue,SkinMultiply,EyeColor,ShapeOverrides,bAdultOnly,bPlainLook,bCrowd:bColonyMember: counts as one of the colony's own (AColonistCharacter::IsColonyMember()). Loops that mean "our colonists" skip the others: taskmaster, portraits, HUD list, colonist count, saves, mod API colonist list.Faction: Colony or Undead, for combat.- The look on top of the rolled one (
ApplyColors):- hue and saturation are added to the skin tone's;
- value and
SkinMultiplymultiply it; EyeColorreplaces the iris color unless its alpha is 0.
ShapeOverrides("Posture=0.8;HeadScale=1.1") sets shape sliders at BeginPlay.bAdultOnlyis for spawners.bPlainLook(Zombie), applied at BeginPlay (ApplyXenotypeShapes):- the Average body type for the sex, or its stage body when young; never Sentinel or Mercenary;
- the default face for the sex (
FaceRefs.csvbDefault) on the rolled head; - no face features and no shape sliders; height, head, eye and neck sizes at their defaults;
- no ancestry: one neutral skin tone, with skin and eyes from the row's look. The designer shows "-" and won't step it, and the look export leaves it empty.
- Only
ShapeOverridesapply on top. Hair, clothes and voice stay rolled. Fewer morphs also means cheaper zombies.
bCrowd(Zombie): the crowd cost profile for hordes.- No strand grooms: a groom hair roll is swapped for a mesh or sculpted hair option, by weight.
- No breast spring.
- The meshes animate every
CrowdFarTickIntervalseconds when farther thanCrowdFarDistancefrom the camera (Combat.csv). Movement and thinking still run every frame, so far zombies walk at full speed.
- Setting the xenotype: call
SetXenotypebeforeFinishSpawning, or pass it toSpawnColonistAt(Cell, Xenotype). - Designer: the colonist page has First, Nickname and Last text boxes. First and last can be cleared; an empty nickname is refused and the old one comes back. The id and xenotype are shown read-only.
Colonist bodies and animation
- Bodies:
Content/Data/ColonistBodies.csvlists the body types, with columnsSex,DisplayName,BodyMesh,FaceMesh,Weight,AnatomyMesh,UnderwearMeshandStage. Rows with an emptyStageare adult body types; see "Age and life stages" for stage bodies. Today it has Average, Slim, Athletic and Heavy for each sex, from the MetaHuman bundle, assembled into/Game/Characters/MetaHumans/<Type>_<Sex>. - Rolling: each new colonist rolls a body type for its sex (weighted; Average is twice as likely as each other type) and a height of 90–110% (
FColonistAppearance::BodyTypeandHeightScale). - Height: scales only the visible mesh; the capsule stays 3 voxels wide.
- Head, eyes and neck: the body plays its animation through
UColonistAnimInstance(ColonistAnimInstance.h/.cpp). It works like the engine's single-node player, then applies the colonist's bone changes (BoneMods) on top:- it stretches the
neck_02andheadoffsets byNeckLength; - it scales the
headbone byHeadScale, which carries the whole face. The face is a poseable mesh. Every frameUpdateFacecopies the body's pose onto it, so the shared neck and head bones move the same way on both meshes and the neck seam stays closed. Before this, these changes went on the face only, which opened a seam at the neck.UpdateFacethen sets the eye bones (FACIAL_L_Eye,FACIAL_R_Eye, which only the face has): scaleEyeScaleand the eye height offset. The eyeballs follow them through theirEyeParallelchildren, and the matching morphs move the sockets (see "Face features"). The values come from the world rules and can be changed per colonist on the Pawn tab.
- it stretches the
- Face bones (for feature shaping): the face rig has bones for each feature, all under
FACIAL_C_FacialRoot:- nose:
FACIAL_C_Nose(withNoseTipandNoseLower),NoseBridgeandNoseUpper(C, L, R); - mouth:
FACIAL_C_MouthUpperandFACIAL_C_MouthLower(lips andLipCornerhang off them), plus lip skin bones; - jaw and chin:
FACIAL_C_Jaw(Jawline,Chin,ChinSide),JawBulgeandJawRecess; - cheeks:
CheekInner,CheekOuterandCheekLower; - ears:
FACIAL_L_EarandFACIAL_R_Ear.-ColonyFaceBoneslogs them all, with parents and rest positions.
- nose:
- Face: follows the body (see the item above). All eight bodies share one skeleton,
metahuman_base_skel, so one animation set covers them all. - Animation:
Scripts/retarget_mannequin_to_metahuman.pyretargets the UE5 Mannequin unarmed set onto the MetaHuman skeleton. The set is engine template content, copied into/Game/Characters/Mannequins.- The script creates
IK_MannequinandRTG_Mannequin_To_MetaHumanin/Game/Characters/Animations/Retarget. - It writes 21 animations to
/Game/Characters/Animations/MetaHuman: theBS_Idle_Walk_Run_MHblend space (Direction -180..180, Speed 0..600), idle, the walk and jog directions, and jump, fall loop and land.
- Retarget setup (each of these caused visible bugs when missing):
- One stack of steps: the retargeter needs exactly one set of steps (pelvis motion, FK chains, IK rig, root motion, curves). Adding the defaults twice runs every step twice and warps the torso; the script clears the steps before adding them.
- Root to root: the root-motion step must map Mannequin
rootto MetaHumanroot, withpelvisas the target pelvis. Without that, the walk's travel lands on the pelvis (4 m per loop) and the body drifts away from the colonist, then snaps back each loop. - Played in place: the retargeted clips have root motion off and the root locked (
enable_root_motion = False,force_root_lock = True), because the movement component moves colonists. - Retarget target:
Base_Male; the other bodies share its skeleton. - Check:
Scripts/anim_diagnose.pylogs how far root and pelvis travel in every clip. After a correct retarget the walk's root moves about 4 m and the pelvis under 2 cm.
- Headless runs: both scripts also run without opening the editor:
UnrealEditor-Cmd <project> -run=pythonscript -script=<script>. Copy the script to a path without spaces first. - Test flag:
-ColonyBodyLineupTest(with-ColonyAutoTest) films every body type in a row, standing and then walking toward the camera. - Playback: colonists play the blend space directly, with no Animation Blueprint (
AColonistCharacter::UpdateAnimation).- Speed and direction are fed every frame. Direction is relative to facing, which matters when strafing while possessed.
- Speed is divided by the height scale, so feet keep pace.
- In the air the colonist switches to the fall loop.
- Assets to override:
LocomotionBlendSpaceandFallAnimationonAColonistCharacter. - First person: the face is hidden from the possessing player.
- Not yet:
- no clothing: MetaHuman bodies arrive in underwear, and the CasualSet clothing was removed;
- no hair, brows or beards: the bundle is bald;
- no facial animation or head-size control;
- no Mutable: the earlier CasualSet Customizable Objects were deleted with the pack.
Scripts/build_colonist_mutable.mjsstill targets CasualSet and needs pointing at MetaHuman meshes before it's useful again.
- Kept from CasualSet: only
/Game/CasualSet01/Tools(axes, pickaxe, scythe, sickle, pitchforks, rake, shovel, broom, hammer), for tools and improvised weapons. - Zombie:
/Game/WWG_ZombieLite, with modular dismemberment, is kept for later. - Body parts: the parts we track (health, injuries, which pieces get swapped when a part is lost or replaced) are listed in
Docs/Design/BodyParts.md.
The game mode spawns one colonist near the world center (more from the dev panel). AColonyGameMode::SpawnColonistAt(Cell) spawns one at the nearest standable cell and gives it a sex, a placeholder name from that sex's list, and a body color.
Sex and world rules
- Sex: every new colonist is female or male (
AColonistCharacter::Sex). The share of women is theFemaleRatioworld rule, 0.5 by default. - The rules file: world rules live in
Content/Data/WorldRules.csv, asKey,Value,Notesrows. Mods can override keys with their ownMods/<Mod>/Data/WorldRules.csv. - Read once: the game reads the file once when it loads (
FColonyWorldRules::Get(), called inStartPlay). - Changing it: the dev panel's World rules tab rewrites the base file (
FColonyWorldRules::SaveValue). Like editing the file by hand, the change applies on the next load.
| Rule | Default | Meaning |
|---|---|---|
FemaleRatio | 0.5 | Share of new colonists that are female |
IntactBodyChance | 0.95 | Chance a new colonist arrives with every body part. The rest lost 1–2 parts long ago, fingers and toes far more often than whole limbs |
HeadScale | 2 | Head size of new colonists (1 = MetaHuman default) |
EyeScale | 1.2 | Eye size relative to the head, 0.5 to 1.2 (capped at 1.2) |
NeckLength | 1.3 | Neck length (1 = MetaHuman default) |
OldInjuryChance | 0.3 | Chance a new colonist has 1–3 permanent old injuries: scars on outer parts, healed fractures on bones |
Generated history (UColonistHealthComponent::GenerateHistory, run for every spawned colonist):
- A generated colonist is never missing a vital part, or any part a vital part hangs from. So the head (which holds the brain), the neck and the torso are never generated missing.
- Old injuries never destroy a part.
- Permanent injuries show as "permanent" in the health tab. Dev "heal all" still clears them.
Ancestry, skin tone and eye color
Toon eyes (everyone): colonists have the Fab "Toon 3D 8" cartoon girl's eyes, not the MetaHuman eyeballs.
BodyGraft -ToonEyes [-Save]cuts her Eye_L/R sections out oftoon_3d_8into/Game/Characters/Eyes/SM_ToonEye_L/R: centred on each eyeball (sphere fit, radius 2.71 cm on her), unit radius, iris along +X, with her material. Her moisture films are left out (opaque glTF default; they painted a disc over the pupil).AColonistCharacter::ApplyToonEyesattaches them to the face'sFACIAL_L/R_Eyebones, sized to our eyeballs (1.45 cm) times the age head size, and hides the MetaHumaneyeLeft,eyeRight,eyeshellandeyeEdgesections. Her eyeball has an open pupil, so a near-black sphere sits just inside each eye.- Eye size, height and spacing still move them (bone scale and translation). The age morphs move the eye sockets as vertices; the eye bones take the same move (
MorphEyeMove: the average move of the skin within 2.4 cm of each eye bone), so children's eyes sit in their smaller sockets. (The male age morphs move only one of the MetaHuman eyeballs, so the eyeballs' own move isn't used.) - The iris is hers for everyone: the eye colour roll below no longer shows until the toon iris gets a tint.
Each new colonist rolls an ancestry from Content/Data/Ancestries.csv, which keeps skin and eye color consistent within a group:
| Ancestry | Skin tone range (0 = lightest, 1 = darkest) | Eye colors (weights) |
|---|---|---|
| African | 0.68–1.0 | dark brown 8, brown 2 |
| Hispanic / Latino | 0.25–0.75 | dark brown 4, brown 4, hazel 2, green 0.5, blue 0.3 |
| East Asian | 0.2–0.5 | dark brown 9, brown 1 |
| Middle Eastern | 0.3–0.65 | dark brown 4, brown 4, hazel 1.5, green 0.8, blue 0.3 |
| South Asian (Indian) | 0.45–0.85 | dark brown 7, brown 3, hazel 0.5 |
| European | 0.0–0.35 | blue 3, brown 3, green 1.5, hazel 1.5, grey 1 |
- Columns:
Weight(chance among ancestries),SkinToneMin,SkinToneMax, andEyeWeights(EyeDarkBrown=8;EyeBrown=2, using option names fromAppearanceOptions.csv). The ranges are broad and overlap on purpose. - Skin color:
- Every bundle body has the same medium-dark baked skin.
Content/Data/SkinTones.csvrecolors it per tone (FAppearanceCatalog::SkinLook, applied inAColonistCharacter::ApplyColors). Tones in between rows are interpolated. Tone 0.72 is the skin as baked. - The columns drive the post-bake parameters on the body and face skin materials (
MF_skin_globalAdjustmentsPostBake). The material converts the baked color to HSV, adjusts it, converts it back, then multiplies: | Column | Material parameter | Effect | As baked | |---|---|---|---| |Hue| Basecolor Global Hue Post-Bake | added to hue (0–1 = full circle; negative = redder) | 0 | |Saturation| Basecolor Global Saturation Post-Bake | added to saturation | 0 | |Value| Basecolor Global Value Post-Bake | multiplies brightness | 1 | |Multiply| Basecolor Global Multiply Post-Bake | final RGB multiply | (1,1,1) |Hue,SaturationandValueare optional; a file (or mod file) without them keeps the baked values. - Light tones brighten through
Value(4.2 at tone 0, because the baked albedo is dark in linear space) with a slight desaturation and a small red hue shift. A bright RGB multiply alone left light skin tan and orange. Dark tones still useMultiply. - Tuning tips: small numbers go a long way. Hue ±0.03 already turns skin red or olive. Saturation below about -0.2 looks grey or ashen at normal brightness, and above +0.3 the color goes out of gamut.
- Real light bases: baking light, medium and dark skins through MetaHuman Creator's skin settings (
commit_skin_settings) needs the optional MetaHuman Creator core data from the Epic Games Launcher (UE 5.8 > Options). Without it, texture synthesis is off and changing the skin tone crashes the editor. If those bases are added later, the recolor can shrink to fine variation around the nearest base.
- Every bundle body has the same medium-dark baked skin.
- Eye color: eye options in
AppearanceOptions.csvhave aColorcolumn. The heads' baked eye materials read a pre-baked iris texture and ignore colour parameters, so each eye is drawn with MetaHuman's procedural eye preset for its side (MI_eye_eyeball_unified_MH_preset_left/_left_right, keeping the head's own iris rotation). The iris is desaturated ("Iris Global Saturation" 0) and tinted with "Iris Color Multiply" =Colorx 3.-ColonyDesignerEye=EyeBluepicks the eye option in designer tests. - Pawn tab: an ancestry stepper (changing it rerolls skin and eyes inside the new limits) and a skin tone slider; stepping eyes updates the iris color live.
- Test flag:
-ColonyBodyLineupTest -ColonyAncestryLineupfilms one colonist per ancestry, up close.
Age and life stages
FColonistAge (ColonistAge.h/.cpp) has two ages. Neither is ticked:
- Chronological age: time since birth (
BirthDay, a calendar day). It always runs, and can be thousands of years for a colonist who slept in a cryo pod. - Biological age: how old the body is (
BiologicalYears). It runs atAgingRate(per colonist: 0 = frozen, for genes and traits later) times the game's aging speed.
Refresh brings both up to date lazily, from the calendar. It runs when the colonist panel or the Pawn tab shows the age, when an age effect hits (ApplyAgeEffect), and for every colonist when a year turns.
Life stage is categorical, from Content/Data/LifeStages.csv (Name,DisplayName,EndsAtYears; mods override by row). Boundaries are written for an 80-year life (FLifeStages::ReferenceLifespan) and scale with the colonist's LifespanYears. The stage therefore reflects how far through their life the colonist is: with a 3-year lifespan, 2 years old is elderly.
| Stage | Ends at (80-year life) |
|---|---|
| Baby | 1 |
| Toddler | 4 |
| Child | 13 |
| Teen | 18 |
| Adult | 60 |
| Elder | - |
- New colonists: biologically 18 to 55, mostly young (scaled to lifespan).
CryoChance(3%) of them slept 10 to 5000 years first, so their chronological age is far higher. - Lifespan:
HumanLifespan(80) inWorldRules.csv. Genes and traits will change it per colonist later. - Calendar: see section 7. A day is
DayLengthSeconds(1500 game seconds, 1.5 RimWorld days) and a yearDaysPerYear(120) days. - Aging speed: picked on the new game screen (0.5x, 1x, 2x, 3x; default 1x). It is biological years per calendar year. Since our year is 3x as long as RimWorld's in real time, 3x ages as fast as RimWorld does.
- Shown: the colonist panel ("age 34 (adult), born 2,316 years ago") and the Pawn tab, which also edits biological and chronological age. Lifespan is not player-editable.
- One face for life, aged by morphs: every colonist keeps its adult type's face at every age. Hidden age morphs on every head (
Age_Baby,Age_Girl/Age_Child,Age_Teen_Female/Age_Teen_Male,Age_Elder_Female/Age_Elder_Male) reshape it by biological age (FFaceFeatures::AgeWeights, applied inApplyFaceFeatures, refreshed with the age). Key ages for an 80-year life, scaled to the lifespan: baby 1, child 7, teen 14, the colonist's own face from 20 to 55, elder 85. Younger than 7 is the child face at full weight: the bundle baby's face is out of proportion (taller than an adult's), soAge_Babyexists but is unused. Between two keys the face blends linearly, so it changes a little every year. Players never see a slider for it. The colonist's feature and shape sliders apply on top at every age.- Sources: each bundle child, teen and elder MetaHuman's own preview face, exported from the MetaHuman editor (
Scripts/metahuman_preview_face.py, toSaved/MetaHumanPreviews) and imported as/Game/Characters/MetaHumans/Sources/AgeFaces/SKM_Age_<Name>_Face(Scripts/import_age_faces.py). - Building:
UnrealEditor-Cmd ColonySim.uproject -run=FaceMorph -Source=<age face> -Age=<Name> [-Save]writes one whole-face morph per head. Unlike reference faces it keeps the source's real size and place on the head bone (a child's face is smaller). Like every morph it fades out at the neck edge, so the neck stays sealed against the body. The eyeballs move with their sockets to where the age reference has its eyes. - Hair shrinks with a child's head (
FFaceFeatures::AgeHeadSize: the child faces are about 14% smaller), and is refitted with the face on every age refresh.
- Sources: each bundle child, teen and elder MetaHuman's own preview face, exported from the MetaHuman editor (
- Bodies by stage: a
ColonistBodies.csvrow with aStage(Baby, Toddler, Child, Teen) is the body a colonist of that stage and sex wears (FAppearanceCatalog::BodyForStage), preferring the row whoseDisplayNamematches the colonist's adult type. The adult type (FColonistAppearance::BodyType) is rolled once and kept, so the adult body returns at adulthood, or if age is ever reversed.RefreshAgeswaps the body when the stage changes. Stage rows are never offered as adult types. A stage row changes the body only; the face stays the adult type's (aged by morphs).- Stage bodies today: Baby and Toddler wear
Baby(any sex); Child wearsGirlorChild; Teen wearsTeen_FemaleorTeen_Male; Elder wearsElder_FemaleorElder_Male(all in/Game/Characters/MetaHumans). - How they were built: the bundle's child, teen and elder characters have no skin texture sources, so the MetaHuman build refuses them.
Scripts/metahuman_reshape.pyduplicates an adult that has textures (Base_Female/Base_Male) into/Game/Characters/MetaHumans/Sources, gives the copy the source's body (its preview body mesh and joints,set_body_mesh/set_body_joints) and builds it. The body is the source's; the skin textures are the adult donor's; the built face is the donor's and unused. Fab originals are only read. - Underwear: every stage body has its own
UnderwearMesh, fitted by-run=BodyGraft -Garment=...(section 6 "Garments"): girls and teen girls the Sample bra plus the Sample shorts (the Sample briefs are a thong cut, not used on children); boys, teen boys and babies the Sample shorts as boxers; elder women Roxy's briefs like adult women; elder men the shorts. Stage bodies never have anAnatomyMesh; underwear removal stays adult-only in code. -ColonyDesignerAge=<years>sets biological age in designer tests.
- Stage bodies today: Baby and Toddler wear
- Garments (
BodyGraftCommandlet):-run=BodyGraft -Garment=<mesh> -GarmentDonor=<the body it was made for> -Bodies=Girl,Baby -Out=<Name> [-Sections=0,1] [-Extra=<second garment>] [-Upper] [-Save]fits any pack's garment onto our bodies as/Game/Characters/Wearables/<Name>/SKM_<Name>_<Body>, one material slot per visible section (a section the pack hides with a hiding material gets the material named after its slot). Skin weights and body morphs come from the body: blended across the skin triangle under each garment vertex (copying one nearest vertex let loose shorts clip where the smoothing and mass morphs change fast).-MinGap=<cm>(default 0.2) is how far off the skin the garment stays at least; the adult boxers use 0.4.
Appearance slots
Each colonist rolls looks for 12 slots (FColonistAppearance, ColonistAppearance.h):
| Slot (Mutable parameter) | Option kind | Body part |
|---|---|---|
Head | Head | Head |
LeftEye, RightEye | Eye | LeftEye, RightEye |
LeftEar, RightEar | Ear | LeftEar, RightEar |
Nose | Nose | Nose |
Mouth | Mouth | Jaw |
Teeth | Teeth | Teeth |
Hair, Brows | Hair, Brows | Head |
Beard | Beard | Jaw |
Lashes | Lashes | Head |
- Options: come from
Content/Data/AppearanceOptions.csv, with columnsSlot(kind),DisplayName,Sex,WeightandMutableOption, and the optionalSculptandbAdultOnly(see "Sculpted facial hair kit" below). Both eyes, and both ears, share one roll. - Numbering: each slot's default is option 1 (
Face 1,Nose 1,Mouth 1,Ear 1,Teeth 1,Hair 1 (bald)). Nose, mouth, ears and teeth rows are kept as body-part slots (they go "missing" with the part); their shapes come from face features below. - Hair and beards (
Hair,Beardoptions): each row takes a piece of our sculpted kit (Sculpt, below), a groom (Groom, plusGroomHead) or a rigged mesh (HairMesh). Leave all empty for none.AColonistCharacter::ApplyHeadPiecehandles grooms and meshes,ApplySculptPiecethe kit.- Rigged meshes (hair cards from other packs) show in their reference pose, fixed to our
headbone: the mesh's ownheadbone is put on ours, scaled by the ratio of the two head bones' heights, then lifted until the hair's top rests 1 cm above our skull's top (head bones sit at different depths in different packs). - Hair options today: 1 bald; the kit's eight sculpted styles (crop, bob, ponytail, undercut, mohawk, side shave, spiky, banded bowl), which are the ones rolled; and, at weight 0 (designer only), the older realistic hair: 2 broccoli fade, 3 high ponytail, 4 messy ponytail (MetaHuman grooms); 5-7 Roxy's three cuts; 8-9 the Sample pack's hair (
SK_Hair_1,SK_Hair_2); 10 twisted braids (SentinelB's groom) and 11 afro fade (SentinelC's). - Per-head fits (
FHairFit, built by theHairFitcommandlet): every hair and beard option has a reshaped copy for each of the 8 colonist heads in/Game/Characters/HairFits/<Option>/, namedGR_<Option>_<Head>for grooms andSKM_<Option>_<Head>for meshes (Headis the face mesh name withoutSKM_MHC_and_FaceMesh, e.g.Base_Female).ApplyHeadPieceuses the copy for the colonist's head when it exists, and falls back to placing the original as described here when it doesn't.- How the reshaping works: the hair is first placed as the game would place the original (grooms by the
GroomHeadbounds stretch,FHairFit::GroomFit; meshes by the head bones,FHairFit::MeshFit). Each strand point or vertex then moves by the inverse-distance average, over its 16 nearest scalp points, of the gap between the authoring head (GroomHead's skin) and our skin. Scalp points more than 5 cm from our skin are ignored. So the hair follows our skull, and long strands move with the scalp area they grow from. The field is sampled on a 1 cm grid and interpolated (grooms have millions of points). - Reshaped grooms are stored already placed on their head, so they get no stretch on top; they still follow the head bone and shrink with a child's head. Reshaped meshes are stored in the hair's own space and keep the original's head-bone placement (always measured on the original mesh).
- For a mesh row,
GroomHeadnames the pack's own head (SK_ROXY_HEADfor Roxy,SK_Bodyfor the Sample hair). - Hair cards and helmet LODs of a groom are not reshaped (they're separate meshes); only the strands are.
- Why not groom bindings: a binding's transfer matches the authoring head and ours by UV layout. The character package faces share only about 13% of our UVs, and their bound strands ended up almost all hidden inside the skull; bindings also build at runtime ("Preparing Groom Bindings") the first time each is used.
- Not in git: the fits total 2.75 GB, so
Content/Characters/HairFits/is git-ignored. After a fresh checkout, or after changing a hair or a head, runUnrealEditor-Cmd ColonySim.uproject -run=HairFit -Save(about 6 minutes). Without the fits, the game places the original hair as described above. - Building:
UnrealEditor-Cmd ColonySim.uproject -run=HairFit [-Only=<Option>] [-Save]. Without-Saveit reports the scalp samples and how far each hair moves. Re-run it after adding a hair or a head, or after changing a head's shape. The full order after any head change:-run=FaceMorph ... -Save, then-run=BodyGraft -WeldSeam -Save(face seam weights and normals), then-run=BodyGraft -StageSeam -Save(the child and stage bodies'Seam_<face>morphs depend on each face's collar), then-run=HairFit -Save.
- How the reshaping works: the hair is first placed as the game would place the original (grooms by the
- Beards: the kit's beards and mustaches (men only, adults only).
-ColonyDesignerHair=HairRoxy1and-ColonyDesignerHeadScale=1.1set them in designer tests.- Head size runs 0.9 to 1.5 (the slider,
Clampand the HeadScale world rule). New colonists start at the rule's value (1.5), the top of the range, so the slider only makes heads smaller from there.
- Rigged meshes (hair cards from other packs) show in their reference pose, fixed to our
- Groom details: the
Groomcolumn takes a groom asset.- The groom (the original or its reshaped copy) is fixed to the face's
headbone with no binding asset, so it follows the head and scales withHeadScale. - The
GroomHeadcolumn names the head mesh the groom was authored on. MetaHuman Fab grooms ship one,GroomMesh/SKM_MH_Groom_Head; for hair from a MetaHuman character package, use that character's face mesh. Without a fit, the hair is moved and stretched from that head's bounds onto each face. - Hair is rigid: it doesn't simulate. Long hair would need a binding or physics.
Hair 2 (broccoli fade)uses Kavya'sHair_TEKATI_Broccoli_Fade_no2, for any sex.- The old placeholder hair rows (Short, Long, Bun, Buzz) had no meshes and were removed.
- Brows, beards and lashes come from the sculpted kit.
- The groom (the original or its reshaped copy) is fixed to the face's
- Warm-up: the loading screen preloads every face preset and groom and compiles their shaders.
Sculpted facial hair kit
Our own hair, brows, beards, mustaches and lashes: solid, sculpted cartoon volumes (no strands or cards), modern and cyberpunk designs, made procedurally in Blender so they match the toon look and read at colony camera distance.
- Pieces (
Art/FacialHair/FacialHairParts.csvlists them with slot, head, adult-only and triangles):- Lashes:
LashShort,LashMedium,LashLong(upper band with flicks at the outer corner; the long set adds a lower lash). - Brows:
BrowThin,BrowThick,BrowArched,BrowStraight,BrowSoft,BrowAngry, and the cyberpunkBrowSlit(two shaved slits) andBrowBlock(three geometric blocks). - Beards:
BeardStubble,BeardBoxed,BeardGoatee,BeardChinStrap,BeardFade(stubble sideburns to a sculpted pointed chin); mustachesMustachePencil,MustacheChevron,MustacheHandlebar,MustacheBlade(two sharp split wedges),MustacheHorseshoe. - Hair:
HairCrop,HairBob,HairPonytail,HairSpiky, and the cyberpunkHairUndercut(faded sides, slick top, a streak),HairMohawk,HairSideShave(one side shaved with cut lines) andHairBandBowl(a bowl over a shaved band).
- Lashes:
- How they're built (
Scripts/blender_facial_hair_kit.py): every piece is clumps (lens-shaped lofts with a groove cut along the top, pointed or blunt) laid over a skin-hugging shell, on a real MetaHuman head (Saved/MetaHumanPreviews/Teen_Male_FaceMesh.fbx, the same face Unreal has asSources/AgeFaces/SKM_Age_Teen_Male_Face), so they sit on the skin. Lashes follow the line where that head's lids start covering the toon eyeball, and are stored in the eyeball's own space. - Mask colour on every piece (the
Maskvertex colour), read byM_LyridiaHair:- R = dye zone (streaks, tips, rims; neon dye glows a little)
- G = groove (darkened)
- B = per-clump shade jitter
- A = coverage: 1 = hair colour, lower blends toward the colonist's skin colour (stubble, shaved sides, fades)
- UV0: U across a clump, V root to tip (shells: azimuth and elevation around the head).
- Fitting (
FacialHairFitcommandlet): carries each piece onto all 8 colonist faces as/Game/Characters/FacialHair/<Piece>/SKM_<Piece>_<Head>(FHairFit::SculptPath), on the face's own skeleton. The heads are aligned by their landmark bones. A vertex near the skin keeps its gap from it: the spot under it on the reference skin is carried onto the target skin, plus the same gap along the target's normal, so shells keep their thickness and never sink in; hanging hair moves with its 8 nearest skin vertices instead. Each vertex takes the blended skin weights of the face vertices it maps to (beards follow the jaw, brows the brow bones) and the face'sAge_*,Shape_*andRef*morphs, only those that move the piece. A fit is just the piece on the face's skeleton (one LOD, no rig data or physics). In game the piece is a poseable mesh following the face (leader pose) with the face's morph weights copied by name (SyncSculptMorphs), so head size, age, brow height and face shapes move it. - Lashes (
Lashesslot) are static meshes (SM_FH_<Lash>_L/_R) attached to the toon eyes, so they follow eye size, height and age. The face's own MetaHuman lash sections are hidden while they show. - Colour:
FColonistAppearance::HairColor(rolled from a natural palette, mostly dark) tints hair, brows and beard; lashes take a near-black shade of it; elders grey from about 45 to 75 (scaled by lifespan).DyeColor(A = 0 for none, rolled about 1 in 14 from a neon palette) colours the hair and beard dye zones. Both are saved (Colonists section version 4; older saves roll a hair colour) and set in the designer's Looks section (swatches; dye has Off). - Children and teens (life stage below Adult) never show a
bAdultOnlyrow: no beards or mustaches, and the extreme shaves (mohawk, side shave, banded bowl) show the first child-safe sculpted style instead (crop). Brows, lashes and hair otherwise show at every age. - Building: ``
blender -b --factory-startup --python Scripts/blender_facial_hair_kit.py -- Art/FacialHair --head Saved/MetaHumanPreviews/Teen_Male_FaceMesh.fbx --preview <png> UnrealEditor-Cmd ColonySim.uproject -run=pythonscript -script="<project>/Scripts/import_facial_hair.py" UnrealEditor-Cmd ColonySim.uproject -run=FacialHairFit -Save`--previewrenders contact sheets (one per category; a column per piece, a row per view).--headtakes any MetaHuman face FBX whose face Unreal has (a colonist face, or one inSources/AgeFaces). Faces are exported to FBX from the editor UI orScripts/export_face_reference.pyin the editor; headless, the skeletal FBX exporter asserts.-run=FacialHairFitwithout-Savereports each piece's distance from the reference skin and how far it moves per head;-Only=<Piece>builds one. Re-run the fit after changing a head (after-run=HairFitin the head-change order above). The fits are git-ignored (Content/Characters/FacialHair/*/); the kit's source meshes andM_LyridiaHairare in git, so after a fresh checkout run-run=FacialHairFit -Save` once. - Designer tests:
-ColonyDesignerBrows=BrowsSlit,-ColonyDesignerBeard=BeardFade,-ColonyDesignerLashes=LashLongpick those options (-ColonyDesignerHair=the hair);-ColonyDesignerHairColor=<index>and-ColonyDesignerDye=<index>(-1 = none) pick from the hair and dye palettes. - Mods: add a row with
Sculpt= a piece name; a new piece needs the kit script (or any mesh built in the same head space with the same mask colour) plus the import and fit steps. - Pawn tab: cycling Head or Hair rebuilds the colonist's face and hair.
- Missing parts:
GetMutableParametersreturns each slot's option, orMissingwhen its body part (or anything above it) is lost. A lost eye then setsLeftEye = Missing. - Panel: the colonist panel shows face, hair, brows, beard and eyes.
- Customizable Objects:
Scripts/build_colonist_mutable.mjsknows how to add one group per slot, with the CSV's options plusMissing. No CO uses it right now: the slots are data until MetaHuman faces and grooms are wired. - Dev panel Pawn tab: edits the selected colonist live: sex (resets health, rerolls looks), body type, height, each slot's option, and a full reroll.
- Teeth:
Teethwas added toBodyParts.csv(underJaw) for this.
Adult anatomy wearable
Adult-only content. Female bodies carry anatomical detail (nipples and areolae, groin) as a wearable: a separate skeletal mesh on the body skeleton, lying on top of the skin. It never cuts into the body. As a separate layer it is hidden by underwear and clothing, and its skin can be replaced later without touching the body.
- Adult only, in code:
AColonistCharacter::IsAnatomyShownrequiresFLifeStages::IsAdult: at leastAdultAge(18) biological years, scaled to the lifespan. It is hard-coded so no data file or mod can lower it. The wearable is also hidden unless shown on purpose (dev toggle below), and when the body row has none. - Data:
ColonistBodies.csvcolumnAnatomyMesh(female rows:/Game/Characters/Wearables/Anatomy/SKM_Anatomy_<Type>_Female; male rowsSKM_Anatomy_<Type>_Male, below). - Underwear: every female body wears underwear: Roxy's lingerie bottoms fitted onto that body, in the
ColonistBodies.csvcolumnUnderwearMesh(/Game/Characters/Wearables/Underwear/SKM_Underwear_<Type>_Female). It follows the body (leader pose). Only dev mode removes it, with "Dev: remove underwear (adults only)" in the Pawn tab (no longer in the Character Designer), only for adults, and never in Streamer mode (Options > Display; saved asStreamerModeunder[/Script/ColonySim.ColonyOptions]inGameUserSettings.ini; on by default;AColonistCharacter::IsStreamerMode/SetStreamerMode;-ColonyStreamerOffturns it off for tests) (IsUnderwearOffuses the same hard-coded check). The anatomy wearable shows only with the underwear off.- Fitting:
-run=BodyGraft -Underwear [-Save]. Each garment vertex finds its spot on Roxy's skin and its distance off it. The spot is carried to our body (per-axis landmark fit on pelvis, spine and thighs, depth following width) and onto our skin, and the vertex goes the same distance off (at least 0.2 cm).
- Fitting:
- No painted briefs: MetaHuman always paints briefs into the baked body textures;
show_top_underwearis its only underwear setting.-run=BodyGraft -Unbrief [-Save]paints them out of each female body:- It finds the fabric by colour (bluish, which skin never is), only in texture areas reached from the body's pelvis vertices, then grows the area 12 pixels to catch the shaded edges.
- It fills base colour and SRMF from the surrounding skin, coarse to fine, and flattens the normals there.
- It writes
T_Body_BC_NoBriefs,T_Body_N_NoBriefsandT_Body_SRMF_NoBriefs(4096) beside the originals and pointsMI_Body_Bakedat them. The originals are kept: setting the three "Baked" parameters back undoes it. Male bodies still have their painted briefs.
- How it's built: the
BodyGraftcommandlet (BodyGraftCommandlet.h/.cpp, editor only) runs, per female body type: ``UnrealEditor-Cmd ColonySim.uproject -run=BodyGraft [-Lift=0.15] [-Save]``- Cuts small regions out of the donor (IdaFaber ROXY,
SK_ROXY_BODY_FULL): aroundnipple_landnipple_r(4.5 cm) andgenital(4.8 cm). - Keeps only their fine surface detail, measured against the donor's own smoothed skin (rim pinned).
- Fits them onto our body: size from nearby landmark bones (clamped: hip width doesn't size the groin), then position refined against our skin.
- Lays the detail on our skin along its smooth normal (closest point on the body's triangles), lifted 0.15 cm, with recesses deeper than 0.6 cm flattened so everything stays above the skin.
- Skin weights come from the body vertex underneath, so it follows the body with leader pose.
- Vertex alpha fades the rim out, dithered by the material. Without
-Saveit only reports. Re-running replaces the 4 wearables.
- Cuts small regions out of the donor (IdaFaber ROXY,
- Kept as items: the anatomy stays a separate wearable (never baked into the body texture), so it can be left off entirely, for streaming for example.
- Nipple placement: the skeleton fit alone put heavy bodies' nipples too high. MetaHuman bodies share one UV layout, so the nipple UV is taken from the average body's fit and every other body's patch is moved onto the point with that UV. The average body must be processed first (it is).
- Material:
M_AnatomyWearable(Scripts/anatomy_material.py), shaded like the stylized skin (subsurface, warm rim, shadow lift): the albedo is the colonist'sSkinColor(set byApplyColors, the same value as the body), and the donor texture only marks the areolae: how much darker a texel is than the donor's skin (DonorSkinLum0.49, timesAreolaGain2.5) blends towardSkinColorxNippleColor(0.5, 0.3, 0.28, the procedural nipples' tint). The donor normal gives the form (NormalStrength0.6).SkinTintandSkinMatchremain but do nothing. It must allow morph targets, or the game falls back to the default material. (The donor's own colour, tinted, made the areolae nearly black.) - Known gaps: small dark folds at the patch rims; on the heavy body the underwear's front bow is slightly squashed.
- Male anatomy (adult only, same rules: shown only with the underwear off, never in streamer mode or on children):
- The piece:
Scripts/blender_male_anatomy.py(Blender, headless:blender -b --factory-startup --python Scripts/blender_male_anatomy.py -- Art/Anatomy/SM_MaleAnatomy.obj [--preview <png>]) builds a plain stylized form from two metaball surfaces (a flat base pad with the scrotum; the shaft with the glans), smoothed and reduced to about 4000 triangles, as an OBJ in cm (X side, Y out from the body, Z up, origin at the attach point). - Fitting:
-run=BodyGraft -MaleAnatomy=Art/Anatomy/SM_MaleAnatomy.obj [-Raise=<cm>] [-Lift=<cm>] [-Save]places it on each male body (Base, Slim, Athletic, Heavy): the attach point is 1.5 cm above the most forward midline skin between the pelvis and the thighs (-Raisemoves it). The base pad (Y below about 1.4 cm) drapes onto the skin, lifted-Lift(0.08 cm), so the join can't show; the rest is rigid on the pubic skin's bones. Body morphs are copied like every wearable's (the Sentinel and Mercenary shapes included). Saved as/Game/Characters/Wearables/Anatomy/SKM_Anatomy_<Type>_MalewithMI_LyridiaSkin_Body, so it takes the skin tone and shading of the body (ApplyColorssets itsSkinColor). The Sentinel row uses the athletic one. - Male underwear: the Sample shorts as boxers, fitted like the elder and kid boxers:
-run=BodyGraft -Garment=/Game/Sample/Meshes/SK_Shorts.SK_Shorts -GarmentDonor=/Game/Sample/Meshes/SK_Body.SK_Body -Bodies=Base_Male,Slim_Male,Athletic_Male,Heavy_Male -Out=Boxers -Save(/Game/Characters/Wearables/Boxers/). - Test:
-ColonyDesignerTest -ColonyDesignerSex=Male -ColonyDesignerTorso -ColonyDesignerAnatomy -ColonyStreamerOff.
- The piece:
- Test:
-ColonyDesignerTest -ColonyDesignerSex=Female -ColonyDesignerBody=FemaleHeavy -ColonyDesignerAnatomyremoves the underwear (adult check logged), with a torso framing for the second screenshot;-ColonyDesignerTorsogives the torso framing with the underwear on.-ColonyAnatomyUntintedleaves the anatomy wearable's skin tint white, to check the bake.
Face features
Every colonist uses one of our 8 heads (the body type's own face). The heads are malleable per feature: each feature takes its shape from a reference face, and a slider sets how far.
| Feature | Region (face bones whose names contain) |
|---|---|
| Nose | Nose, Nasal |
| Brow ridge | Brow, Forehead |
| Eyes (lids, sockets, lashes) | Eye, Lid, Lash |
| Mouth (lips, corners, teeth) | Lip, Mouth, Teeth, Tongue |
| Ears | Ear |
| Jaw and chin | Jaw, Chin |
| Cheeks | Cheek |
| Head shape | Skull, Temple, and the head bone itself (the scalp, temples, back of the head); an accent rolled at 20-60% (FFaceFeature::RollMin/RollMax). Hair is rigid and fitted to each head's own skull, so above the brow and at the back the morph may sink the scalp but raises it at most 0.25 cm. Versions: Ref2, Ref4-7 (Ref3's skull is the same as ours, so it doesn't list Head) |
- References: ref 1 is always the head's own shape. Others come from
Content/Data/FaceRefs.csv(Name,DisplayName,Sex,Weight,Source,Features); references are shown by number only (version 1, 2...). A version is always picked; at strength 0 the feature is the head's own shape (the default face), and the pick is kept as a 0-weight entry.Ref2(women) is built from/Game/Characters/MetaHumans/Kavya/Face/SKM_MC_Kavya_FaceMesh(back as option 1 for the Polyart defaults to come).Ref3(any sex) is from the Sample pack's/Game/Sample/Meshes/SK_Body.Ref4(men),Ref5(women) andRef6(any sex) come from three MetaHuman character packages unpacked into/Game/CharacterAssemblies/(Sentinel,SentinelB, and the childSentinelC), and give eyes, ears, nose and mouth only.Ref7(men only) is the Vampire MetaHuman's face (/Game/MetaHumans/Vampire/SKM_Vampire_Face;Content/MetaHumans/is git-ignored, so only re-baking needs it: the morphs live in the heads) and gives every feature. It's a different topology from our heads, so it takes the closest-point path below. Its parts are subtle (hooded eyes, heavier brows, a thinner mouth, a narrower jaw, hollower cheeks).- Unpacking (not in git, 2.4 GB): an
.mhpkgis a zip whoseManifest.jsonnames its package root (/Game/CharacterAssemblies/<Name>/...). Extract each one's.uassetfiles intoContent/CharacterAssemblies/<Name>/, where<Name>is the zip's top folder (Sentinel,SentinelB,SentinelC); each package carries its ownCommon/copy, so nothing outside that folder is touched. The baked morphs live in the heads, so only re-running FaceMorph needs the packages; the game needs them only for the two hair options they supply (twisted braids, afro fade), which show nothing without them. bDefault(optional, False if missing) makes a row the default face for its sex: not a numbered version, never rolled. Each feature shows it underneath the picked version, at weight 1 minus that version's strength (FFaceFeatures::DefaultFor,AColonistCharacter::ApplyFaceFeatures), so strength 0 is the default face's part.PolyartFandPolyartM(the Polyart pack'sSK_Female_Head/SK_Male_Head) are the defaults; Kavya (Ref2) is version 1 for women.PolyartMgives no Eyes or Cheeks: its stylized eyes have no eyeball skinning to fit by, and its lids landed as heavy bags.-ColonyNoDefaultFaceturns the default layer off (before/after tests). Bake them like any reference:-run=FaceMorph -Source=<head> -Ref=PolyartF -Save.Features(optional) lists the features a reference gives, separated by|(Eyes|Ears|Nose|Mouth); empty means all. FaceMorph writes morphs only for those, and colonists roll that reference only for them. A feature's versions are numbered within the references that give it (FFaceFeatures::RefsFor,Label(Ref, Feature)).- Each feature is rolled separately: the head's own shape has weight 2 (
FFaceFeatures::DefaultWeight), each allowed reference itsWeight, and a reference gets a 40-100% amount. So a colonist can have nose 2 at 60% and their own mouth. Several references can mix.
- Unpacking (not in git, 2.4 GB): an
- How it works: each reference is a set of morph targets on every head, one per feature (
Ref2_Nose,Ref2_Eyes...).AColonistCharacter::ApplyFaceFeaturessets their weights on the face. Data:FColonistAppearance::Features, morph name (Ref2_Nose) to weight 0-1; missing means 0. - Building the morphs: the
FaceMorphcommandlet (FaceMorphCommandlet.h/.cpp, editor only) writes them into the 8 heads (all 3 detail levels): ``UnrealEditor-Cmd ColonySim.uproject -run=FaceMorph -Source=<reference face mesh> -Ref=Ref2 [-Save]``- MetaHuman faces share one topology, so vertices match by texture coordinates, falling back to the nearest vertex.
- A face from another pack (different topology, under half matched by UV) lands each of our vertices on the closest point of its surface within 3 cm, and the moves are smoothed over our mesh so its triangles don't print through. A reference with fewer detail levels serves all of ours from its last one.
- Only the reference's skin (its largest mesh section) is landed on. Its eyeballs, eye shells, lashes and teeth sit millimetres from our lids and lips, and landing on them pulled lids wide open or shut.
- The eye area lands in the reference's eye frame: our point is moved from our eye to the reference's, and scaled by its eyeball radius over ours, before the closest skin is found; the result is brought back the same way. Eyes are found from the eyeball meshes (a least-squares sphere through each eyeball section, the smallest good one), not the eye bones. The MetaHuman character packages (Ref4-6) have their eyes 1-2 cm lower on the face than ours and eyeballs about 1.5x the radius, so landing first and re-centring afterwards matched our lids to the skin above their eyes.
- The MetaHuman character packages (Ref4-6) take this path too: only about 13% of our head vertices match theirs by UV, while Kavya (Ref2) matches 98%.
- Every morph the run writes fades to nothing near the face's open neck edge, so the neck seam stays closed. Morphs of other references are left alone. Non-eye features and shapes stay out of the eye sockets.
- The reference is fitted onto each head (move plus uniform scale), so only shape differs. Kavya's face is 12-25% smaller than ours; after fitting, the faces differ by a median of 0.6-1 cm.
- Regions come from the rig's skin weights, are softened over the mesh, and never include the neck (the seam with the body stays closed) or the eyeballs.
-Only=Head|Earswrites just those features of the reference and keeps its other morphs as they are (used to add the head shape to every reference without re-baking the rest).-ShapesOnly(no-Sourceneeded) rewrites only theShape_*morphs, after a shape rule changes; every reference's and age morph stays. Only the morphs a run writes are pinned at the neck seam again.- Without
-Saveit only reports. Re-running replaces the morphs. To add a face: put its built MetaHuman face mesh in a newFaceRefs.csvrow (Ref3...) and run the commandlet with-Ref=Ref3.
- Eyes stay seated: eyeballs (the vertices on
FACIAL_[LR]_EyeParallel) are never morphed; they follow the eye bones. A reference's lids and sockets are re-centred on the head's own eyeballs. Three shape morphs, the same on every head, keep sockets and eyeballs together: | Morph | What it moves | The game also | |---|---|---| |Shape_EyeHeight| the socket area 1 cm up per unit (full strength within 2 cm of the eye centre, fading out by 3.6 cm) | moves the eye bones up by the same amount | |Shape_EyeSize| the socket area scaled about the eye centre, weight = eye size - 1 | scales the eye bones by eye size | |Shape_BrowHeight| the brow ridge region 1 cm up per unit | - | - Sliders (Pawn tab and Character Designer, "Face features"): one row per feature:
<>picks the version ("Nose: 0" is the head's own shape, 1 the first reference face, 2 the next) and one strength slider (faint to strong, hidden at 0); then eye height (-0.5 to +0.3 cm) and brow height (-1.5 to +1.5 cm). Sliders show their two ends in words, never numbers. Eye size is the existing slider; it now scales the whole socket. - Limits: only the shape changes. The skin texture and normal map stay the head's own, so a strongly reshaped feature can show the original's creases slightly out of place.
- Kavya's build:
/Game/Characters/MetaHumans/Kavya(44 assets, 142 MB) is only a source for the morphs now. Its body has holes where her outfit was (the MetaHuman build deletes hidden skin); the face is unaffected. MetaHuman builds need the live editor: the texture bake doesn't run headless. - Test flags (with
-ColonyDesignerTest):-ColonyDesignerFeatures=Ref1|Ref2|Eyes:Ref2sets every feature, or one, to that reference;-ColonyDesignerEyeHeight=<cm>,-ColonyDesignerBrowHeight=<cm>and-ColonyDesignerEyeScale=<x>set the sliders;-ColonyDesignerSeed=<n>keeps the same preview colonist across runs.
Shape sliders
Limits: FColonistAppearance::Clamp puts every appearance value back in its slider range before anything is applied (ApplyAppearance, ApplyFaceFeatures, ApplyBodyShapes), so a value pushed out of range by a mod, an age change or an old save never reaches the meshes.
Every colonist has shape sliders from -1 to 1 (FColonistAppearance::Shapes, by name). They're rolled with small random values (posture starts upright) and edited in the Pawn tab and the Character Designer, under "Face shape", "Body shape" and "Body frame". The list is FFaceFeatures::FaceShapes, BodyMorphShapes and BodyBoneShapes (FaceFeatures.cpp); each has a low and a high end.
Face shape (morph Shape_<Name> on the 8 heads, made by the FaceMorph commandlet from the feature regions and the face landmark bones; eyeballs and the neck are never in them):
| Slider | Low | High |
|---|---|---|
| FaceWidth, FaceLength | narrow, short | broad, long |
| JawWidth, JawAngle, JawDepth | tapered, soft, back | square, angular, forward |
| ChinProjection, ChinWidth | recessed, pointed | prominent, broad |
| Cheekbones, CheekFullness | flat, hollow | pronounced, full |
| EyeSpacing, EyeTilt | close, outer corners down | wide, up |
| BrowRidge | soft | prominent |
| NoseLength, NoseWidth, NoseHeight, NoseProfile | short, narrow, low, concave | long, broad, high, convex |
- Nose length is how far the nose stands out: the tip and lower bridge move forward, most at the tip, fading out 4 cm from it. Nose height turns the tip up or down; only the lower nose moves. (Both used to move the whole nose mask, so it slid and smeared the cheeks and lip.)
| MouthWidth, MouthDepth | narrow, back | wide, forward |
|---|---|---|
| UpperLip, LowerLip, LipFullness, LipBalance | thin, thin, thin, lower fuller | large, large, full, upper fuller |
| EarProminence | flat | sticking out |
| NoseRidge | sharp | soft |
| EarRotation | own angle | turned out |
- Nose ridge softness works across the bridge, between the tip and the top of the bridge: soft takes the centre line back and brings the flanks forward and out, so the ridge rounds off; sharp is the reverse, a narrow peaked ridge.
- Ear rotation (0 to 1, never rolled: a stylized pick) turns each ear about the front-to-back axis through its root (1.5 cm below the ear bone), tops out, 30 degrees at 1: "antenna" ears. The sweep shoots it head-on.
Slider limits: each FShapeSlider has its own Min and Max (most -1..1). Wider ones: EyeTilt to 1.1, NoseLength -1.15..1.15, NoseWidth to 1.1, EarProminence to 1.15, BreastSize to 2 (the morph past full strength), Muscle 0..1, HandSize and FootSize 0..1. The panel, Clamp (values first to -2..2, then each slider's range), the roll, the morph weights and the sweep all use them. Head size is 1.2..1.5 and eye size 1.0..1.32 (FColonistAppearance::HeadScaleMin/Max, EyeScaleMin/Max; the world rules clamp to the same).
- Hand size, Foot size (body bone sliders): 0 is the body's own size, 1 is twice as big (
hand_l/r,foot_l/rbone scale inUpdateFace). Rolls start them at 0. Feet grow only in length and width (FColonistBoneMod::AxisScale: the foot bone axis nearest vertical in the rest pose keeps its size), so the sole stays on the ground. - Dev: show slider values and limits (a tick at the top of the Character Designer,
ColonyHUDCommon::ShowSliderNumbers;-ColonyDesignerNumbersturns it on for tests): every slider shows its value and its min and max under it.
Eye spacing moves the eyeballs with their sockets, like eye height. Eye size, eye height and brow height are the earlier sliders.
Body shape (morphs on the 8 bodies, made by -run=BodyGraft -BodyShapes [-Save]; the neck and head never move):
- Muscle definition (0 to 1, under Muscularity; not a morph):
AColonistCharacter::ApplyBodySkinDetailputs the body's own baked MetaHuman normal map (Normal Bakedon the body asset's material) into the stylized skin'sNormalMapand setsNormalStrengthto the slider, so the muscle lines come back as shading whileShape_Smoothkeeps the mesh smooth. Sentinels get at least half (Lerp(slider, 1, 0.5)). The face mesh stays flat. - Nipples (not morphs):
ApplyBodySkinDetailalso sets the stylized body skin'sNippleStrength,NippleL/NippleR(u, v, radius in UV) andNippleColor(0.5, 0.3, 0.28, multiplying the skin): procedural areolas, 1.4 cm (men) or 2 cm (women) across, in the MetaHuman body UV layout. Every body and both sexes share it: the centres are (1.5538, 0.2104) and (1.4462, 0.2104) in UDIM tile 1002, about 0.0072 UV per cm, up is -V (-run=BodyGraft -NippleUVmeasures them from the average bodies' breast apex; the face collar ends 11 cm above). Shown on adults only: men always, women when the underwear is off (so never in streamer mode); never children. Nipple spacing and Nipple height (-1..1) move them up to 1.5 cm.UpdateAnatomyrefreshes them when the underwear changes. Women with the underwear off also show the anatomy wearable's donor nipples on top (darker).- Tests:
-ColonySkinLoglogs the parameters per body slot;-ColonySkinDebugUVpaints the body's UVs (material scalarDebugUV: R = u, G = v, B = half the UDIM tile).
- Tests:
- Breast physics (
UpdateBreastPhysics, every tick, adult women on screen): a damped spring (about 3 Hz, a little slower for large breasts, damping 0.35) driven by thespine_05bone's vertical acceleration (smoothed), added to the Breast height morph weight (1.5 cm per unit) on the body, anatomy and underwear; at most 0.8 cm. Walking moves it about 3 mm on average (peaks 8 mm).-ColonyBreastLoglogs the offset and chest acceleration for tests. It sleeps while the chest isn't moving and the spring is at rest, sets the morph only when the weight changes by 0.003 or more, and checks for the chest bone once per body asset. - Mass (lean to heavy):
Shape_MassDowntoward the same sex's Slim body,Shape_MassUptoward Heavy. - Muscle (0 to 1, from the body's own build to athletic): toward the same sex's Athletic body. It starts at 0 (
FShapeSlider::Min, which the panel,Clamp, the roll and the sweep all use): a negative weight pushed the body away from the athletic one, which made the heavy body fatter. - Limits: neck length runs 0.5 to 1.6 (2 was a giraffe neck); brow height runs -0.6 to 1 cm (lower pushed the brows into the eyes).
- How mass and muscle are built: the other body's vertices are carried through every bone that moves the vertex, weighted like the skin, out of the other skeleton and into ours. Only the soft-tissue shape transfers, not the other body's bone proportions.
- BreastSize, BreastHeight, BreastSag, BreastFullness (women only): generated around each breast's apex: on the average body the most forward chest point 9-13 cm from the midline, on the others the point with the same UV (the most forward point would be the belly on heavy bodies). Only front-facing chest skin moves, never the arm.
- Shape_Smooth: Taubin smoothing of the whole body (hands and feet kept), to take out sculpted muscle and rib bumps.
-run=BodyGraft -BakeSmooth -Savefolds it into the base mesh of every body, anatomy and underwear (all LODs) and removes the morph, so it isn't blended every frame;ApplyBodyShapesblends it only into a mesh that still has it. After the bake,-BodyShapeswrites it again only with-Smooth(it would smooth twice). The skin materials also get flat normal maps (T_Flat_N). - Shape_Sentinel (the
Sentinelshape, 0 to 1): a late-game transformation, never rolled for new colonists. It's built by-run=BodyGraft -BodyShapes -Sentinel=<obj> [-Save]from a sentinel body OBJ (Maya Y-up;Saved/Sentinel/Sentinel_Body.obj, not in git), scaled to each body's height. Without-Sentinel=, the morph stays empty, so pass it on every-BodyShapesrun.- Hands and feet keep their shape by their own bones' skin weight (hand, fingers, foot, ball, toes), softened four rings over the wrist and ankle: no limb bulk and no landing on the model there. (They used to be kept only within 5-10 cm of the wrist and ankle bones, so the fingers and toes landed on the model's T-pose surface: the broken sentinel hands and feet.)
- Shape_Mercenary (male bodies;
-run=BodyGraft -BodyShapes -Sentinel=<obj> -Mercenary=/Game/Mercenary_Male_WaveforceStudio/Mesh/Modular/SK_MER_ME_Body.SK_MER_ME_Body [-Save]): the Mercenary (Waveforce Studio) build, made the same way as the sentinel shape from his skeletal mesh.ColonistBodies.csvBodyShapenames a shape a row always wears at full weight (adults only):MaleAthleticwearsMercenary, so his body replaces the muscular male body.- His musculature:
-run=BodyGraft -MuscleNormal -From=<his mesh> -Map=/Game/Mercenary_Male_WaveforceStudio/Texture/T_MER_ME_Body_N.T_MER_ME_Body_N -Bodies=Athletic_Male -Slots=Torso,UpperArm,LowerArm,Hand,Leg,Foot [-Size=2048] [-Save](his body's sections;-Slotspicks material slots by name, defaultBody) carries his tangent normal map into our body's UV layout (per texel: our skin point landed on his skin, his normal through his tangent frame into ours; his groin and anything over 3 cm from his skin stay flat; 82% of our skin lands on his) as/Game/Characters/MuscleMaps/T_MuscleN_Athletic_Male. TheMuscleNormalcolumn (MaleAthletic,MaleSentinel) makes it the map the Muscle definition slider shows. - His face:
FaceRefs.csvRef8(men) is his head; FaceMorph lands on a reference's section whose material slot is named Head or Face before its biggest one (his body is the biggest). - The Sentinel body type:
ColonistBodies.csvrowsMaleSentinelandFemaleSentinel(the female variant) use the athletic meshes withbSentinelTRUE: the morph at full strength, 17% taller and 15% broader shoulders. Weight 0, so new colonists never roll it (rolling skips weight-0 rows); it's a late-game body. In the designer it's a fifth body-type button, greyed out below adult age.GetSentinel()reads the row and returns 0 below adult age whatever the data says, so a child never shows it (height, shoulders or morph).
- His musculature:
- "Barbie" neck socket (
-run=BodyGraft -NeckSocket [-Save]): bodies getShape_NeckPlug(their own neck reaching up into the skull) and faces getShape_NeckTuck(the face mesh's neck pulled up inside the head), so the face-body join sits hidden inside the head at any head size or age. Both morphs are built but run at 0 in game: the body meshes have no neck of their own yet (the face mesh carries it), so it needs a separate neck piece first. -run=BodyGraft -SmoothSkin [-Save]makes smoothed copies (<name>_Smooth) of every face and body skin base colour inColonistBodies.csvand points their materials at them. The originals stay, so pointing the parameters back undoes it.-run=BodyGraft -Probelogs the anatomy donor mesh's (the Roxy full body, fixed in code) LODs, bone count, skeleton and bounds, then quits (a quick check before a graft).- De-spike: after a mass or muscle transfer, a vertex that moves more than 1.5 cm differently from its neighbours' average takes that average (a mismatched vertex otherwise makes a spike).
- Buttocks: generated around the most backward hip point (about 11 cm).
- AnkleThickness, WristThickness: the limb thickened away from its axis around the joint.
Body frame (bones, in the body's animation layer UColonistAnimInstance):
| Slider | What it does |
|---|---|
| ShoulderWidth | stretches the upper arms' offset from the clavicles (±18%) |
| HipWidth | stretches the thighs' offset from the pelvis (±20%) |
| TorsoLeg | spine 2-5 longer and lower legs shorter (+12% / -10%), or the reverse; the mesh moves so the feet stay on the ground |
| Posture | bends spine 3-5 forward (up to 7° each); the neck tilts back to keep the gaze |
Height and head size are the earlier sliders.
- Wearables follow: the anatomy and underwear wearables copy every body morph from the body vertex under each of their vertices (BodyGraft does this when it builds them). Rebuild them after rebuilding the body shapes.
- Order to rebuild: FaceMorph (heads),
BodyGraft -BodyShapes(bodies), thenBodyGraftandBodyGraft -Underwear(wearables). - Test:
-ColonyDesignerTest -ColonyDesignerNeutral -ColonyDesignerShapes=Mass:1,BreastSize:-1. Neutral zeroes every slider, then the listed ones are set. - Limits: generated shapes are approximations, not sculpts; extremes can look rubbery. The skin texture doesn't change with the shape.
MetaHumans
The Ultimate MetaHuman Body Bundle is in /Game/Fab/MetaHuman: 15 MetaHuman Character assets (MHC_*), Baby to Elder.
- Assembling (scripted): they're turned into game assets by
Scripts/metahuman_build.py:py "<project>/Scripts/metahuman_build.py" MEDIUM /Game/Characters/MetaHumans /Game/Fab/MetaHuman/MHC_Base_Female ...It opens each character for editing, then runs the Optimized pipeline at the given quality. - Output: per character, a body mesh (
metahuman_base_skel), a face mesh (its own face skeleton), a physics asset, 23 textures, 9 materials and aBP_<Name>actor. No hair grooms: the bundle's characters are bald. - Timing: 3 minutes for the first character, 30–90 s after that.
- Neck seam (the face mesh holds the head, neck and a collar over the shoulders; the body starts below):
- Pose: every bone the face shares with the body (spine, clavicles, neck, head) takes the body's rendered component-space transform each frame (
UpdateFace), and the face's own bones follow from their reference pose. Copying local transforms by name (CopyPoseFromSkeletalComponent) drifted 4-5 cm from what the body drew after about a minute, and the seam opened. - Weights:
BodyGraft -WeldSeam [-Save]gives the face's seam vertices (within 1 cm of the body, all LODs) the body's skin weights, bones matched by name. Without it the edges moved apart whenever torso-to-leg, posture, neck length or head size changed the spine, neck or head. Run it after FaceMorph, which rewrites the face meshes. - Life-stage bodies (baby to elder) are worn under the ADULT face, whose collar the age morphs keep pinned, so the stage body's neck opening didn't meet it.
BodyGraft -StageSeam [-Save]builds, on every stage body, one morphSeam_<face asset>per adult face it can be worn with (same sex; every face for "Any" rows). The face's collar is placed on the stage skeleton the way the game poses it (each vertex through its own bones), and the body's opening is tucked up under the collar's skin, 1-2 cm above its bottom edge, fading out over 6 cm.ApplyBodyShapesturns on the morph for the face being worn. Moves are up to about 8 cm (babies), 5-7 cm (children) and 2-4 cm (teens, elders). Rerun it after rebuilding stage bodies or adult faces. Left over: the adult collar is wider than a child's shoulders, so its outer ends can show small outline specks. - NeckHide (prepared, off):
-WeldSeamalso writes a gradient into the faces' vertex colour R (1 at the collar's edge, 0 by 10 cm up), and the face skin (MI_LyridiaSkin_Face, masked) can fade the collar out by it with theNeckHidescalar.FColonistBodyRow::HidesFaceCollardecides which stages use it; it returns false for now. Tried on babies to teens, it faded their whole short neck, not only the yoke that overhangs, and left the head floating. It needs a radial gradient (distance out from the neck's axis) first. Two other attempts at the child collar, a per-stage collar morph by nearest skin and a per-axis fit, folded the jaw and were removed.
- Pose: every bone the face shares with the body (spine, clavicles, neck, head) takes the body's rendered component-space transform each frame (
- Neck seam colour:
BodyGraft -SkinSeammeasures each body row's baked skin colour on both meshes where face and body meet (vertices within 0.5 cm of the other mesh) and prints aFaceSeamTintfor ColonistBodies.csv. The face's skin base colour is multiplied by it (white = none, the default). The bundle's faces and bodies already match at the seam (within 0.5%), so no row sets it; it's there for faces or bodies from other sources. A colour step seen in game between the neck and the shoulders is the lighting (cool sky on the front, warm sun on the sides), not the textures. - Size at Medium quality: | Mesh | Vertices (LOD0 / LOD1 / LOD2) | |---|---| | Body | 8,568 / 3,974 / 1,239 | | Face | 4,065 / 903 / 243 | The face mesh has separate material sections for each eye, the teeth, the eyelashes and the eye shell.
- Shared topology: Base, Slim and Heavy assembled to identical vertex counts on every LOD, and Athletic matches at LOD0. So the bundle's bodies can become body-type morphs on one base.
- Inspecting:
Scripts/metahuman_inspect.py <folder>logs a summary of an assembled folder. - Don't save the sources: opening a
MHC_*source for assembly marks it dirty. Don't save those; they're the purchased originals.
Body parts and health
The body plan is data, in Content/Data/BodyParts.csv (plus mod overrides by row name). The spec, and the renames from the RimWorld reference, are in Docs/Design/BodyParts.md.
| Column | Meaning |
|---|---|
| Name | Part id (Arm) |
DisplayName | Shown name; sided parts become "Left arm" / "Right arm" |
Parent | Parent part id; empty = the root (Torso). A sided part under a sided parent attaches to the same side |
Sides | LR = one part per side; empty = one part |
Kind | External, Internal (organ), Bone, or Slot (equipment slot like Waist, no health) |
MaxHealth | Damage before the part is destroyed |
Sex | Any, Female or Male: which bodies have it (Breasts is female only) |
Region | Where the health diagram draws it: Head, Neck, Torso, Shoulder, Arm, Hand, Leg, Foot |
bVital | Losing it would be fatal (death isn't simulated yet; the health tab warns) |
The human plan has 67 parts from 43 rows.
UColonistHealthComponent holds each colonist's injuries and lost parts. It uses tree logic: a part is present only while every part above it is, so losing the head also loses the skull, brain, eyes, ears, nose, jaw and tongue. An injury that reaches a part's MaxHealth destroys it. Overall health is the health of every part, weighted by MaxHealth, with lost parts counting as 0. Bleeding, healing over time, pain, death, prosthetics and combat are not built yet.
Damage types
Content/Data/DamageTypes.csv (mods add or replace rows by Name; FDamageTypes, DamageTypes.h/.cpp) lists 47 damage types, from the RimWorld reference: cutting and piercing, blunt, projectiles, natural, toxic, explosions, heat and energy, environmental, psychic and nerve, stun, medical, biological, area utilities, and object damage.
| Column | Meaning |
|---|---|
Group | For lists only |
Armor | Which armor stops it: Sharp (slashing), Piercing, Blunt, Heat, or None |
Wound | Leaves a wound on a body part. FALSE for effects and utilities (stun, EMP, smoke, extinguish, mining, rotting...) |
InjuryLabel | The wound's name in the health tab (Gunshot, Stab wound) |
ScarChance | Chance (0-1) the wound leaves a permanent scar when it heals |
Notes | Free text |
- Piercing is ours: RimWorld has Sharp, Blunt and Heat. Bullets, stabs, arrows and bites pierce, and they scar most (0.5-0.7, against about 0.2-0.3 for slashes).
- Applying damage:
UColonistHealthComponent::AddDamage(Part, Type, Amount)adds a wound named by the type (bones always crack) and remembers the type on the injury (FInjury::DamageType). Non-wounding types add nothing. - Scars:
FDamageTypes::RollScar(Type)rolls the scar chance (for healing, once it exists). A new colonist's old scars pick their damage type weighted by scar chance ("Old gunshot scar", "Old stab wound scar"). - The dev "injure" button and
-ColonyHealthTestuse damage types.
Combat (melee, first version)
A rudimentary melee system with zombies to test it on (ColonyCombat.h/.cpp, ColonyZombie.h/.cpp). Its layout follows the module split in the Combat Extended reference (Lyridia/Combat_Extended_Combat_Reference.md, section 15): move selection, a resolve step, the injury step, and an attack log. Ranged weapons, armor, ammo and suppression come later.
- Fighters:
UColonyCombatComponent.- Colonists get one at runtime from
UColonyCombatSubsystem; zombies have one from the start. Faction(ColonyorUndead) decides who is hostile to whom.- Colonist blows wound
UColonistHealthComponent(a random outer part, weighted by the part's health). - Others use plain hit points.
- Colonists get one at runtime from
- Animations: combat moves play on the body's single-node animation.
- While one plays,
IsActionPlaying()is true and the owner's locomotion update leaves the body alone.AColonistCharacter::UpdateAnimationchecks it. - Downed colonists hold their death pose.
- While one plays,
- A blow, step by step (
LandBlow, at the move'sHitAt):- Reach: the target must be within the move's
Rangeand in front, or it's aMiss. - Parry: a defender that faces the attacker and isn't swinging turns it aside with
ParryChance(Parried). - Crit: with
CritChance, blunt blows stun (StunSeconds: no attacking or dashing) and sharp or piercing ones dealCritDamagetimes more. - Armor: not yet; colonists wear none. The step is marked in the code.
- Injury: a wound or hit points. A hit reaction plays with
HitReactChance, unless the target is mid-swing.- Where a colonist is hit: a body region first, by the
HitRegion<Region>weights (torso 35, leg 20, arm 16, head 8, shoulder 6, hand 6, foot 6, neck 3), then an outer part in that region by its health. - So a hand and its fingers share one small slice of the hits, instead of fingers and toes taking most.
- Where a colonist is hit: a body region first, by the
- Reach: the target must be within the move's
- Outcomes:
- Colonists go down when a vital part is lost, overall health falls below
DownedHealth, or their fresh (not old, permanent) wounds add up toDownedDamage. The last stands in for pain and blood loss until those exist. Overall health is weighted by part size, so it barely moves under many small wounds. - Going down is incapacitation: the colonist passes out where it stands. It plays a fall clip (
Kind = Death) and stays on its last frame, lying there. There's no physics: capsule and mesh don't move, and zombies leave downed colonists alone. - Death isn't simulated yet. Once healed (dev "heal all"), the colonist gets up on the spot.
- It plays a
GetUpmove ifCombatMoves.csvhas one; the packs have none yet, so the fall clip plays backwards (a little slower). - Movement returns when the clip ends. Until then
IsBusystays true, so the taskmaster waits.
- It plays a
- Hair while down: hair is rigid (its rest shape), which would stick out once the head lies sideways. While down, it hangs instead (
SetHairDrape):- rigged hair meshes simulate their bones below
headon their own physics asset, with the pack's swing limits freed (they assume a standing head); - grooms turn strand simulation on. Both go back to rigid on getting up. Checked in game with the Roxy hair mesh; grooms not yet.
- rigged hair meshes simulate their bones below
- Zombies feel no pain: they fall for good only when a vital part goes or their fresh wounds reach
ZombieDownDamage. They play the same falls, lie thereZombieCorpseSeconds, then go.
- Colonists go down when a vital part is lost, overall health falls below
- Limb loss (colonists and zombies):
- A blow that pushes a part to its maximum damage destroys it; a sharp critical blow on an arm, hand, leg, foot, finger or toe cuts it off (
Severedin the attack log). - Lost limbs vanish from the body: each lost limb's top bone is scaled to nothing at its joint (
ApplyLimbLoss,LimbBonemaps body parts to MetaHuman bones). These mods live in the anim instance's ownLimbModslist, applied after the colonist'sBoneMods, so tick order doesn't matter. The list is rebuilt only when the health revision changes (wounds, loads, healing) or the body swaps (a new anim instance). - A colonist missing a leg goes down and stays down. A zombie missing a leg crawls (the pack's crawl, 35% speed).
- A blow that pushes a part to its maximum damage destroys it; a sharp critical blow on an arm, hand, leg, foot, finger or toe cuts it off (
- Damage from the world (lava, fire):
ReceiveEnvironmentDamage(Type, Damage). It wounds and downs like a blow, but with no attacker, hit reaction, fighting state or attack-log entry, and it keeps wounding a downed body. - Authority: only the simulation commits damage (
ColonyNet::HasSimAuthority). Clients see the swing. - Replays: every roll (part, damage, parry, crit, flinch) comes from one random stream per world, seeded from the world seed.
- Attack log: every resolved blow is recorded as an
FColonyAttackResult, logged asAttack ...and kept for the last 200 blows (GetAttackLog(),OnAttackResolved). It records attacker and target ids, move, damage type, outcome (Miss,Parried,Hit,Crit,Stun,Downed,Killed), body part, damage, time and seed. - Colonist behaviour:
- Colonists fight back against enemies within
EngageRadius: they walk up and swing. - Below
FleeHealththey dash away and run. - While fighting or downed,
UColonyCombatComponent::IsBusy(Colonist)is true, so the taskmaster should leave them alone.
- Colonists fight back against enemies within
- Possessed colonist: left mouse = attack (whatever hostile is in front), Left Shift = dash (the way WASD point, or forward), G = gesture.
- Actions (the action layer, so a multiplayer host decides):
Combat.Attack(Attacker, optionalTargetandMove),Combat.Dash(Agent,Dir),Combat.SpawnZombies(Count,Nearcell). - Console:
colony.SpawnZombies [count](around the possessed, selected or first colonist),colony.KillZombies. - Zombies (
AColonyZombie, a subclass ofAColonistCharacter):- Body: a colonist body of the
Zombiexenotype (Data/Xenotypes.csv), plain (bPlainLook): the Average body and default face for its sex, no sliders, no ancestry, with rolled hair. The xenotype's look goes on top (greener, greyer, milky eyes, a stoop throughShapeOverrides). - No outline: only colony members get the ink outline (
ColonyArcane::InkCharacters). - Shamble: the WWG zombie pack's walk, retargeted onto the MetaHuman body (
BS_ZombieWalk_MH, one axis: speed). - Hunting: zombies go for the nearest colonist within
AggroRadiuson the colonists' own voxel paths, with at mostZombiePathsPerFramepath searches per frame across all zombies. With no path, they lurch straight at it. - Ages: adults only (rolled like a colonist's, 18-55 scaled to the lifespan), unless the world option Child zombies is on. Then about a third are children and teens (5-17, scaled), each with its stage body, and the children's hard-coded rules hold: no Sentinel, no anatomy wearable, underwear on.
- The option is picked on the new game screen (
FColonyGameSettings::bChildZombies).UColonyCombatSubsystemkeeps it and saves it in its own "Combat" save section. SpawnColonistAtwith the Zombie xenotype respects it too:bAdultOnlyinXenotypes.csvapplies only while the option is off.- The test flag
-ColonyChildZombiesturns it on in-ColonyCombatTest, which logs each zombie's age and stage and takes a close shot of the youngest.
- The option is picked on the new game screen (
- Not colonists: zombies are never the colony's to command (
IsBusyis always true for them, so the taskmaster leaves them alone). - Saved: living zombies are kept in their own "Zombies" save section (
SerializeZombies). It holds the colony id, place, identity, sex, look, age and wounds, and a load brings them back with the same ids. Corpses aren't kept. The colonists' loader only replaces colony members, so a section for another kind of enemy works the same way.-ColonyCombatTestchecks it at 30.5 s: it captures through bytes, applies the snapshot to the running world, and logs "zombie save round trip PASSED" when the same ids come back with the same names, xenotypes, health and bodies.
- Body: a colonist body of the
- Data:
Content/Data/Combat.csv(Key,Value,Notes): the tuning above, plus dash speed, time and cooldown, and zombie down damage, speed, corpse time and spawn distance, and the crowd far distance and tick interval.- Every clip in
CombatMoves.csv(and the zombie crawl) is loaded in the background when the combat subsystem starts (ClipPreload), so the first swing, fall or get-up doesn't hitch. Content/Data/CombatMoves.csv: one row per move, with columnsUser(Colonist/Zombie),Kind(Attack,HitReact,Death(the fall into lying down),Dash,Gesture,GetUp),Animation,DamageType,Damage(±20%),Range(cm),HitAt(0-1 of the clip),PlayRate,CooldownandWeight.- Rows of the same user and kind are picked by weight; mods add or replace rows by
Name. The sword swings are in with weight 0, for when weapons can be held.
- Animations (
Scripts/retarget_combat_anims.py, run headless like the locomotion retarget):- UE5 Mannequin attacks, dash, 8 hit reactions and 6 deaths, plus RamsterZ Free Anims Vol. 1 (UE4 Mannequin) punches, kicks, shove, palm strike, sword swings and gestures, retargeted onto the MetaHuman body in
/Game/Characters/Animations/MetaHuman/Combat(suffix_MH). - The zombie pack's walk, injured walks and crawl go onto the MetaHuman body in
/Game/Characters/Animations/MetaHuman/Zombie(RTG_Zombie_To_MetaHuman). Zombies swing and flinch with the colonists' clips, slower. - Everything plays in place; the script writes each clip's length to
Saved/Logs/combat_anims.jsonfor tuningHitAt. - The RamsterZ clips use the old engine-default bone compression, whose codecs UE 5.8 lacks: compressing a copy asserts on
RotationCodecand crashes the editor. The script points the sources atDefaultRecorderBoneCompressionin memory only (the pack's files aren't saved), so the copies get a working codec. - Falls: three RamsterZ victim clips that end on the ground (neck break, knife takedown and groin kick), played faster (
PlayRate1.3-1.8). Their root motion is locked, so the colonist falls on the spot.- The script logs each fall's head height on the colonist's last frame: 3-7 cm, lying.
- The UE5 Mannequin "death" clips aren't used: their pelvis never drops below ~85 cm (they're slumps meant to hand off to a ragdoll), which is what looked like a half-crouch.
- The packs have no dedicated lying loop or get-up clip yet. The zombie pack's
AS_ZombieCrawlIdle(prone, head at 21 cm) could become a lying loop.
- No roll or slide animation exists in the project's packs yet;
MM_Dashis the only quick move.
- UE5 Mannequin attacks, dash, 8 hit reactions and 6 deaths, plus RamsterZ Free Anims Vol. 1 (UE4 Mannequin) punches, kicks, shove, palm strike, sword swings and gestures, retargeted onto the MetaHuman body in
- Test flag:
-ColonyDevTest -ColonyCombatTest.- It spawns two more colonists and four zombies, and checks the dash and attack actions on the newest colonist.
- It reports every 2 s from a close camera on the first colonist, takes six screenshots (no UI), prints a summary and quits.
- At 20 s, if nobody is down yet, a heavy blow takes the first colonist down, so the downed path always runs.
- At 27 s it heals anyone down; at 30 s it checks they're up again (not downed, no physics, walking).
- Result on 2026-10-17: both actions executed, and zombies hunted and hit torso and limbs. One zombie was killed. The first colonist went down and lay curled on the spot (2 cm from where she stood); the zombies left her alone. After healing she got up (the fall played backwards) and walked on.
- Not yet: ranged weapons and projectiles, armor and penetration, held weapons, ripostes, suppression, bleeding and death, combat jobs in the taskmaster (drafting, rescue).
7. Game flow, controls, HUD and dev panel
- UI style (
ColonyUIStyle.h/.cpp): every screen uses one shared look, after the "Cypher" HUD design.- Palette: graphite panels (
ColonyUI::Surface(),Raised(),Paper()), white frame lines and text (Ink(),Ink2(),Ink3()), and one accent colour (ColonyUI::Accent()) for hover, current items, numbers and primary buttons. Colours are read when drawn, so changing the accent updates open screens at once. - Accent: gold (
#C9A45C) by default, which is what the game ships with. Players pick another in Options → Accent colour: eight swatches (Gold, Ember, Signal, Rose, Violet, Cyan, Jade, Silver) or a custom hue. The choice is saved inGameUserSettings.inias[ColonyUI] Accent=RRGGBB.-ColonyAccent=RRGGBBoverrides it for one run (screenshots). - Type: the engine's mono face (DroidSansMono) for titles, numbers and readouts, uppercase and widely spaced; Roboto for body text. Items are numbered
01,02... in the accent. - Widgets:
SCyFramedraws the chamfered frames (Window,Card,Button,Key,Slim) with an accent mark and a soft glow; hover or selection turns the frame to the accent (eased, about 0.2 s).SCyButton(withActiveandPrimarystates),SCyWindow(a// 01 TITLEheader with a close key; it flies in each time it is shown),CySection(/// TITLErule),CyKey(key chip),CyReadout,SCySegmentBarandSCyHex(swatches). Stock sliders, checkboxes, spin boxes, text boxes, scroll bars and combo boxes takeColonyUI::*Style(). - 3D navigation (
ColonyUISpace.h/.cpp,SCySpace): a screen as a scene with depth. Stops (cards) float along a camera path; the camera flies between them (0.65 s plus 0.52 s per stop passed, at most 2.4 s, eased in and out). Stops fade in from the distance, turn to face the camera as it arrives and slide off as it passes. Dashed beams join them, a dotted sky drifts behind, accent brackets lock onto the current stop, and a stop menu along the bottom lists every stop with previous / next keys. Space / Shift+Space, the arrow keys, Page Up / Down and Home move between stops. A stop at rest has no transform, so its text stays sharp.-ColonyNoMotionmakes every flight instant.
- Palette: graphite panels (
- Start menu (
SColonyStartMenu,ColonyStartMenu.h/.cpp): a 3D space with five stops, 01 Lyridia (New Game, Dev Test, Character Designer, Mods, Quit), 02 Map size, 03 Colony (seed and aging speed), 04 Launch (a summary, the notice and Start) and 05 Mods (the mod list and the data editor; section 12).-ColonyMenuStop=Nopens on stop N (1-5).- New Game flies to the map size stop. Sizes are in voxels and are RimWorld's presets ×3: | Group | Sizes (voxels per side) | |---|---| | Small | 600, 675 | | Medium | 750 (default), 825 | | Large | 900, 975 | | Custom | width × length, 64–1024 each | Each size also shows its footprint in metres. There's a seed field with a Random button. Height is fixed at 192 voxels.
- Dev Test generates the small 256×256 district (seed 1337) right away.
- The colony stop picks the seed, the aging speed (see "Age and life stages" in section 6) and Child zombies (off by default; see "Combat" in section 6). Like the others, it is fixed once the world is made.
- While a world loads, the camera rests on the launch stop, which becomes the loading screen: the district size and seed, the step in progress, a bar over all steps and the time so far. The voxel work runs on worker threads (see "Threading" in section 3), so the screen stays responsive. Dev test runs (
-ColonyAutoTest,-ColonyDevTest) show it too.
AColonyGameModespawns lighting (a warm sun, sky atmosphere, and a sky light using the engine's daylight ambient cubemap) when play starts.StartGame(FColonyGameSettings)starts loading;AColonyGameMode::Tickmoves through the steps (EColonyLoadStep: Terrain, Features, Meshing, Applying, Warmup) and logs how long each took (Loaded 975x975 district in 9.35s: Terrain 4.76s ...).IsLoading(),GetLoadingText(),GetLoadingProgress()andGetLoadingSeconds()feed the loading screen.- Preloading: when loading starts, every body and face mesh in
Data/ColonistBodies.csv, the locomotion blend space and the fall animation start streaming in the background (FStreamableManager, high priority). Bodies added by mods are preloaded too. - Shader warm-up: after the terrain is uploaded, the first colonist spawns and one hidden mesh per body and face is drawn far below the world, so their shaders and pipeline states compile now instead of hitching when a colonist with that body first appears. Loading waits for the preloads, the shader compiler (editor builds) and the pipeline state precompile queue to finish: at least 0.5 s, at most 45 s. The screen shows how many compiles are left.
- Then the controller closes the menu, opens the HUD and focuses the camera on that colonist.
- Preloading: when loading starts, every body and face mesh in
RegenerateWorld(Seed)rebuilds the world and moves colonists back to the spawn.AColonyPlayerControllerhandles input and holds the state the UI reads.AColonyCameraPawnis the top-down camera.
| Input | Action |
|---|---|
| WASD / arrow keys | Pan |
| Middle mouse drag | Left/right rotates the view, up/down tilts it (-85° to -25°) |
| Q / E | Rotate |
| Mouse wheel | Zoom |
| Left click | Select colonist, or apply the active dev tool |
| Right click | Move selected colonist |
| F | Focus camera on selected colonist |
| Space | Pause / unpause |
| 1 / 2 / 3 | Game speed 1×, 2×, 3× |
| F1 | Toggle dev panel |
| Esc | Back to the Select tool |
| PageDown / PageUp | Cutaway view: slice lower / higher by 4 voxels |
| Home | Clear the cutaway |
| V | Take direct control of the selected colonist (or the one under the cursor); V again leaves |
Pausing and game speed affect the simulation only; the camera, UI and dev tools keep working.
Direct control (possession). V possesses a colonist for precise play. Leaving is instant, with no cooldown.
- On possession:
- The colonist's AI controller is destroyed; on release a fresh one is spawned with
SpawnDefaultController. - The colony camera pawn is kept, and refocuses on the colonist when you leave.
- Possessing while paused resumes the game at 1×.
- The colonist's AI controller is destroyed; on release a fresh one is spawned with
- While possessed: | Input | Action | |---|---| | WASD / arrow keys | Move relative to the view | | Mouse | Look (cursor hidden; pitch clamped to ±80°) | | Middle mouse (hold, third person) | Free look: the camera orbits the colonist without turning them; they keep walking where they face, and the camera swings back behind them on release | | Space | Jump | | Mouse wheel | Zoom from first person out to over the shoulder | | V or Esc | Leave | | 1 / 2 / 3 | Game speed (unchanged) |
- Movement:
- Ground speed (terrain × undergrowth) still applies.
- The colonist faces where you look (controller yaw).
- On release it goes back to turning toward its movement.
- Tuning (
AColonistCharacter):MaxViewDistance(400 cm),ViewDistanceStep(50 cm) andShoulderOffset(45 cm).- The jump:
JumpZVelocity350, about 2.5 blocks high, withAirControl0.35. AColonyPlayerController::LookSensitivity(2° per mouse unit).
- The camera:
- The camera is a
ViewArmspring arm at eye height on each colonist. It is only used while the colonist is the view target. - In first person the head mesh is hidden from the possessing player only.
- The camera is a
- Test flag:
-ColonyPossessTest(with-ColonyAutoTest) possesses the first colonist, jumps, screenshots over the shoulder and in first person, then releases it and checks that the AI walks again.
HUD (Slate; one file per screen: ColonyHUDWidgets for the HUD itself, ColonyStartMenu, ColonyColonistPanel, ColonyDevPanel, with shared helpers in ColonyHUDCommon.h; ColonyHUDWidgets.h includes them all):
- Calendar:
AColonyGameModecounts calendar days (GetGameDay) on game time, so 2x and 3x run it faster and pause stops it. The date ("Day 5, Year 1") shows in the top-left readout plate. - Character Designer (main menu): a preview colonist on a small white stage, panned to the left of the screen so the wide (560 px) panel on the right leaves it clear, with the Pawn tab's controls (sex, ancestry, skin, body type, height, head, eyes, neck, age, looks). It also has Whole body and Face camera framings and a turn slider. It runs without a world (
bPreviewcolonists have no AI, gravity or voxel checks).- Top row: Back (also Esc), Export and Exit game, on their own row so the panel width never hides them.
- Export writes the character to
Saved/Characters/<name>_<yyyymmdd_hhmmss>.json(AColonistCharacter::ExportJson):{"Format": "LyridiaCharacter", "Version": 1, "Name" (the nickname), "Id", "Nickname", "FirstName", "LastName", "Xenotype", "Sex", "BiologicalYears", "Appearance": {"Choices", "BodyType", "Features", "Shapes", "EyeHeight", "BrowHeight", "Ancestry", "SkinTone", "HeightScale", "HeadScale", "EyeScale", "NeckLength"}}. The panel shows where it went. There's no import yet. - Face view aims just below the eyes (
GetFaceCenter), so the whole face is framed at any head size. - Tests:
-ColonyDesignerTestscreenshots both framings (-ColonyDesignerWholemakes the second one whole body too, and-ColonyDesignerYaw=<degrees>turns the colonist);-ColonyDesignerSex=Femalepicks the sex, and the face test flags are under "Face features" in section 6. Also:-ColonyDesignerFrames=Ntakes N more shots 0.4 s apart, to watch the idle animation;-ColonyDesignerLiveHead=<scale>changes head size mid-idle the way the slider does;-ColonyFaceDebuglogs the face's spine, clavicle, neck and head bones against the body's every frame (they match: the face follows the body's pose exactly);-ColonyDesignerSweep(best with-ColonyDesignerNeutral) is the visual check of every slider. Bald, it shoots each one at its low and high end with no UI: face sliders and face feature versions from three-quarters, body sliders from the front, depth sliders (buttocks, posture, breast sag, fullness and height) from the side, reframed after each change. Each shot is logged in order asColonyDesignerSweep: shot <n> <slider> <low|high>, so a script can lay them out as contact sheets (low and high side by side, labelled). Face depth sliders (nose length and profile, chin projection, jaw and mouth depth) are shot from the side.-ColonyDesignerSweepOnly=A|Bruns just those sliders,-ColonyDesignerSweepStart=<s>waits before the first, and-ColonyDesignerSweepKeepHairkeeps the hair.
- Top: colonist portraits (
SColonyPortraitBar,ColonyPortraitBar.h/.cpp), one per colonist from the left, with the name under each face. Click a face to select that colonist; click it again within 0.4 s to also move the camera there. The bar hides in planet view and the character designer. The readout plate below moves down to make room (SColonyPortraitBar::ReservedHeight).- Pictures (
UColonyPortraitSubsystem,ColonyPortraits.h/.cpp): each colonist has a scene capture that sees only that colonist (body, face, hair, clothes; dark background), aimed level atGetFaceCenterfrom the colonist's facing, at 30 degrees FOV, into a 168 px render target. Post-process materials (the selection and ink outlines, whose edges are a fixed number of pixels wide) and eye adaptation are off in the capture. So are global illumination, reflections, ambient occlusion and dynamic shadows, most of a capture's cost and invisible at 168 px (colony.Portraits.CheapLighting 0turns them back on). - Stills by default: each portrait is re-shot every 3 seconds, at most 2 per frame, so new hair, age or wounds show up. Animated portraits re-shoot every portrait 12 times a second (
colony.Portraits.AnimatedFps; 0 = every frame). - The bar rebuilds its row only when the colonists with portraits or the settings change (
UColonyPortraitSubsystem::GetVersion). - Customize: the small
...button in the bar's corner opens a popup with face position (left/right), face height, zoom (0.6-2), portrait size (small 48, medium 64, large 84), Animated portraits (off by default), Reset and Done. Saved inGameUserSettings.ini, section[ColonyPortraits](OffsetX, OffsetY, Zoom, Size, Animated). - Test flags:
-ColonyPortraitCustomizestarts with the popup open;-ColonyPortraitsAnimatedstarts with animated portraits.
- Pictures (
- Top left, below the portraits: a readout plate (date, colonist count, FPS) with the game speed keys and zoom out and in keys (
-and+; each click is two wheel steps), colonist count and FPS. The camera zooms from 250 cm (MinZoom, close enough to see faces) to 6000 cm. - Above the bar, left: the selected colonist panel (
SColonistPanel, a Cypher window titled with the colonist's name and its overall health; hidden while a bar panel is open):- Header: name, sex, overall health, state, ground, walk speed and cell.
- Tabs: Needs, Health, Schedule, Skills and Inventory. Only Health is filled in so far.
- Health tab, left: a paper-doll diagram, drawn as if facing the colonist (its right side on the left). Each region is tinted pale green to yellow to red by the lowest health of its parts, missing fingers or toes tint the hand or foot, and lost regions are faint outlines. Female bodies are drawn narrower at the shoulders and wider at the hips.
- Health tab, right: every injured part with its injuries, and every lost part with the reason ("Cut off", "Destroyed") and what went with it.
- Health tab, with the dev panel open: buttons to add a random injury, cut off a random limb or head part, or heal everything.
- Above the bar, right: the cell under the cursor, with its terrain, water, item stack, plant and walkability.
- Bottom bar, numbered keys from the left, styled as the stop menu (each opens a window above the bar; click again, or the window's close key, to close it). Each window flies in when it opens:
- Colonists: every colonist with state, ground and walk speed. Click one to select it and move the camera to it.
- Options: camera pan speed, middle-drag rotate speed, path line and hover-cell toggles, the accent colour picker, the controls as key chips, "Open dev menu" (same as F1; closes Options) and Quit.
- Build: floors from the terrain CSV (
Floorcategory). Pick one and left-click the ground to place it on the top voxel; Esc stops. Walls, doors and furniture come with construction. - Tasks: the taskmaster's own panel (
SColonyTaskmasterPanel, 560 px): the typed-order box and the task list (see section 8).
- Test flag:
-ColonyOpenPanel=Tasks(orColonists,Options,Build) starts with that panel open.
Dev panel (F1; SColonyDevPanel, a Cypher window titled "Dev Panel", or "Character Designer" in designer mode) has three tabs: Tools, World rules and Pawn. Selected tools, tabs, framings, sexes and body types light up in the accent; sections have /// titles.
Tools tab:
- World: set a seed and regenerate. The cutaway checkbox and height are here too.
- Tools, applied with left click (drag to paint):
- Paint terrain: recolors the top voxel of each column in the brush
- Dig: removes it, never below the world floor
- Raise: adds a voxel of the chosen terrain on top
- Place plant: places the chosen plant
- Remove plants: clears plants in the brush
- Spawn colonist: adds a colonist at the cursor
- Teleport selected: moves the selected colonist to the clicked cell (the cutaway works too)
- Pickers for terrain and plant, plus a brush radius.
- Toggles for colonist path lines and the hover-cell box.
- Teleport by coordinates: X, Y and Z fields, a Teleport button, and "Use colonist's cell" to fill them in. The colonist lands on the standable cell nearest the target, and the ground there is meshed first.
World rules tab: a slider for the share of new colonists that are female. It saves to Data/WorldRules.csv when you release it, and applies on the next load.
The HUD stays in Slate: the shared style layer (ColonyUIStyle) and the 3D navigation (ColonyUISpace) draw everything themselves, so no UMG assets are needed. New screens should build from those widgets. To add a screen or control that matches: wrap it in SCyWindow (or SCyFrame with a Kind), use SCyButton (.Active(...) for the selected state, .Primary(true) for the main action), CySection for headings, ColonyUI::Body/Mono/Label with Ink()/Ink2()/Ink3() for text, and pass &ColonyUI::SpinBoxStyle() (and the other *Style() getters) to stock Slate controls. Use ColonyUI::Accent() for anything that marks the current or chosen item. For stepped navigation, add stops to an SCySpace with AddStation.
Test hooks (command line):
-ColonyAutoTestskips the menu (a dev test world), opens the dev panel, orders a move, logs how far the colonist walked, takes a screenshot with UI (shot showui), then quits.-ColonyCutaway=<Z>takes the screenshot in the cutaway view.-ColonySizeX=,-ColonySizeY=and-ColonySeed=override the dev test world.-ColonyCaveTestputs the colonist on a cave floor below the meshed depth (afterEnsureMeshed) and logs whether it stays standing.-ColonyCollapseTestbuilds a granite pillar with an arm 8 voxels from the spawn, digs out its base and checks that it falls.-ColonyFocusWaterpoints the camera at the surface water nearest the spawn instead of the colonist.-ColonyHealthTestspawns 60 colonists to log the sex ratio, checks the body-part tree logic (head and arm loss), saves and restores a world rule, teleports the colonist, then screenshots its health tab.
-ColonyDevTestskips the menu like Dev Test, for normal play.- Splash and credits (
ColonyCredits.h/.cpp):- Splash: once per launch, before the start menu: the LYRIDIA title, "Uses Unreal® Engine" and "Characters built with MetaHuman" as text, and Epic's trademark line, for 2 seconds; any key or click skips it. The same marks and line also show under the world generation / loading readout (
ColonyCredits::MakeStrip), so they cost no time. Automated runs (any-Colony...Test,-ColonyMenuStop=,-ColonyLoad=) skip it;-ColonySplashforces it and-ColonyNoSplashturns it off. - Credits (Main menu, the button above Quit;
-ColonyMenuCreditsopens it at once): the title, then every row ofData/Credits.csvby group (Engine, AI and voice, Characters, World, Animation, Development tools, Licenses, then mods), rolling slowly; the wheel scrolls, any key stops the roll, Esc or the close box goes back. - Data:
Content/Data/Credits.csv(Name,Group,Title,Author,License,Note);Docs/Credits.mdis the source of truth, with every pack's folder, license and use, the required notices, and the authors still to confirm. The Unreal Engine row's Note is the EULA's notice, word for word. - Logos: none ship. The splash shows an official logo only if its texture exists (
/Game/UI/Logos/T_Logo_UnrealEngine), which needs Epic's trademark license first (seeDocs/Credits.md). - Mods: a mod's
Data/Credits.csvis listed under "Mod: <folder>".
- Splash: once per launch, before the start menu: the LYRIDIA title, "Uses Unreal® Engine" and "Characters built with MetaHuman" as text, and Epic's trademark line, for 2 seconds; any key or click skips it. The same marks and line also show under the world generation / loading readout (
-ColonyMenuTestscreenshots the start menu's first stop, flies to the map size stop and screenshots it, then quits. Add-ColonyMenuStop=Nto begin on stop N, and-ColonyAccent=RRGGBBto shoot another accent.-ColonyLoadingShotscreenshots the loading screen a second into loading (use with-ColonyAutoTestand a large-ColonySizeX/-ColonySizeYso it's still up).-ColonyClickTest(use with-ColonyDevTest) logs the screen position of the speed buttons, Options and "Open dev menu", plus every change to game speed, the open panel and the dev panel. A script clicks them with the real mouse and checks the log. Synthesized Slate events don't reach widgets inside the game viewport, so they can't test this.- UI rule: widgets added with
AddViewportWidgetContentcover the whole screen. Give their root.Visibility(EVisibility::SelfHitTestInvisible). A root left at the defaultVisibletakes every click meant for the layers below it. That's why, until now, none of the HUD's buttons responded under the dev panel's layer.
Actions, saves and the multiplayer seams (game side)
The game side runs on the multiplayer-ready core (ColonyActions, ColonyNet, ColonyEvents, ColonyWorldEdits, ColonySave). Single player works as before; these are the seams a multiplayer mod and the save system use.
Game state.
- The game mode's state class is
AColonyGameState. StartGamesets its world settings: size, the game seed (not the district's, see section 11), and aging speed.- The day is mirrored into it about once a second, and the speed on every change, so a client's HUD can read both.
- Only the machine with sim authority advances the calendar (
ColonyNet::HasSimAuthority). - Events (
UColonyEvents):OnGameStartedfires when loading is done,OnDayChangedat each new calendar day.
Actions. What the player controller changes in the simulation is an action. The public calls submit one, and handlers registered in RegisterActionHandlers (at BeginPlay, with sim authority) do the work:
| Action | Arguments | From |
|---|---|---|
Time.SetSpeed | Speed 0–3 | Space, 1/2/3, the HUD's speed buttons (SetGameSpeed) |
Colonist.Possess / Colonist.Release | Colonist id | Tab (PossessColonist / ReleaseColonist) |
Colonist.Move | Colonist id, Target (world position) | Right click |
Dev.Teleport | Colonist id, Cell | Teleport tool, dev panel (TeleportSelected) |
Dev.SpawnColonist | Cell | Spawn tool (selects the new colonist for the host player) |
World.PlacePlant | Cell, Plant type id | Place plant tool |
World.Dig, World.PaintTerrain, World.Raise, World.RemovePlants | Cell, Radius (0–16), Terrain id, CutawayZ | Brush tools (ApplyBrush) |
- A rejected action shows its reason on screen ("No path", "No room to stand there", "Select a colonist first").
- A mod can replace any of them with
RegisterHandleron the same type. - Test from the console, e.g.
colony.Net.Submit Time.SetSpeed Speed=3. - Still direct for now:
- mining designations (the taskmaster's
Tasks.*, Systems Dev's); - the designer's appearance edits;
- the dev panel's health and age buttons.
- mining designations (the taskmaster's
World edits.
AVoxelWorld::SetTerrainand collapse moves (CollapseUnsupported) recordSetTerrainedits.- The plant layer's
PlacePlantandRemovePlantAtrecordPlacePlant/RemovePlantedits, keyed by the plant's column. - Recording is on only after loading is done (
AVoxelWorld::bRecordEdits), so generation, which comes from the seed, is never recorded. - The game mode registers the appliers: terrain edits mark their chunks dirty and rebuild together the next frame.
- Rock mining (
URockLayerComponent::RemoveRockAt) records aRemoveRockedit at the mined voxel as well as its core voxels' terrain edits, so a replay removes the rock's mesh too. - Not covered yet: a rock that falls in a collapse moves its instance, and a replay only moves the voxels.
Colonists.
- Every colonist has a
UColonyEntityComponent(GetEntity()): a stable colony id. - The id is given at spawn and seeds the colonist's rolls with
FMath::RandInit(hash(world seed, id)). So the same game seed gives the same colonists (sex, look, age, history) on any machine and in replays. - The
Colonistssave section (version 3) stores, for each colonist:- id, location and yaw;
- identity: first, last and nickname, and xenotype (version 3; versions 1 and 2 had one name, which loads as the nickname of a Human with no first or last name);
- sex and body color;
- the whole appearance (choices, body type, features, shapes, ancestry, tone, scales);
- age;
- health (
UColonistHealthComponent::SerializeState: every part's injuries and losses).
- The look is stored whole, not re-rolled, so it survives data file changes.
- Loading replaces the fresh world's colonists with the saved ones, under their saved ids.
Loading.
AColonyGameMode::StartGameFromSnapshot(Snapshot)generates the snapshot's world (size, seed, aging) with no starting colonist.- When loading is done it applies the snapshot: world edits, every registered section, then the day and speed.
-ColonyLoad=<slot>loadsSaved/SaveGames/<slot>.savat startup;colony.Net.Save <slot>saves.-ColonySaveTestis the round trip:- with
-ColonyDevTestit digs throughWorld.Dig, logs the clock, edit count and every colonist, saves toSaveTestand quits; - with
-ColonyLoad=SaveTestit logs the same lines after loading, for comparison.
- with
8. Jobs, tasks, needs, whims and typed orders (the taskmaster)
Colonists decide what to do on their own, and the player steers them in two ways: by arranging tasks and by typing orders. The taskmaster is the colony's job brain. It owns needs, mood, whims, tasks, the job board and who does what. It's plain C++ with no engine objects, and it thinks on a worker thread, so the game thread only renders and moves colonists.
| Piece | File | Thread |
|---|---|---|
| Data (jobs, needs, whims, work types, work lists, command words) | ColonyTaskDefs.h/.cpp | loaded once, read-only |
| The brain: needs, mood, whims, tasks, job board, assignment | ColonyTaskmaster.h/.cpp (FColonyTaskmaster) | worker |
| The driver: walks colonists to jobs, times the work, applies effects; runs typed orders | ColonyTaskmasterSubsystem.h/.cpp (UColonyTaskmasterSubsystem) | game |
| Typed order parser | ColonyCommands.h/.cpp (FColonyCommandParser) | game |
| Taskmaster panel (order box and task list) | ColonyTaskmasterPanel.h/.cpp (SColonyTaskmasterPanel) | game (Slate) |
How the threads talk. Every 0.2 game seconds, the subsystem collects the previous think's result (orders, a status snapshot for the HUD, and the task list for the panel), then starts the next think with the commands queued since then. While a think runs, the game thread never touches the brain. Commands and results are the only way in and out. colony.Taskmaster.Threaded 0 runs the think inline instead, for debugging. colony.Taskmaster.PathsPerFrame (default 2) is how many colonists may start a path search per frame. Searches run on the game thread, so the rest wait a frame.
Tasks
A task is a group of board jobs: one Mine tool stroke (designations within 2 seconds of each other), or one typed order. Tasks have a priority order that the player rearranges in the taskmaster panel:
- Up / Down moves a task in the priority order.
- Pause stops anyone from working on it. Anyone on it drops it.
- For: cycles who may do it, from anyone to each colonist in turn.
- Force makes it a direct order for whoever it's assigned to.
- Cancel removes the task and all its jobs.
New tasks for anyone land among the tasks of their work type (in WorkTypes.csv order), and direct orders go to the top.
What a colonist does next
When it has nothing to do, a colonist takes the first of these that applies:
- A need below
OrdersYieldBelow(nearly empty). This is the only thing that interrupts a direct order. - Its direct orders (forced tasks), in priority order. A new order interrupts whatever the colonist was doing.
- An urgent need (below
UrgentBelow). This also interrupts ordinary work. The job keeps its progress for whoever picks it up next. - A need below
SeekBelow. - A whim, sometimes. The chance is
WhimChance+WhimChanceBored× how empty Recreation is. - Work. It goes down the colony's task list, taking the first task it may do (not paused, for anyone or for it) whose work type is on its work list. Within the task it takes the top layer first, then the nearest job (climbing counts extra). It skips jobs it failed to reach in the last 30 seconds.
- Otherwise it idles or wanders, and now and then follows a whim after all.
Possession kills the AI. A possessed colonist's job goes back on the board, its needs keep draining, and it gets nothing until released. A right-click move does the same until the colonist arrives (OrderMove).
Mood = BaseMood + each need scaled from −MoodWeight (empty) to +MoodWeight (full) + whim moodlets (fading out over MoodSeconds), clamped to 0..100. Whims can be limited to a mood range. For example, only unhappy colonists brood.
Typed orders
Type orders in the taskmaster panel's box, or in the console as colony.Cmd <order>. Examples:
Ada: mine an area 15 squares deep (5x5 by default; "here" = where the mouse was, else Ada's spot)
Bram and Cass, dig 7 by 5 here 3 down then go to 120 96
everyone stop (back to their own judgement)
also, Ada sleep ("also"/"after that" up front queues instead of replacing)
dig out a 9x9 pit fifteen deep at 100 100 (for the selected colonist)
- Who: names (or
everyone) before the verb, joined byandand commas. With no names, the order is for the selected colonist. An unknown name before a:is refused. - Verbs come from
CommandWords.csv(phrases and synonyms such asdig,excavate,go to,take a break) plus every personal job'sId.Minedigs an area,Gotowalks to a spot, andStopdrops the orders. Any personal job (Sleep,Stargaze...) is done once. Before another verb,gois just a lead-in ("go mine"). - Numbers can be digits or words up to fifty.
- Sizes:
5x5,5 by 7,5 wide. - Depth:
15 deep,15 squares deep,3 down,depth 15. - Places:
here,at 120 96,to 120 96.
- Sizes:
- Chains: steps split at
then,after that, and atandor a comma before a new verb. - A new order replaces the colonists' previous orders. Mining jobs left over from it stay on the board for anyone. Each step becomes a forced task, so it shows in the panel, where it can be reordered, paused, reassigned or cancelled.
- Mining an area levels it from the surface at its middle. Deeper than 1, the outer 3-block ring becomes a ramp going down one block per step around the pit. Colonists are 3 wide and step up 1, so whoever digs can always walk out. That needs at least 5x5, and an edge long enough for the depth. Too small and the order is refused with the reason. One order can dig up to 8000 voxels, 64 across and 64 deep.
FColonyCommand::ToJson()shows the parsed shape. The language model below fills in the same structure, so both parsers feed the same execution.
The language model (optional)
When the rules can't read an order, it goes to a small local language model instead. The panel log then shows how the order was read ("read as: Ada: mine 4x4, 3 deep here") before the colonists carry it out.
- Runtime:
ThirdParty/LLM/bin/llama-server.exe, llama.cpp build b11687, the Windows x64 CPU build (MIT license).FColonyLanguageModel(ColonyLanguageModel.h/.cpp) starts it on127.0.0.1:<colony.LLM.Port>(8771 by default) when the game starts, and stops it with the game. A server that's already running on that port is reused. - Model:
ThirdParty/LLM/models/<colony.LLM.Model>. The default isqwen2.5-1.5b-instruct-q4_k_m.gguffromhuggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF(1.12 GB, Apache 2.0; SHA-2566a1a2eb6…e434e9407e). Models are git-ignored, so fetch the file yourself. Without it, or withcolony.LLM.Enabled 0, everything works on the rules alone. - Constrained output: each request carries a JSON schema (
FColonyCommandParser::ModelSchema) that limits verbs and colonist names to the real ones. The server turns the schema into a grammar, so the reply is always that shape. The temperature is 0, so the same order is read the same way every time. - Guardrails in
FromModelJson. The model decides what the order is; the game checks the details:- Names must be real colonists.
- A position counts only if both of its numbers were typed. A walk to a made-up position is dropped.
- Only digging and walking take a place.
- Repeated steps are dropped.
- Sizes and depth come from the rules' reading of the exact words whenever the rules can read them.
- Prompt:
ModelPromptuses Qwen chat markup. It covers the verbs (with the phrases fromCommandWords.csv), the colonist names, the rules and a few worked examples. Adding a verb or synonym to the data files updates the prompt too. - Speed: on a 14-core CPU, replies take about 1–3 s, occasionally up to 6 s. The first order after start waits for the model to load. Orders are handled one at a time.
- Test hook:
-ColonyDevTest -ColonyLLMTestsends a set of awkward phrasings, plus a couple of non-orders that must be refused. It logs how each was read and how long it took, then quits. - Shipping:
- The model ships with the game.
ColonySim.Build.csstagesllama-server.exeand only the libraries it loads as loose files next to the game:llama-server-impl,llama,llama-common,mtmd,ggml,ggml-base, theggml-cpu-*backends (one is picked for the CPU at run time) andlibomp. The bench, quantize, CLI and TTS tools are left out. - It also stages the model and
ThirdParty/LLM/licenses/: llama.cpp's MIT license (covers ggml), the LLVM OpenMP license, Qwen's Apache 2.0 license, andTHIRD_PARTY_NOTICES.txtwith sources, versions and hashes. - The model is git-ignored, so it must be present on the packaging machine. Without it the build warns and the package falls back to the rule parser.
- Other platforms need their own llama.cpp builds. Consoles will need llama.cpp linked into the game rather than run as a separate server.
- The model ships with the game.
Character voices (text-to-speech, in progress)
Colonists will speak with Kokoro-82M (Apache 2.0). It was chosen over Piper, whose voices derive from research-only data, and OuteTTS, which doesn't run on the bundled llama.cpp and is partly non-commercial. Files, sources and hashes are in ThirdParty/TTS/README.md.
- Voices: 54 presets that can be blended (
bf_isabella:0.6;af_sarah:0.4). Per colonist the plan is a voice id plus a pitch offset (±10%, through a MetaSound pitch shift that keeps timing). Kokoro has no child voices. - No GPL in the game: Kokoro's usual Python front end turns text into phonemes with espeak-ng (GPL-3.0).
- Curated lines are baked to WAV offline with
Scripts/tts_bake.py, so the game ships only audio. - Runtime lines will use the misaki lexicon in
ThirdParty/TTS/lexicon/(Apache 2.0). - Words the lexicon doesn't know (colonist names, invented words) go in
lexicon/project.jsonas"word": "phonemes". The bake script lists missing words inunknown_words.txt.
- Curated lines are baked to WAV offline with
- Packaged: the int8 model (92 MB), the voice pack (28 MB), the lexicons,
NOTICE.txtand the Apache 2.0 text.ThirdParty/TTS/models/is git-ignored like the language model. - Per colonist (
FColonistAppearance):VoiceId(aData/Voices.csvrow),VoicePitch(semitones, up to +-1.7, about +-10%) andVoiceSpeed(0.9 to 1.1). They're rolled last inRoll, by sex, from the Adult rows byWeight, with pitch and speed near the middle. They're saved (Colonists section v2; older saves get a voice on load) and exported by the designer. Voices.csvName,Recipe,Sex,Stage,Weight,Description. Recipe is a Kokoro preset or a blend; Sex is Female, Male or Any; Stage is Adult or Child. It ships 8 female and 8 male voices from the 12 English presets checked in the evaluation, with blends, plusChild_Placeholder.- Who speaks how:
FAppearanceCatalog::VoiceFor(Appearance, BiologicalYears, Lifespan, OutPitch)gives the voice row and the pitch to play at. Below teen age (13 at an 80-year lifespan) it's the first Child row at the highest pitch, until a real child voice exists. - Not built yet:
- runtime synthesis (a sidecar next to llama-server, or NNE);
- Build.cs staging for
ThirdParty/TTS.
Data files
All in Content/Data/ and overridable from Mods/<Mod>/Data/. A row with the same Id replaces the base row, and new Ids are added. Columns are found by header name, so their order doesn't matter and optional columns can be left out.
WorkTypes.csvId,Label. File order is where new tasks land by default. OnlyMininghas jobs today.WorkLists.csvId,WorkTypes(;-separated). These are the kinds of work a colonist does; types left out are never done. The colony's task order decides what comes first.Defaultis what new colonists get.Jobs.csvId,Label,WorkType,WorkAmount,Effect,Need,Radius.WorkAmountis in game seconds at normal speed.WorkTypeis empty for personal jobs (needs, whims, idling, orders).Effectis one of:Dig: removes the target voxel, or the whole rock it belongs to, then lets unsupported terrain fall.RestoreNeed: refillsNeedwhile working, and ends when it's full.Wander: walks to a random spot withinRadiusblocks.Visit: walks to a colonist withinRadiusblocks.Goto: walks to the order's spot.None: just spends the time on the spot.Idle,Wander,MineandGotomust exist.
Needs.csvId,Label,DecayPerDay,SeekBelow,UrgentBelow,Job,RestorePerSecond,MoodWeight. Values are 0..1 and drain per calendar day. A need namedRecreationdrives the whim chance.Whims.csvId,Label,Job,Weight,MinMood,MaxMood,MoodBonus,MoodSeconds,Need,NeedAmount.CommandWords.csvPhrase,Verb. Typed-order words: a mod adds synonyms, or other languages' words, here.Taskmaster.csvKey,Value:WhimChance,WhimChanceBored,BaseMood,OrdersYieldBelow.
Code hooks
UColonyTaskmasterSubsystem::Get(World), then:DesignateMine/CancelDesignationExecuteCommand(Text, Selected, Cursor),GetCommandLogGetTasks,MoveTask,SetTaskPaused,CancelTask,AssignTaskOrderMove,ClearOrdersSetWorkList/NextWorkListGetStatus/DescribeActivity/DescribeColonistPlanDig(the ramp-safe pit plan)
- The panel:
SNew(SColonyTaskmasterPanel).Controller(PC)placed anywhere in the HUD. It's self-contained. - New job effect: add it to
EColonyJobEffectandParseEffect. Then give the driver its walking target (StartWalking), its reach test (InReach), and what happens when it's done (Finish). Effects that touch the world always run on the game thread. The brain only decides. - New typed verb with an area or target (e.g. Build): add its words to
CommandWords.csv, then add a branch inExecuteStepthat turns the step into a task and its jobs. - Job highlights (
colony.Taskmaster.Draw, on):- Areas: each designated area is one faint, mostly transparent tint on the ground (
M_ColonyBrushDecalin patch mode: about 12% with a slightly stronger soft edge, no line). It's in the UI accent colour, or the warm colour for direct orders. - Merging: a task's touching columns (8 ways) form one area, whose decal reaches from a block under its lowest cell to a block over its highest, so pit walls and dig faces are tinted too.
- Work in progress: each cell a colonist is working on shows a small, slightly brighter, pulsing round patch.
- Cost: at most 256 decals, refreshed every 0.25 s.
- The engine view:
colony.Taskmaster.DrawDebug 1draws a wireframe box per designated voxel (orange open, red direct order, green someone on it).
- Areas: each designated area is one faint, mostly transparent tint on the ground (
colony.Taskmaster.Enabled 0stops all colonist AI.- Names in commands can be several words ("Loose Cannon: stop"); the longest matching name wins.
- Test hooks:
-ColonyDevTest -ColonyJobTest: designations, whims and possession over 90 seconds.-ColonyDevTest -ColonyCommandTest(runs at 3x speed):- Logs how the parser reads a set of phrases.
- Gives a refused order, then two colonists a 7x7, 4-deep pit and a third a go-then-stargaze chain.
- Overrides one order with a new one and moves a task to the top.
- Finally checks that a colonist can walk out of the pit.
Not yet:
- A work tab for per-colonist list editing.
- Hauling and items, beds and food, schedules.
- Skills and work speed: everyone works at 1 unit per second.
- Named zones and drag-selected areas for orders.
- Pathfinding on the worker. The voxel world isn't safe to read off the game thread while it's being dug.
9. Social life (the social bubble)
Beside the taskmaster runs the colony's social system. It tracks who is in whose bubble, the interactions between them, the events they notice, what they remember, and what they think of each other. It's a separate system with a narrow link to the taskmaster: the taskmaster decides who does what, and the social system decides how they feel about each other.
| Piece | File | Thread |
|---|---|---|
| Data (event types, interaction types, bubble rules) and the brain | ColonySocial.h/.cpp (FColonySocialDefs, FColonySocial) | worker |
| The bridge: snapshot in, mood and friends out, the log | ColonySocialSubsystem.h/.cpp (UColonySocialSubsystem) | game |
How it thinks. It uses the same pattern as the taskmaster. Every ThinkSeconds (1 game second), the subsystem hands the brain a snapshot (each colonist's cell, mood, and whether it's awake) plus the events reported since, and collects the previous think's result. While a think runs, the game thread never touches the brain. colony.Social.Threaded 0 runs it inline instead, for debugging.
The bubble. A colonist's bubble covers BubbleRadius blocks sideways and BubbleHeight blocks up or down. Walls don't block it yet. Sleeping colonists neither talk nor notice anything.
Interactions. Every pair of awake colonists in each other's bubble rolls, about ChatsPerMinute times a minute, to interact. The same pair then waits PairCooldownSeconds before the next one. The colonist who starts it picks an interaction from those its opinion of the other, and its own mood, allow (weighted). For example, a deep talk needs an opinion over 20, an insult needs one under −10, and anyone in a bad mood may snap. Each interaction changes both colonists' opinions and moods and leaves a memory for both.
Events. Systems report events through UColonyTaskmasterSubsystem::OnColonyEvent or UColonySocialSubsystem::ReportEvent(Id, Who, Cell). Whoever caused the event gets ActorMood. Everyone awake within Radius of it witnesses it: they get WitnessMood, their opinion of whoever caused it changes by WitnessOpinion, and it leaves a memory. The taskmaster reports these today:
TaskDone: the colonist who finished the last job of a task.Collapse: digging brought terrain down.NewColonist: someone arrived.
Opinions and memories. Opinions run −100..100 for each direction separately: what A thinks of B isn't what B thinks of A. They drift back toward 0 by OpinionDecayPerDay, so people get over things in time. Memories last MemoryDays (interactions last 1 day), keeping at most MaxMemories per colonist. Memories are what reactions and dialogue will draw on later.
Back to the taskmaster (data only):
- Mood changes become moodlets that fade out.
- Opinions above
FriendAbovemake a colonist a friend and belowAvoidBelowmake it one to avoid. Chat whims pick friends three times as often and never pick someone the colonist avoids.
Data files
All in Content/Data/ and overridable from Mods/<Mod>/Data/. A row with the same Id replaces the base row, and new Ids are added.
SocialEvents.csvId,Label,PastLabel,Radius,ActorMood,ActorMoodSeconds,WitnessMood,WitnessMoodSeconds,WitnessOpinion,MemoryDays.PastLabelis how a memory of the event reads ("finished a task"); it defaults toLabel.Labelreads after a name: "Bram brought the ground down". A mod adds an event with a row here plus aReportEventcall where it happens.SocialInteractions.csvId,Label,Weight,MinOpinion,MaxOpinion,MinMood,MaxMood,OpinionOfRecipient,OpinionOfInitiator,InitiatorMood,RecipientMood,MoodSeconds.Labelreads between two names: "Ada made small talk with Bram". The opinion and mood ranges apply to the colonist starting it. A new kind of interaction is just a row.Social.csvKey,Value:BubbleRadius,BubbleHeight,ChatsPerMinute,PairCooldownSeconds,OpinionDecayPerDay,FriendAbove,AvoidBelow,MaxMemories,ThinkSeconds.
Code hooks
UColonySocialSubsystem::Get(World), then:ReportEventGetOpinion(Who, Other)DescribeRelations(Colonist)("Bram: friend (+34) · Cass: −12")DescribeMemories(Colonist)GetLog()(recent interactions and noticed events)
colony.Social.Log 1logs every interaction and noticed event.- Test hook:
-ColonyDevTest -ColonySocialTestputs four colonists close together for ten game minutes at 3x, with a cave-in at one minute. It logs relations and memories every two minutes, then a summary, and quits.
Not yet:
- Walls and line of sight for the bubble.
- Relationships beyond opinion (family, partners, rivals).
- Traits that bias interactions.
- Opinions and memories in saves.
- Spoken lines for interactions and reactions. They'll come from these interactions and memories, with the language model wording them.
10. Flora and fauna species (new every playthrough)
Plant and animal functions are fixed. A function is what something does: a Plants.csv row (its footprint, blocking, walk speed, habitat, density and, later, its yields) or a Fauna.csv row (diet, size, temperament, tamability). What they look like and are called is generated from the world seed, so every playthrough has new species, and players have to find out what each one is.
| Piece | File |
|---|---|
| Species generation (names, looks), rules, fauna functions | ColonySpecies.h/.cpp (FColonySpeciesCatalog) |
| Dressing the plants, identification, Study jobs | ColonySpeciesSubsystem.h/.cpp (UColonySpeciesSubsystem) |
What changes per playthrough (for plant categories with Vary<Category> = 1: trees, wild plants and decoratives by default; crops and stumps stay as written):
- Shape, within its silhouette family, so the function still reads right:
- Trees stay trees (round, spire or frond).
- Blocking plants stay columns or big fungi.
- Undergrowth that slows walkers stays something to get tangled in (shrub, grass, creeper).
- Other small plants can be any small shape.
- Foliage and stem colors (any hue).
- Size,
SizeMin..SizeMaxtimes the function's size. - Name: a genus and an epithet, e.g. "Korrath glimmerleaf".
Footprint, blocking, walk speed, habitat and density never change. The same seed with the same data files always gives the same species.
Identification.
- An unknown species shows as "Unknown violet spiny shrub", a description generated from its color and shape. The hover text, plant pickers and anything else that reads the plant's
DisplayNameshow that. - The species system keeps up to
MaxStudyJobsStudy jobs posted, one per unknown species, at the nearest plant of it withinStudyRangeblocks of a colonist. They belong to one task, "Identify plant species", under the Research work type, so the player can move, pause or cancel it in the Tasks panel like any other. - Each study counts toward the species ("studied 1/3").
- After
StudiesToIdentifystudies, the species shows as "Korrath glimmerleaf (Oak Tree)": its real name and what it is. - It's logged ("Ada identified the violet spiny shrub: Korrath glimmerleaf, a kind of oak tree"), and an
Identifiedsocial event cheers the identifier and the colonists who saw it.
Fauna. Every Fauna.csv function also gets a species each playthrough: a body plan fitting its size, a covering, a feature, an eye count, colors, a name ("Duskstrider") and an unknown description ("amber scaled hexapod"). Animals of every function roam the world (see "Animals in the world" below).
Data files
Species.csvKey,Value:VaryTree,VaryWild,VaryDecorative,VaryCrop,VaryStump: 1 = that category gets new looks every playthrough.SizeMin,SizeMaxStudiesToIdentify,StudyRange,MaxStudyJobs
SpeciesWords.csvKind,Word,Value. These are the words names and descriptions are built from:GenusStart+GenusEndmakes the genus ("Kor" + "rath").FloraLeft+FloraRightmakes a plant epithet ("glimmer" + "leaf").FaunaLeft+FaunaRightmakes an animal name ("dusk" + "strider").Featurewords tell apart two unknown species that would be described the same way.Colorwords come with a hue in degrees (Value); an unknown species gets the nearest one, plus "pale" or "dark". All copies add up, so a mod adds words with its own file. Translators can give a language its own word set.
Fauna.csvId,Label,Diet,Size,Temperament,Tamable,Habitat.Habitatis terrain names separated by;.Size(Small,Medium,Large,Huge) picks the body plans a species can have.- The
Studyjob is inJobs.csv(work amount 15 game seconds).
Code hooks
UColonySpeciesSubsystem::Get(World), then:GetCatalog()(FindFlora(PlantType),FindFauna(Function),GetAll())IsIdentified,GetKnowledge(0..1)GetShownName(Species)GetLog()(studies and identifications)
- Animals take their looks from
FindFauna(Function)(AColonyAnimal::Setup).UColonyFaunaSubsystem::Get(World)givesNumAnimals(). colony.Species.Listlogs this playthrough's species.colony.Species.IdentifyAllreveals them all (dev).- Test hook:
-ColonyDevTest -ColonySpeciesTestlogs the catalog and lets three colonists study at 3x for 450 game seconds. It logs each study and identification, then quits.
Plant and creature models (the kits)
Plants and animals are assembled from small, generated model kits, so every species looks different without one model per species.
| Piece | File |
|---|---|
| Plant kit models (Blender, procedural) | Scripts/blender_plant_kit.py → Art/Plants/Kit/*.fbx |
| Creature kit models (Blender, procedural) | Scripts/blender_creature_kit.py → Art/Fauna/Kit/*.fbx |
| Facial hair kit (Blender, procedural: hair, brows, beards, lashes) | Scripts/blender_facial_hair_kit.py → Art/FacialHair/*.fbx |
| Part tables | Scripts/gen_kit_data.py → Content/Data/PlantParts.csv, Content/Data/CreatureParts.csv |
| Import and the kit material | Scripts/import_kits.py (Unreal Python) → /Game/Art/Plants/Kit, /Game/Art/Fauna/Kit, /Game/Colony/Kit/M_ColonyKit |
| Part lookup | ColonyKits.h/.cpp (FColonyKits) |
| Plant assembly | ColonyPlantKit.h/.cpp |
Rebuilding the kits.
blender -b --factory-startup --python Scripts/blender_plant_kit.py -- Art/Plants/Kit --preview
blender -b --factory-startup --python Scripts/blender_creature_kit.py -- Art/Fauna/Kit --preview
python Scripts/gen_kit_data.py
UnrealEditor-Cmd ColonySim.uproject -run=pythonscript -script=Scripts/import_kits.py
The scripts are seeded, so a rerun gives the same parts. New parts are appended at the end of each script's list so existing parts don't change.
Part conventions.
- Parts are 1 unit (100 cm) across. The game scales them to each species' size. Plant parts stand on the origin. Legs hang from the hip at the origin to the foot at z = -1.
- Each part has one vertex color,
Mask: R = body (foliage, hide) vs pattern (stem, markings), G = glow, B = a little shade variation.M_ColonyKitturns it into color with the parametersBodyColor,PatternColor,GlowColor,GlowStrengthandRoughness, so one material serves every species.
PlantParts.csv Name,Slot,Family,Mesh,Shapes,Notes.
SlotisStalk,Crown,Accent,Base(trees),Column(blocking plants),Tangle(slowing undergrowth) orSmall(everything else).Shapeslists the species shapes the part suits, e.g."(RoundTree,PalmTree)". If no part of a slot suits a shape, any part of that slot is used.- A tree is a stalk (55% of its height, thicker for taller trees) with a crown over its top 60%. It sometimes gets an accent (blossoms, fruit, moss) and a base (ferns, fungus). Crowns, columns and small plants are stretched by their real bounds to fill the species' size, so a part needn't fill its 1 m box. Other plants are one part at full size. The picks come from the species' name and colors, so a saved species looks the same after a load. Stumps keep the basic shapes.
CreatureParts.csv Name,Slot,Plan,Mesh,Sockets,Notes.
SlotisTorso,Leg,Head,Tail,FeatureorEye.Planis the body plan a torso belongs to (quadruped,hexapod,biped,serpent,crawler,crustacean). For a feature it's the species feature it shows (horns,frill,tusks,spines,crest,antennae,claws,plates).Socketsare attachment points in part units:Name=x;y;z|Name=x;y;z. A torso has oneLegsocket per leg, plusHead,TailandBack. A head hasEyes,TopandFront.- A mod adds parts with its own CSV rows and meshes. A matching
Namereplaces a part.
Animals in the world
| Piece | File |
|---|---|
| The animal actor (assembly, wandering, fleeing, animation) | ColonyAnimal.h/.cpp (AColonyAnimal) |
| Spawning, saving | ColonyFaunaSubsystem.h/.cpp (UColonyFaunaSubsystem) |
- Spawning. On a new game, once the world and its species exist, every
Fauna.csvfunction gets a few herds on ground itsHabitatlists, at least 20 m from the colony. Herd size goes bySize: Small 4, Medium 3, Large 2, Huge 1. Host only. - Looks. Each animal is assembled from the creature kit using its species: a torso for the body plan, legs (none for serpents and crawlers), a head, a tail, eyes by eye count, and the species feature. It's colored with the species' colors. Body length goes by
Size(45, 95, 170 or 300 cm), with a little variation per animal. - Registered like characters. Every animal has a
UColonyEntityComponentcolony id from the shared registry (never reused), so actions, the HUD and saves can name it (AColonyAnimal::GetColonyId). Its kind is itsFauna.csvrow (GetKind, like a colonist's xenotype); this playthrough's look and name come fromFindFauna(GetKind()). A load restores each animal's id. - Behavior. Animals idle and wander on the voxel pathfinder.
Skittishones flee from colonists within 9 m,Waryones within 6 m, andDocileones don't flee. Legs, body, head and tail move procedurally; there are no skeletal meshes. Off screen they skip that animation, and idle ones off screen tick 4 times a second (colony.Animals.CullOffscreen 0turns that off). - Perf probe:
-ColonyDevTest -ColonyPortraitsAnimated -ColonyPerfProbemeasures the toggled optimizations off and on in one run (portrait frame/GPU time, the animals' Tick time, a remove-plants brush) and logsPerfProbe:lines. - Not yet: hunting, taming, eating, breeding and dying.
- Test hook:
-ColonyDevTest -ColonyFloraShottakes close screenshots of the tallest tree near the middle, a wild plant and three animals, then quits.
Saving flora and fauna
Three save sections keep the living world as it was (section 12, "Save sections"):
Plantssaves every plant by its type's name with its cell, size and facing. A load swaps the seed's plants for the saved ones, so new Plants.csv rows, a changed density or new kit parts can't change an existing save's flora. Plants of a type that no longer exists (a removed mod) are skipped and logged.Speciessaves the generated catalog and the research (studies, identified species, the log). A load keeps the saved species even when the data files have changed since, which is logged as "inputs changed".Animalssaves each animal's function, colony id, position, facing and variant.
Saves from before these sections still load: their plants come from the seed plus the replayed plant edits, their species are generated again from the seed, and they get a fresh animal population.
When a save has the Plants section, a load doesn't replay the plant world edits (PlacePlant, RemovePlant): the section restores the layer itself, and replaying them first meant rebuilding every plant type over again. They're kept in the edit history, so later saves and joins still have them. Saves without the section replay them as before.
Test hook: -ColonyDevTest -ColonyFloraSaveTest removes plants by every path there is: Cut jobs on two trees and a wild plant (designated as the Cut tool and colony.Cut do), flowing water, flowing lava, digging the ground out from under a plant (World.Dig) and the remove-plants tool (World.RemovePlants). It waits for them (up to 120 s), saves to the FloraTest slot and quits. Then -ColonyDevTest -ColonyFloraSaveTest -ColonyLoad=FloraTest checks that every one of those plants is still gone (PASS/FAIL). Both runs log each target, the plant count and the animals, so the two logs compare.
Not yet:
- Studying animals.
- Unknown species having gameplay limits, such as not eating or harvesting what isn't identified, once harvesting exists.
11. The planet, biomes and districts
The game runs on two levels: the planet and the district map. Each geodesic tile of the planet is a district: a fixed-size voxel map, generated from that tile's properties when the colony goes there. The planet view and the district view switch with M (and M or Esc to come back). The district keeps simulating while the player looks at the planet.
| Piece | File |
|---|---|
| Geodesic grid, climate, biomes, district registry (plain C++) | ColonyPlanet.h/.cpp (FColonyPlanet) |
| Planet state for the game, edge rule, view switching | ColonyPlanetSubsystem.h/.cpp (UColonyPlanetSubsystem) |
| The globe and its camera, district picking, the selection circle | ColonyPlanetGlobe.h/.cpp (AColonyPlanetGlobe) |
| White glow selection (outline pass, glow material) | ColonyGlow.h/.cpp |
The coordinate system.
- The grid: an icosahedron's 20 faces are subdivided
Frequencytimes along great circles and projected onto the sphere. The tiles are the dual cells: exactly 12 pentagons, the rest hexagons, and 10 ×Frequency² + 2 tiles in all (24 gives 5,762). - Even cells: subdividing along great circles keeps hexagons within about 1.2× of each other's area (1.6× counting the pentagons) without any relaxation. An offline comparison showed relaxation makes it worse.
- What each tile has: a stable id (its index), its neighbors (in the same order as its corners), its center on the unit sphere, latitude and longitude.
- Display: the grid is never drawn, and the globe isn't built from it. The globe is one seamless surface (a cube-sphere mesh,
FaceCells288 per face, built in parallel in about 150 ms). Each vertex samples the hidden grid: its direction is warped by noise, then the district under it and its neighbors are blended by a smooth falloff of distance (a district's center shows only its own values). Land color blends the land districts, water color the water districts (lighter in the shallows), and fog dims unexplored ones. The coast is where the blended elevation, plus fine noise, crosses sea level. Coasts, biome borders and fog edges are organic curves, never tile edges. Rivers are ribbons on top. The grid only answers "which district is here" (picking, ids, data).
Generation (FColonyPlanet::Generate, deterministic for the seed and the data files; about 20 ms for 5,762 tiles):
- Elevation: continents, detail and mountain ridges from layered 3D noise on the sphere. Sea level is set so that
LandFractionof the tiles are land, and land rises toMaxElevation. - Temperature: from latitude (between
EquatorTempandPolarTemp, mild in the mid-latitudes), minusLapseRateper km of height, plusTemperatureOffsetand a little noise. - Rainfall: wet equator and mid-latitudes, dry subtropics and poles. It's patchy, drier far from the sea, and times
RainfallScale. - Hilliness and swampiness: hilliness (
Flat,SmallHills,LargeHills,Mountains) comes from how steep a tile is against its neighbors plus the ridges, scaled byMountains. Swampiness is high on wet, flat lowland. - Biome (rules): each
Biomes.csvrow lists conditions: temperature, rainfall, elevation, swampiness and hilliness ranges. Among the biomes a tile qualifies for, the highestPrioritywins, with ties broken by a stable roll.Patchbiomes (lumen grove, archotech scar, basalt reach) appear only in clustered regions covering that share of the planet. A tile no biome fits gets the nearest one by climate. Water tiles only getWaterbiomes. - Geography:
- Lakes: connected water bodies of at most
LakeMaxTilesdistricts. Bigger ones are the sea. - Coast and lakeshore: land next to the sea, or next to a lake.
- Rivers: each land district collects its rain and passes it, with all that flows in, to its lowest neighbor, highest districts first. Rivers end in water, or in a basin with no way down. Enough flow makes a river:
RiverFlowgives a creek, 2.5x a river, 6x a large river, 16x a huge river.Riveris the size andRiverTothe next district downstream.
- Lakes: connected water bodies of at most
- Land features (
LandFeatures.csv, after RimWorld's landmarks and tile mutators):- Landmarks come first. For each land district, the qualifying landmark with the best roll against its
Chance(timesLandmarkDensity) wins. There's at most one per district and never two side by side. - Names: a landmark gets a generated name ("Veyrath basin").
- Secondary features:
Id:oddspairs a landmark may bring along (a cavern often brings cave pools). - Ordinary features roll on every land district they qualify for. Geography features (Coast, River, Headwater, Confluence, Delta, Lakeshore) have chance 1 and only label what is there.
- Landmarks come first. For each land district, the qualifying landmark with the best roll against its
Districts.
- State: each district is
Unexplored(dimmed on the globe),Known,VisitedorColony. It also has its own map seed, plus a faction and settlement for later. - The start: a settleable, temperate, not-too-rugged land district is picked (stable for the seed). It becomes the colony, and its neighbors become known.
- Map settings:
GetCurrentDistrictParams()gives world generation everything it needs for that district's map:- the seed
- height scale (0.55 flat to 2.2 mountains)
- water-table offset and plant density, from the biome
- the biome's surface
TerrainSwap(e.g. Glassdune:Soil=Sand) - the edge margin
- geography:
Riversize,bCoast,bLakeshore - land features:
Features(ids),LandmarkwithLandmarkName, andTags(every feature's tags together:Caves,Lake,Island,Ruins,Lava, ...). Features'PlantDensity,HeightScaleandWaterTableeffects are already folded into the values above.AColonyGameMode::StartGameapplies these when a game starts. It callsNewPlanet(Seed)with the game seed, then sets: AVoxelWorld::Seedto the district's seedHeightScale(multiplies the ground-height noise)WaterTableOffsetto -3 plus the district's water tableSurfaceTerrainSwapto the biome's swaps (a target terrain missing from Terrains.csv keeps the original)UPlantLayerComponent::WildDensityScaleto 0.5 × the plant density The log lineColony: district tile N, biome B, ...shows what was used.-ColonyNoPlanetskips the planet and keeps the plain seeded map, for tests that need fixed terrain.
The edge rule.
- No digging or building within
DistrictEdgeNoBuild(12) voxel columns of a district map's edge (UColonyPlanetSubsystem::CanModifyColumn(X, Y)). - The taskmaster refuses mining designations there, and typed pit orders that reach into the band are refused with the reason.
- The dev tools respect it: Dig, Raise, Paint, Place plant and rock mining.
- Any new building or terrain system must check it too.
Selection looks (all glowing white, as the user asked):
- Outlines:
ColonyGlow::SetOutlined(Actor or component, bOn, bHover)draws the object into custom depth/stencil (252 selected, 251 hovered). An unbound post-process volume (spawned once per world) withPP_ColonyOutlinedraws a glowing white edge around it, plus a faint wash over it. It's the technique of Brick and Bones'PP_Highlightas an edge. The pass setsr.CustomDepth 3at run time. - Markers:
ColonyGlow::MakeGlow()gives a white additive, pulsingM_ColonyGlowinstance, used for the planet's district circle and colonist selection rings. - On the planet: the selected district shows a bright ring about its own width; the district under the mouse shows a dim one.
- Landmarks on the globe: each landmark is a small diamond on the ground, outlined dark and colored by its \
Category\: geographic amber, coastal teal, water blue, ruins violet. Its name ("Veyrath basin") faces the camera at a constant screen size. Names show for every landmark facing the camera once you zoom in to \LabelZoom\(2.4 globe radii). Zoomed out, only the hovered and selected landmarks are named. - The materials:
Scripts/build_colony_fx_materials.pybuilds the three in/Game/Colony/FX(run it as an editor commandlet:UnrealEditor-Cmd <project> -run=pythonscript -script=<path>).
Data files
Planet.csvKey,Value:Frequency,LandFraction,TemperatureOffset,RainfallScale,Mountains,EquatorTemp,PolarTemp,LapseRate,MaxElevation,DistrictEdgeNoBuild,RiverFlow,LakeMaxTiles,LandmarkDensity. The new-game screen may override the world settings later.Biomes.csvId,Label,Color,Water,Settleable,MinTemp,MaxTemp,MinRain,MaxRain,MinElev,MaxElev,MinSwamp,MaxSwamp,MinHills,MaxHills,Priority,Patch,PlantDensity,WaterTable,TerrainSwap:Coloris written(R=..;G=..;B=..;A=1), with semicolons because the file is comma-separated.- Empty conditions mean "any".
TerrainSwapisFrom=To;From=TousingTerrains.csvnames. Mods add or override biomes byId. The base set is 3 water biomes and 17 alien land biomes, in the roles of RimWorld's (temperate forest = Glowwood, desert = Glassdune, glowforest = Lumen grove, scarlands = Archotech scar, and so on).
LandFeatures.csvId,Label,Landmark,Category,Biomes,NotBiomes,MinHills,MaxHills,MinTemp,MaxTemp,MinRain,MaxRain,Needs,Forbids,Chance,Secondary,Excludes,PlantDensity,HeightScale,WaterTable,Tags,Description:Landmarkis 1 for landmarks.BiomesandNotBiomesare;lists of biome ids (empty means any land biome).NeedsandForbidstake geography fromCoast,Lakeshore,River,LargeRiver,Headwater,Confluence,Delta,Inland.SecondaryisId:odds;..., andExcludeslists features it can't share a district with.- The effects multiply plant density and height and add to the water table.
Tagsare free-form, for world generation. The base set (about 75 rows) follows RimWorld Odyssey: - geographic, coastal and inland-water landmarks;
- precursor ruins and vents in the roles of ancient sites;
- Lyridia's own lumen well and blight lake;
- mutators for caves, life, weather, ground and ore. Mods add or override features by
Id.
Code hooks
UColonyPlanetSubsystem::Get(World), then:GetPlanet,NewPlanet(Seed),DescribeTileGetCurrentDistrict,SetCurrentDistrict,SetDistrictState,GetCurrentDistrictParamsCanModifyColumnShowPlanet,TogglePlanet,IsPlanetViewActive,SelectTileOnDistrictSelected(delegate)
FColonyPlanet::FindTile(direction, start),Distance(A, B)(in tiles),GetTile(i). A tile hasNeighbors,Corners, its climate,River,RiverTo,bCoast,bLake,Features,LandmarkandLandmarkName.FColonyPlanet::Features()andFindFeature(Id).- Console:
colony.Planet: toggles the view.colony.Planet.New <seed>colony.Planet.Info [tile]
- Test hook:
-ColonyDevTest -ColonyPlanetTestgenerates seed 1337 and logs:- district count, pentagons and evenness
- land share and the biome breakdown
- rivers by size, lakes, coastal districts and landmarks, counts per feature, and a few landmark districts
- the start district and its map settings
- the edge rule at a few columns It then opens the planet view and takes three screenshots: the start district, the far side, and the landmark nearest the colony up close with names showing. It then returns to the district view and quits.
Not yet:
- The district map built from the tile's features: dev1's hook can read
Tags(caves, lakes, islands, ruins and rivers on the map). Only the plant, height and water effects apply today. - Searching for landmarks, and settlement markers.
- Travel and caravans, settlements and factions.
- World events, hidden areas and trade locations on the map.
- Roads (they come with settlements).
- Fake terrain under fog beyond the district edge.
- The onion-layer planets: each layer would get its own grid and biome set using the same system.
12. Mods, the modding API and multiplayer-ready design
Quick start and tools: ModKit/README.md. Plan for a multiplayer mod: ModKit/MultiplayerInADay.md.
Mods and load order (ColonyMods.h, FColonyModRegistry; ColonyDataFiles.h)
- A mod is a folder
Mods/<folder>/with an optionalmod.json. Its fields:id: lowercase, digits and_; it prefixes the mod's action types.name,version,gameVersion,description,authors.requires:"id"or"id>=1.2"; the listed mods must be installed and on.loadAfter: soft ordering.loadOrder: an int; lower loads first.network:required(the default),clientorhost.
- A mod can have
Data/.csv(the same files and columns asContent/Data) andContent/.pak(packaged assets). - Load order:
- The player's saved order.
- Then
loadOrderand id. - Then dependencies, which always load first.
- A mod is active when it's on, its
mod.jsonreads, and everything itrequiresis active at a high enough version. - Data files load the base game's copy, then each active mod's copy in that order (
ColonyData::GetDataFilePaths). The same row name replaces the earlier row and new names add rows. Mods can't delete rows. - The player's choices are saved in
GameUserSettings.ini([ColonyMods] Disabled=,Order=).-ColonyNoModsstarts with none, and-ColonyMods=a,bwith only those. - Content: at game start each active mod's
Content/*.pakis mounted and/<id>/maps to it. Paks only mount in packaged builds; the editor logs and skips them. - Validation (
FColonyModRegistry::Validate) checksmod.jsonand everyData/*.csvagainst the base file:- the key column;
- unknown and missing columns;
- rows without a key;
- cell counts;
- duplicates. It runs as the Mods screen's Check button, as
colony.Mods.Validate, and as theColonyModValidatecommandlet (UColonyModValidateCommandlet:-run=ColonyModValidate [-Mod=<id>], exit code 1 on errors).
colony.Mods.ListrescansMods/and logs every mod, its state and problems, and the network fingerprint.UColonyModSubsystem(a game instance subsystem) scans the mods and mounts their content when the game starts, so the first data load sees the player's choices.
In-game Mods screen and data editor (ColonyModEditor.h)
- 05 Mods, the start menu's fifth stop, lists every mod in load order. Each row has:
- an on/off switch and its state (ON, OFF, or CAN'T LOAD with the reason);
- its network mode;
- load-order arrows;
- Check, Edit data and Folder buttons. Create makes
Mods/<id>/with amod.json.
- The data editor shows a data file as the game will load it (base plus active mods), with each row's source.
- Cells are editable, rows can be added (a new row starts as a copy of the last one), and you can filter by name.
- Save writes only the rows this mod changes or adds to
Mods/<id>/Data/<file>. - Changes apply the next time the game starts.
Unreal Editor Mod Kit (ModKit/)
Templates/example_mod/: a complete data mod; it adds the terrain Glow Loam.Templates/example_content_plugin/: the content-only plugin a content mod works in, atPlugins/Mods/<id>/.PackageMod.ps1 -ModId <id> [-ReleaseVersion <v>]:- Validates.
- For mods with assets, cooks and paks the plugin as DLC against a released base game (
BuildCookRun -DLCName). - Zips the mod to
ModKit/Out/<id>-<version>.zip.
- The content step needs a packaged release of the game made with
-createreleaseversion, so it hasn't been run yet. Data mods package today.
Modding API (UColonyModLibrary in ColonyModLibrary.h, Blueprint category Colony|Mod)
The stable surface for C++ and Blueprint mods:
- Actions:
MakeAction,SetActionArg,GetActionArg*,SubmitAction,RegisterActionHandler. - Net:
HasSimAuthority,IsMultiplayer,GetColonyGameState. - Entities:
GetColonyId,FindEntity,GetColonists. - World:
GetWorldSize,GetTerrainAt,FindTerrainId,GetTerrainDisplayName,GetSurfaceZ,GetGameDay. - Events:
GetColonyEvents. - Mods:
GetActiveModIds,IsModActive,GetModFolder,GetDataFilePaths,GetModNetFingerprint.
Multiplayer-ready design
The game is single player and host-authoritative by construction:
| Piece | File | Role |
|---|---|---|
| Actions | ColonyActions.h (UColonyActionSubsystem) | Every player-caused change is an FColonyAction ("Domain.Verb", named text args as FColonyActionArg, sender's player id) through UColonyActionSubsystem::Submit. On the host it runs the handler registered for its type; on a client it goes to the IColonyActionTransport a multiplayer mod installs. Handlers come from C++ or Blueprint, and a mod can override a base action. Executed actions are logged (colony.Net.Actions) and broadcast. Core.Log is a test action; colony.Net.Submit <Type> Key=Value submits from the console. |
| Authority | ColonyNet.h | ColonyNet::HasSimAuthority: everything that simulates checks it. |
| Entity ids | UColonyEntityComponent, UColonyEntityRegistry | Host-allocated ids that replicate and save, never 0, never reused. |
| Game state | AColonyGameState | Replicated: speed, calendar day, world size and seed, aging, the host's mod fingerprint, and mod key/values (SetModValue). |
| Events | ColonyEvents.h (UColonyEvents) | OnActionExecuted, OnWorldEdited, OnEntitySpawned / OnEntityRemoved, OnGameStarted, OnDayChanged, OnModMessage (mod-to-mod). |
| World edits | ColonyWorldEdits.h (UColonyWorldEditLog) | FColonyWorldEdit (SetTerrain, PlacePlant, RemovePlant, RemoveRock, Custom per mod), recorded by the systems that change the world, compacted per voxel or column, and replayed onto a freshly generated world. |
| Snapshots and saves | ColonySave.h (UColonySaveSubsystem, UColonySaveGame, FColonySaveSection) | FColonySnapshot holds the settings, edits, clock, id counter, one versioned section per system (RegisterSection) and mod values. ToBytes / FromBytes give compressed bytes for joins. SaveToSlot / LoadFromSlot handle Saved/SaveGames. Load = generate from the settings, then ApplyPending. colony.Net.Snapshot round-trips one; colony.Net.Save <slot> saves. |
| Mod matching | ColonyMods.h | GetNetFingerprint (only network: required mods), DescribeMismatch. |
Save sections (core sections are PascalCase; mod sections are ModId.Key; versions start at 1 and every shipped version stays readable). They load after the world edits replay, in no guaranteed order between them:
| Section | Owner | Holds |
|---|---|---|
Colonists (v3) | AColonyGameMode | every colony member (see "Colonists" above): id, place, identity (names, xenotype), look, age, health |
Items | UColonyItemSubsystem | stacks on the ground |
Plants (v1) | UColonyFloraSaveSubsystem (ColonyFloraSave.h) | every plant by its type's name, cell, size and facing. On load it clears the layer the seed and the replayed edits made and puts these back, recording no edits, so new or reordered Plants.csv rows, density changes or new plant models can't change a save's flora. Types that no longer exist are skipped and counted in the log. Plant changes are still recorded as world edits (joins and older saves use them), but a load with this section skips replaying them. |
Species (v1) | UColonySpeciesSubsystem | the generated catalog (seed, an inputs fingerprint, every species with its names, colors, shape and fauna look), studies, identified species and the log. A load keeps the saved species even if the data files changed since (logged), and research is kept. |
Animals (v2) | UColonyFaunaSubsystem | each animal's function, colony id (v2; v1 saves get new ids), feet position, facing and variant. They're spawned once loading has finished and the species exist, so they don't depend on section order. |
Saves from before a section existed load the old way: no Plants section means the seed plus replayed plant edits; no Species means species from the seed and no research; no Animals means a fresh animal population.
Readiness (systems routed through the core)
| System | Owner | Status |
|---|---|---|
| Core (everything above) | modding session | Done, built and tested |
| Controller tools, speed, possession, moves → actions | dev1 | Done (see section 7, "Actions, saves and the multiplayer seams") |
| Voxel, collapse, plant and rock-mining edits recorded, plus appliers | dev1 | Done. Gap: a rock that falls in a collapse moves its instance, and replay only moves the voxels |
GameMode uses AColonyGameState, loads snapshots (-ColonyLoad=<slot>) | dev1 | Done; the -ColonySaveTest round trip passes |
| Colonist entity id, seeded randomness, Colonists save section | dev1 | Done |
| Taskmaster, Social, Species, Planet: host-only ticking, actions, save sections | Systems Dev | Pending |
| Seeded randomness in Taskmaster and Species | Systems Dev | Pending |
13. Planned modding support
Not built yet. This is roughly the order it's expected to land in. See Docs/Research/MinecraftTerrainAndWater.md for the reasoning behind the world generation and water items.
- World-generation stages as mod hooks (the stages exist; mods can't add to them yet), so mod terrains can appear in new worlds
- Sectioned voxel storage (skipping all-rock and all-air sections) before districts get bigger
- Plant growth, harvesting and farming
- Streams, rivers (
Stream,River) and oceans at generation, now that water flows (section 3, "Flowing water"). Flow follows Minecraft (water isn't conserved), not the research doc's volume-conserving plan. - Constructed floors that flatten the ground, plus overlays (snow, pollution, filth, ash, burn damage) that change a surface without replacing its terrain
- Data-driven colonist definitions, and spawning colonists from the Mutable COs (section 6) with randomized outfits and body shapes
- Body plans beyond the human one (
BodyParts.csvand health per part exist, section 6; seeDocs/Design/BodyParts.md) - Building and hauling jobs (mining exists, section 8)
- Save and load: the format, world edits and colonists exist (section 12); a Load game menu and the remaining system sections are to come
- Content mods: the loader mounts mod paks (section 12); packaging is untested until a packaged release exists
14. Lyridia's look (character ink, grade, stylized materials)
The target is a hand-painted, warm, lantern-lit scene seen from above. Characters stand out with a thick, solid, dark outline, like a sticker's edge. The stylized shading lives in the materials (painted textures, skin subsurface, AO, rim; see Epic's "Stylized PBR Character Shading" course), not in a full-screen filter. What runs on screen:
| Piece | File |
|---|---|
| The look's post-process volume: ink, grade, console settings, test | ColonyArcane.h/.cpp (UColonyArcaneSubsystem) |
The character outline material /Game/Colony/FX/PP_ColonyInk | Scripts/build_ink_material.py |
| Inking meshes (custom stencil 10), handing over to selection | ColonyGlow::SetInked, ColonyGlow::IsInked |
The old full-screen painterly filter PP_LyridiaArcane (off) | Scripts/build_arcane_material.py |
- Who gets ink: colony members only (
AColonistCharacter::IsColonyMember, checked byUColonyArcaneSubsystem::InkCharacters, which also un-inks anyone who stops being one). Zombies, animals and other non-members have no outline (the user's rule). Hover and selection outlines are separate and still work on anyone.
The ink.
- What: a solid dark line just outside a character's visible silhouette. There are no inner lines and nothing on terrain or props.
- Occlusion: the line shows over whatever is behind the character, never over anything in front of it.
- Width:
colony.Ink.Widthpixels at 1080p (5). The line is full width atRefDistance(25 m) and gets thinner farther away, never below 45%. - Which characters: colonists are inked automatically every half second, including new colonists, clothes and hair. Only colonists get the outline: animals and other characters that aren't colonists stay uninked.
- Selection: while a character is selected or hovered, the white selection glow (stencil 251/252) replaces the ink. Clearing the selection hands the outline back to the ink.
- Cost: the pass walks 16 directions outward up to about 1.4 × the width, so roughly 90 stencil reads per pixel.
The grade:
- saturation ×1.12 and contrast ×1.06;
- warm highlights, shadows kept warm-dark rather than blue;
- a slightly richer shadow color and a soft vignette.
Console:
colony.Ink 0/1andcolony.Ink.Width.colony.Look.Grade 0/1.colony.Arcane 1turns the old full-screen filter back on for comparison: Kuwahara strokes, edge ink, value bands and grain, tuned withcolony.Arcane.*. It's off by default because it smeared textures and inked every edge. Its lookups now use each scene texture's own UVs, clamped to the view, which fixes the garbage strip along the bottom edge of screenshots.
Stylized materials.
- The master:
/Game/Colony/Materials/M_LyridiaStylized, built byScripts/build_skin_material.py. Skin comes first.- It uses the subsurface shading model with a warm scatter.
- The albedo is flat and comes from one colour,
SkinColor(linear). It uses no MetaHuman base colour, so a face and a body fed the same value match by construction. - A soft warm Fresnel rim and a small
ShadowLiftglow keep shadows warm rather than black.
- Instances: colonists get runtime instances (dev1's
AColonistCharacter):- head slots use
MI_LyridiaSkin_Face, which has the static switchIsFaceon; - body slots and the anatomy wearable use
MI_LyridiaSkin_Body.
- head slots use
- Parameters:
SkinColor;NippleStrength(0),NippleL,NippleR(u, v, UV radius),NippleColor: procedural areolas in UV space (bodies; see Nipples under Body shape);DebugUV(0; 1 paints the UVs, for tests);SubsurfaceColorandSubsurface(0.8);Roughness(0.7) andSpecular(0.35);RimColor,RimStrength(0.12) andRimExponent(3.5);ShadowLift(0.1);NormalMapandNormalStrength(0 = flat);AOMap(red channel) andAOStrength(0).- Face only:
DetailMask(R = lips, G = blush),LipColor/LipStrengthandBlushColor/BlushStrength. - Face only,
NeckHide(0..1, default 0): fades the face mesh's collar out along its vertex colour R (0 at the neck, 1 at the collar's open edge; dev1's face commandlet writes it), so a child's narrower body shows instead of the adult-sized collar. The fade starts atNeckHideStart(0.35) when fully on and ramps overNeckHideSoftness(0.15), dithered.MI_LyridiaSkin_Faceis Masked through an instance override; the master and body stay Opaque. Only set it on face meshes that have the gradient.
- Never edited: the Fab
MHC_*/MC_*originals. - Still to do: a hair pass to make it clumpier and more painted; painted environment textures and lantern lighting.
Test hook: -ColonyDevTest -ColonyArcaneTest takes screenshots with the look off and on, a colonist close-up both ways, and the planet, then quits.
15. Items, materials and quality
Every portable thing is an item: resources, food, medicine, organs, prosthetics, weapons, apparel, shells, artifacts, books and so on. The schema follows RimWorld's item ThingDefs (category=Item). Buildings, plants, creatures, terrain and projectiles are not items.
| Piece | File |
|---|---|
| Item, material and quality definitions; stacks and their value, mass, hit points and stats | ColonyItems.h/.cpp |
| Stacks on the ground, their look, saving, console commands, test | ColonyItemWorld.h/.cpp (UColonyItemSubsystem, AColonyItemActor) |
| The Cut job: felling plants drops their yield | ColonyTaskmasterSubsystem (DesignateCut), Jobs.csv |
| The starting tables (regenerate or edit by hand) | Scripts/make_item_tables.py |
A stack (FColonyItemStack) is an item, the material it's made of (for stuffable items), its quality, a count and the top item's hit points.
- Materials ("stuff"): wood, metals, stone blocks, fabrics, wools, leathers. A material is an item itself (its
Items.csvrow gives value and mass). ItsMaterials.csvrow says what it does to things made of it: hit points, beauty, work, armor (sharp, blunt, heat), melee damage, flammability and colour. - Which materials: an item lists the categories it accepts (
StuffCategories:Metallic,Woody,Stony,Fabric,Leathery) and how much it takes (StuffCost). - Quality: items with
QualityAllowedget Awful, Poor, Normal, Good, Excellent, Masterwork or Legendary.- It scales value (×0.5 to ×5), and damage, armor and insulation (×0.8 to ×1.65), but not range or cooldown.
RollQualitygives a bell curve that skill shifts up.
- Value is the item's base value plus
StuffCost× the material's value, × quality, less for a worn top item.Describe()reads "Steel longsword (good)", "Wooden club (masterwork)", "Wood x25".
From the test, longswords:
| Material | Value (normal / masterwork) | Hit points | Sharp damage (normal / masterwork) |
|---|---|---|---|
| Wooden | 160 / 400 | 65 | 10.6 / 15.3 |
| Steel | 230 / 575 | 100 | 20.7 / 30.0 |
| Plasteel | 940 / 2350 | 280 | 25.3 / 36.7 |
On the ground (UColonyItemSubsystem):
- One stack per voxel column, like RimWorld's one item per cell.
Drop(Stack, Column)merges into a matching stack up toStackLimitand spills the rest to the nearest free columns (dry ground, no trunk or rock, within 4). - Taking:
Take(Column, Count).GetStackAt,GetStacksandCountOnGroundread. - The look: the row's
Meshfitted so its longest side isSizeCm, a little bigger for big stacks. With no mesh, a box tinted by the material (or the item'sColor), in the item's rough shape: long and thin for weapons, a folded bundle for clothes and textiles, a crate otherwise. 159 rows use Fantastic Village Pack meshes: food, sacks, bottles, barrels, logs, stones, the knife, scroll and paper. - Saving: the
Itemssave section (version 1). A new map or a load clears the ground first.
Cutting plants:
- The Cut job (
Jobs.csv, the Growing work type, 10 work seconds): the colonist walks up to the plant, cuts it down, and its yield drops where it stood. - Yields:
PlantYields.csvgivesPerMeter× the plant's height, clamped toMin..Max, matched by the plant's own name, else its category. Trees giveWoodLog(about 6 per metre); bamboo gives fewer. - Designating:
UColonyTaskmasterSubsystem::DesignateCut(X, Y); it replaces a study job on the same plant. Designations made within 2 s of each other form one task, like mining.colony.Cut [radius]marks every tree around the selected colonist. The dev panel's Cut plants (job) tool designates every plant within the brush radius of the clicked column (drag to paint). Hovering a column with a stack shows it in the cell readout ("Items: Wood log x24 (value 48, 36.0 kg)", fromDescribe,GetMarketValueandGetMass). Zones are in the Build menu (SZoneBuildSection,ColonyZoneTool.h/.cpp): square tiles with drawn icons (SZoneIcon, lines and boxes, no textures) for Stockpile and Remove zone. Pick one, press on the ground and drag; the shape shows as tiles (blue to mark, light red to take away) and applies on release (right click cancels, Esc stops). Three option rows shape the drag (FZoneShapeOptions,ColonyZones::ShapeCells): Shape (rectangle, ellipse, diamond, line), From (corner to corner, or press at the centre and drag to the edge) and Fill (filled, or a one-block outline; lines ignore it). A drag covers at most 121 x 121 columns. While Remove zone is on, the system cursor is hidden and a light red arrow with a minus badge is drawn in its place (SZoneCursor, a viewport layer above every panel). A release marks or clears all its columns in one call (UColonyItemSubsystem::SetStockpileCells). The cell readout starts with "Stockpile." over one. The hovered cell and the tool brush are a deferred decal projected through the terrain (M_ColonyBrushDecal,AColonyPlayerController::UpdateBrushDecal), so they follow slopes: white for selecting, orange for brush tools at the brush size (round for Cut), blue or light red for zone tools; an older build without the material falls back to the box outline.-ColonyZoneTestscripts a stockpile drag and a remove drag with UI screenshots. The selected colonist's readout lists every need fromNeeds.csv(Food among them) with mood (DescribeColonist).
Eating (the Food need, Needs.csv; the Eat job, Jobs.csv):
- The need: Food empties over most of a calendar day (
DecayPerDay1.2). BelowSeekBelow(0.3) a colonist goes to eat, and belowUrgentBelow(0.12) it drops its work for it. - Choosing food: the nearest food on the ground (
ColonyItems::IsEdible: hasNutritionand isn'tAnimalFeed), where a meal is worth a walk of 20 columns more than raw food, and no two colonists go for the same stack. - Eating: on arrival it eats as many items as the need has room for (one meal at 0.9, or about 19 raw at 0.05 each), then takes the job's 3 s. Eating on arrival means the need is already up before the taskmaster picks the next job.
- Mood: raw food gives a -4 moodlet, a fine meal (value 20+) +3, a lavish one (40+) +6, each for 600 s.
- No food: when no food lies anywhere, the taskmaster is told (
SetJobAvailable) and leaves the Food need alone, so hungry colonists don't loop. There's no starvation yet: an empty stomach only costs mood. - Test and dev hook:
UColonyTaskmasterSubsystem::AdjustNeed(Colonist, Need, Delta)changes a need.
Hauling and stockpiles (the Haul job, Jobs.csv, Hauling work type):
- Stockpiles: zones of columns,
UColonyItemSubsystem::SetStockpile(Min, Max, bOn), drawn as tinted floor tiles and saved with the items (section version 2). For now they accept every item; filters come later. - Haul jobs: every 2 s the taskmaster posts one for each loose stack (outside stockpiles) that some stockpile column can take: an empty column, or a matching stack with room (
FindStorage, which prefers topping up). They belong to one standing task, "Haul to stockpiles", which the player can reorder or pause like any other. - Carrying: the hauler walks to the stack, picks it all up (
ShowCarriedholds it in front of the chest), claims the nearest free storage column, walks there and puts it down. A big stack spills to columns next to it. - Interrupted: a hauler that's stopped, fails, gets possessed or is caught in combat drops what it carries where it stands; a new haul job will pick it up.
Data files
Items.csv, one row per item, never condensed: each leather, egg (fertilized and not), organ, weapon and meal variant is its own row. Mods add or override rows byId.Id,Label,ItemClass,Category,SubCategory,TechLevel,MarketValue,Mass,StackLimit,MaxHitPoints,Flammability,DeteriorationRate,RotDays,Beauty,Nutrition,QualityAllowed,StuffCategories,StuffCost,DefaultStuff,Tradeability,Craftable,Stats,Tags,Mesh,SizeCm,Color,Dlc,DescriptionItemClass:Material,Resource,Component,Textile,Food,AnimalProduct,Meal,Drug,Medicine,Organ,Prosthetic,WeaponRanged,WeaponMelee,Grenade,Shell,Apparel,Armor,Utility,Artifact,Neurotrainer,Techprint,Book,Relic,Unfinished,Chunk.Stats:Key=Value;.... Weapons useDamage,Range,CooldownandBurst, orSharpDamageandBluntDamage. Apparel usesArmorSharp,ArmorBlunt,ArmorHeatandInsulation. Medicine usesPotency.Color:(R=..;G=..;B=..;A=1).Mesh: a/Game/...static mesh path. Leave it empty for a box.Dlc: the RimWorld source (Core, Royalty, Ideology, Biotech, Anomaly, Odyssey) orLyridiafor our own rows.
Materials.csvId,Label,Categories,Color,HitPointsFactor,BeautyFactor,WorkFactor,SharpArmor,BluntArmor,HeatArmor,SharpDamage,BluntDamage,Flammability.Idmust be an item. There are 41 rows: wood, 7 metals, 5 stone blocks, 9 fabrics and wools, 19 leathers.Qualities.csvId,Label,ValueFactor,StatFactor,BeautyFactor: overrides the built-in RimWorld factors.PlantYields.csvPlant,Item,PerMeter,Min,Max:Plantis a plant name or category (Tree,Wild,Crop,Decorative,Stump).
The starting set has 290 items in all 25 classes. Values are approximate (RimWorld 1.5 and its DLCs). Lyridia adds rows for the pack's fruit, vegetables, bread, cheese, sausage and fish.
Code hooks
ColonyItems::FindItem,FindMaterial,MakeStack(Item, Count, Material, Quality),RollQuality,AcceptsMaterial,Quality(Q),FindPlantYield.FColonyItemStack::Describe,GetMarketValue,GetMass,GetMaxHitPoints,GetStat(Name),CanStackWith.UColonyItemSubsystem::Get(World), thenDrop,Take,GetStackAt,GetStacks,CountOnGround.UColonyTaskmasterSubsystem::DesignateCut(X, Y). When a plant is cut, the colony eventPlantCutfires.- Console:
colony.Items.Spawn <Item> [Count] [Material] [Quality]: drops near the selected colonist.colony.Items.List: every stack, with value, mass and hit points.colony.Items.Defs [Class]: counts by class, or one class's rows.colony.Cut [radius].
- Test hook:
-ColonyDevTest -ColonyItemTestdoes the following, then quits:- logs the tables and a longsword in three materials and two qualities;
- drops a spread of 17 items near the colonist and takes a screenshot;
- marks the nearest tree for cutting and waits for its logs;
- logs the count and takes a screenshot of the felled tree's logs;
- makes the colonist hungry and checks it eats (PASS or FAIL);
- makes a 5x5 stockpile and checks at least 3 stacks get hauled into it (PASS or FAIL), with a screenshot.
- Console:
colony.Stockpile [width] [length]makes a stockpile next to the selected colonist (a negative width clears it).
Not yet:
- Stockpile filters (by class or tag) and priorities; carrying several stacks at once.
- Starvation, and food poisoning from raw food.
- Rotting and deterioration over time, and the colonist's inventory and equipment.
- Crafting at benches, with quality rolled from skill.
- The Cut tool button, a Stockpile zone tool, an item tooltip and the Food need in the colonist panel (dev1).
- Lyridia's own rock types mapped onto the stone chunks and blocks.
- Per-animal meat, leather and eggs tied to the fauna system.