// 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.ini caps 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/Data is data-driven, and a mod can add or override its rows from Mods/<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:

ConstantValueMeaning
Voxel::BlockSize25 cmEdge length of one voxel
Voxel::AgentRadius1Voxels either side of a colonist's center column, so a footprint is 3 voxels wide
Voxel::AgentHeight8Clear voxels of headroom a colonist needs
Voxel::AgentMaxStepUp1Highest ledge a colonist can step onto
Voxel::AgentMaxDrop3Furthest 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

ColumnMeaning
NameUnique id, e.g. RichSoil. World generation and saves refer to terrain by this name
DisplayNameName shown to players
CategoryNatural, Stone, Water, Road, Floor, Special, or Lava
bSolidTrue = fills its voxel: walkable, collidable, part of the terrain mesh. Water is False
MoveSpeedWalk speed multiplier for colonists standing on it. 0 = impassable
FertilityCrop growth multiplier. 0 = can't farm (not used by any system yet)
HardnessRelative work to dig it out (not used yet)
bSupportsHeavyStructuresCan carry heavy buildings such as concrete walls (not used yet)
bIndestructibleTrue = can't be dug or removed (the Mantle)
bHiddenTrue = solid but never drawn by the terrain mesher, because something else renders it (RockCore, inside rock meshes)
TopColorAverage color on upward-facing surfaces, sRGB, as "(R=0.24,G=0.28,B=0.05,A=1.00)"
SideColorAverage color on slopes and cliffs, sRGB
TopLayerTexture layer used on top surfaces (0–7, table below)
SideLayerTexture layer used on slopes and cliffs

Texture layers (built by Scripts/build_terrain_material.py):

LayerLookTextureColor mode
0GrassPack_Bonus Grass_2Painted
1DirtPack_Bonus Stone_2 (35% detail)Recolored
2SandPack_Bonus Stone_2 (20% detail)Recolored
3StonePack_Bonus Stone_1Painted
4GravelPack_Bonus Stone_3Painted
5MudPack_Bonus Stone_2 (30% detail)Recolored
6IcePack_Bonus Stone_1 (25% detail)Recolored
7Planks and tilesPack_Bonus Wooden_Floor_1Painted
  • Painted layers keep the texture's hand-painted colors, shifted so the surface averages to the terrain's TopColor or SideColor.
  • 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):

PropertyDefaultNotes
SizeX, SizeY256District footprint in voxels (256 = 64 m). Set per game from the new game screen (64–1024)
SizeZ192District depth in voxels, mantle included (192 = 48 m)
MantleThickness4Voxels of indestructible Mantle at the bottom
CaveAmount1.00 = solid underground, 1 = default, 2 = very hollow
WaterTableOffset-3Water table relative to the district's average ground, in voxels. Lower = fewer, smaller lakes. The district's biome adds to it (section 11)
HeightScale1.0Ground height variation; set from the district's hilliness (0.55 flat to 2.2 mountains)
SurfaceTerrainSwapemptySurface terrain substitutions by name, from the district's biome (e.g. Soil=Sand)
WaterMaterial/Game/Terrain/M_WaterWater surface material; leave empty for the default
ChunkSize32Voxels per chunk side; one mesh per chunk
VisibleDepth16Voxels below a chunk's lowest surface that get meshed; deeper ground is meshed on demand
CutawayDepth40Voxels below the cutaway slice that the cutaway view meshes
Seed1337Same seed, same world
Material/Game/Terrain/M_TerrainLeave 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.

  1. 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.
  2. 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: Sand in the shallows (less than 3 voxels deep), Mud deeper
    • Shore (1–2 voxels above water): Marsh (very wet), MarshySoil (wet) or Sand
    • 3–5 voxels above water: Mud (wet), Sand (dry) or Soil
    • 16+ above the district average: StonySoil with Gravel patches
    • Elsewhere: Soil, with RichSoil where it's wet, LichenSoil where it's dry, and Gravel patches Everything else is rock:
    • Regional rock (Granite, Slate, Limestone, Sandstone or Marble) from a large-scale noise.
    • Deep Slate below about a third of the way down.
    • Mantle in the bottom MantleThickness voxels. Cave floors, walls and overhang undersides are rock.
  3. 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 WaterTableOffset voxels 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 become DeepWater, which is impassable. Streams, rivers and oceans need flow and aren't generated yet.
  4. 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.
  5. Rocks (meshes, see "Rocks" below): surface boulders, pebbles, stalagmites on cave floors, stalactites on cave ceilings, and boulders buried inside the ground.
  6. 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):

ColumnMeaning
NameUnique id
MeshStatic mesh path, e.g. /Game/RockEnv_Pack/Meshes/Rocks/SM_Rock_1.SM_Rock_1
PlacementSurface (boulders), Pebble (decoration), CaveFloor (stalagmites), CaveCeiling (stalactites, mesh flipped), Buried
MinScale, MaxScaleRandom uniform scale range
WeightRelative chance among rocks with the same placement
SinkFraction 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 RockCore terrain. It's solid for pathing, support, collapse and digging, but bHidden, 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 / 8 of 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 by Scripts/build_water_material.py. It's translucent, and its ShoreFade parameter (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 MoveSpeed replaces 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:
    1. 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.
    2. Down first: it flows into air or flowing water below, as falling water. Downward flow has no limit.
    3. 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.
  • 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::SetWaterVoxel rebuilds only the fluid surfaces and records nothing). The Water save 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 bump AVoxelWorld::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 at MaxUpdatesPerTick cells); 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;Decorative by 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's getFlow), 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.csv Key,Value: TickSeconds, SlopeDistance, DropOff, InfiniteSources, MaxUpdatesPerTick (20000), MaxStepMs (2), WashAway, PushSpeed. Mods override any value.
  • Lava (Lava, FlowingLava1..7, FallingLava in Terrains.csv, category Lava) flows by the same rules with Minecraft's Overworld numbers (LavaFluid): a step every 1.5 s, DropOff 2 (a source reaches 3 voxels: amounts 6, 4, 2), SlopeDistance 2, 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 into Obsidian and flowing or falling lava into Cobblestone, on the next step. Lava falling onto water turns that water into Stone. 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 takes BurnDamage (10) Burn damage every BurnInterval (0.5 s), like Minecraft's lavaHurt, downed or not.
    • Data/Lava.csv Key,Value: TickSeconds, SlopeDistance, DropOff, InfiniteSources, BurnAway, BurnBlocking, BurnDamage, BurnInterval, CaveLavaChance. For Nether-style fast lava, use 0.5, 4 and 1.
    • Generation (AVoxelWorld::GenerateTerrain stage 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. -ColonyLava turns 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.
  • Console: colony.Water.Enabled 0 freezes water and lava where they are; colony.Water.Stats logs pending cells, updates so far, lava cells hardened, cells in the save and the last step's time.
  • Test hook: -ColonyDevTest -ColonyWaterTest builds 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 -ColonyWaterTestShots to 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 one ParallelFor, 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); call FlushDirtyChunks() 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: Min and Max grow 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 CutawayDepth voxels below its slice.
  • Water surfaces follow the same ranges.
  • Chunk geometry is computed in parallel (BuildChunkMeshData is read-only) and applied to the mesh components on the game thread.
  • Column heightmap: SurfaceZ holds each column's first air voxel above its highest solid one. It's filled at the end of GenerateTerrain and kept current by SetTerrain and collapses. GetSurfaceZ reads 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).

StepThreadWhat it does
PrepareGeneration()gameLoads terrain, rock and plant definitions (data tables, asset references)
GenerateTerrain()workerStages 1–4: shape, surface rules, water, floating rock removal (each stage is itself ParallelFor)
GenerateFeatures()gameStages 5–6: rocks and plants (they create components and instances)
BeginBuildAllChunks()gameLoads materials and creates the chunk and water mesh components
ComputeAllChunkData()workerEvery chunk's terrain and water geometry, in parallel
ApplyBuiltChunks(Max)gameHands 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 WalkableFloorAngle set 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 by Scripts/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

ColumnMeaning
NameUnique id, e.g. Oak
DisplayNameName shown to players
CategoryTree, Wild, Crop, Decorative, or Stump (stumps, pods and other plant-related objects)
ShapePlaceholder model: RoundTree, ConeTree, PalmTree, Cactus, Mushroom, Bush, Grass, Crop, Flower, Moss, Vine, Stump, Pod
HeightCm, WidthCmFull-grown placeholder size
FootprintGrowing space: 1 = one voxel, 3 = a 3×3 area. Wide plants use 3 so they don't overlap
bBlocksMovementTrue = the plant's center voxel can't be walked through (trunks, stumps, cacti)
MoveSpeedWalk speed multiplier while passing through it (brambles 0.5, tall grass 0.85). Ignored if it blocks
Color, StemColorFoliage and trunk/stem colors, sRGB
HabitatTerrain names it grows on in the wild, e.g. "(Soil,RichSoil)"
WildDensityChance 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 × WildDensityScale per column. WildDensityScale (default 0.5) is a property of the VoxelWorld's Plants component, 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 0 rebuilds 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 Color parameter.

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 MoveSpeed above 0, the center column has AgentHeight clear 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 AgentMaxStepUp or drop by AgentMaxDrop.
  • Cost is (1 per straight step, √2 per diagonal, plus 0.5 per voxel of height change) divided by GroundSpeed: the destination terrain's MoveSpeed times any undergrowth's MoveSpeed. Colonists route around mud, marsh and brambles when the detour is cheaper.
  • FindNearestStandable snaps 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). -ColonyNoAreaCache turns 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.
    • FindNearestFree is FindNearestStandable that 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) read UColonyCharacterIndex (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 -ColonyJobTest mined 20 voxels with 4 designations left after this change (earlier runs: 19 or 20).
  • Editable properties: BodyColor and BaseWalkSpeed (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, an FColonyIdentity) 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 from UColonyEntityComponent. 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(), a Data/Xenotypes.csv row). Colonists are Human, zombies Zombie.
  • ColonistNames.csv Name,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, with Mods/<Mod>/Data/ColonistNames.csv.
  • Who rolls the names:
    • AColonyGameMode::SpawnColonistAt rolls 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.csv Name,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 SkinMultiply multiply it;
      • EyeColor replaces the iris color unless its alpha is 0.
    • ShapeOverrides ("Posture=0.8;HeadScale=1.1") sets shape sliders at BeginPlay.
    • bAdultOnly is 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.csv bDefault) 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 ShapeOverrides apply 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 CrowdFarTickInterval seconds when farther than CrowdFarDistance from the camera (Combat.csv). Movement and thinking still run every frame, so far zombies walk at full speed.
  • Setting the xenotype: call SetXenotype before FinishSpawning, or pass it to SpawnColonistAt(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.csv lists the body types, with columns Sex, DisplayName, BodyMesh, FaceMesh, Weight, AnatomyMesh, UnderwearMesh and Stage. Rows with an empty Stage are 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::BodyType and HeightScale).
  • 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_02 and head offsets by NeckLength;
    • it scales the head bone by HeadScale, which carries the whole face. The face is a poseable mesh. Every frame UpdateFace copies 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. UpdateFace then sets the eye bones (FACIAL_L_Eye, FACIAL_R_Eye, which only the face has): scale EyeScale and the eye height offset. The eyeballs follow them through their EyeParallel children, 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.
  • Face bones (for feature shaping): the face rig has bones for each feature, all under FACIAL_C_FacialRoot:
    • nose: FACIAL_C_Nose (with NoseTip and NoseLower), NoseBridge and NoseUpper (C, L, R);
    • mouth: FACIAL_C_MouthUpper and FACIAL_C_MouthLower (lips and LipCorner hang off them), plus lip skin bones;
    • jaw and chin: FACIAL_C_Jaw (Jawline, Chin, ChinSide), JawBulge and JawRecess;
    • cheeks: CheekInner, CheekOuter and CheekLower;
    • ears: FACIAL_L_Ear and FACIAL_R_Ear. -ColonyFaceBones logs them all, with parents and rest positions.
  • 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.py retargets the UE5 Mannequin unarmed set onto the MetaHuman skeleton. The set is engine template content, copied into /Game/Characters/Mannequins.
    • The script creates IK_Mannequin and RTG_Mannequin_To_MetaHuman in /Game/Characters/Animations/Retarget.
    • It writes 21 animations to /Game/Characters/Animations/MetaHuman: the BS_Idle_Walk_Run_MH blend 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 root to MetaHuman root, with pelvis as 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.py logs 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: LocomotionBlendSpace and FallAnimation on AColonistCharacter.
  • 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.mjs still 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 the FemaleRatio world rule, 0.5 by default.
  • The rules file: world rules live in Content/Data/WorldRules.csv, as Key,Value,Notes rows. Mods can override keys with their own Mods/<Mod>/Data/WorldRules.csv.
  • Read once: the game reads the file once when it loads (FColonyWorldRules::Get(), called in StartPlay).
  • 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.
RuleDefaultMeaning
FemaleRatio0.5Share of new colonists that are female
IntactBodyChance0.95Chance 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
HeadScale2Head size of new colonists (1 = MetaHuman default)
EyeScale1.2Eye size relative to the head, 0.5 to 1.2 (capped at 1.2)
NeckLength1.3Neck length (1 = MetaHuman default)
OldInjuryChance0.3Chance 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 of toon_3d_8 into /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::ApplyToonEyes attaches them to the face's FACIAL_L/R_Eye bones, sized to our eyeballs (1.45 cm) times the age head size, and hides the MetaHuman eyeLeft, eyeRight, eyeshell and eyeEdge sections. 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:

AncestrySkin tone range (0 = lightest, 1 = darkest)Eye colors (weights)
African0.68–1.0dark brown 8, brown 2
Hispanic / Latino0.25–0.75dark brown 4, brown 4, hazel 2, green 0.5, blue 0.3
East Asian0.2–0.5dark brown 9, brown 1
Middle Eastern0.3–0.65dark brown 4, brown 4, hazel 1.5, green 0.8, blue 0.3
South Asian (Indian)0.45–0.85dark brown 7, brown 3, hazel 0.5
European0.0–0.35blue 3, brown 3, green 1.5, hazel 1.5, grey 1
  • Columns: Weight (chance among ancestries), SkinToneMin, SkinToneMax, and EyeWeights (EyeDarkBrown=8;EyeBrown=2, using option names from AppearanceOptions.csv). The ranges are broad and overlap on purpose.
  • Skin color:
    • Every bundle body has the same medium-dark baked skin. Content/Data/SkinTones.csv recolors it per tone (FAppearanceCatalog::SkinLook, applied in AColonistCharacter::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, Saturation and Value are 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 use Multiply.
    • 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.
  • Eye color: eye options in AppearanceOptions.csv have a Color column. 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" = Color x 3. -ColonyDesignerEye=EyeBlue picks 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 -ColonyAncestryLineup films 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 at AgingRate (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.

StageEnds at (80-year life)
Baby1
Toddler4
Child13
Teen18
Adult60
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) in WorldRules.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 year DaysPerYear (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 in ApplyFaceFeatures, 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), so Age_Baby exists 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, to Saved/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.
  • Bodies by stage: a ColonistBodies.csv row with a Stage (Baby, Toddler, Child, Teen) is the body a colonist of that stage and sex wears (FAppearanceCatalog::BodyForStage), preferring the row whose DisplayName matches 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. RefreshAge swaps 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 wears Girl or Child; Teen wears Teen_Female or Teen_Male; Elder wears Elder_Female or Elder_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.py duplicates 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 an AnatomyMesh; underwear removal stays adult-only in code.
    • -ColonyDesignerAge=<years> sets biological age in designer tests.
  • 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 kindBody part
HeadHeadHead
LeftEye, RightEyeEyeLeftEye, RightEye
LeftEar, RightEarEarLeftEar, RightEar
NoseNoseNose
MouthMouthJaw
TeethTeethTeeth
Hair, BrowsHair, BrowsHead
BeardBeardJaw
LashesLashesHead
  • Options: come from Content/Data/AppearanceOptions.csv, with columns Slot (kind), DisplayName, Sex, Weight and MutableOption, and the optional Sculpt and bAdultOnly (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, Beard options): each row takes a piece of our sculpted kit (Sculpt, below), a groom (Groom, plus GroomHead) or a rigged mesh (HairMesh). Leave all empty for none. AColonistCharacter::ApplyHeadPiece handles grooms and meshes, ApplySculptPiece the kit.
    • Rigged meshes (hair cards from other packs) show in their reference pose, fixed to our head bone: the mesh's own head bone 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 the HairFit commandlet): every hair and beard option has a reshaped copy for each of the 8 colonist heads in /Game/Characters/HairFits/<Option>/, named GR_<Option>_<Head> for grooms and SKM_<Option>_<Head> for meshes (Head is the face mesh name without SKM_MHC_ and _FaceMesh, e.g. Base_Female). ApplyHeadPiece uses 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 GroomHead bounds 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, GroomHead names the pack's own head (SK_ROXY_HEAD for Roxy, SK_Body for 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, run UnrealEditor-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 -Save it 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.
    • Beards: the kit's beards and mustaches (men only, adults only).
    • -ColonyDesignerHair=HairRoxy1 and -ColonyDesignerHeadScale=1.1 set them in designer tests.
    • Head size runs 0.9 to 1.5 (the slider, Clamp and 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.
  • Groom details: the Groom column takes a groom asset.
    • The groom (the original or its reshaped copy) is fixed to the face's head bone with no binding asset, so it follows the head and scales with HeadScale.
    • The GroomHead column 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's Hair_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.
  • 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.csv lists 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 cyberpunk BrowSlit (two shaved slits) and BrowBlock (three geometric blocks).
    • Beards: BeardStubble, BeardBoxed, BeardGoatee, BeardChinStrap, BeardFade (stubble sideburns to a sculpted pointed chin); mustaches MustachePencil, MustacheChevron, MustacheHandlebar, MustacheBlade (two sharp split wedges), MustacheHorseshoe.
    • Hair: HairCrop, HairBob, HairPonytail, HairSpiky, and the cyberpunk HairUndercut (faded sides, slick top, a streak), HairMohawk, HairSideShave (one side shaved with cut lines) and HairBandBowl (a bowl over a shaved band).
  • 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 as Sources/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 Mask vertex colour), read by M_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 (FacialHairFit commandlet): 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's Age_*, Shape_* and Ref* 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 (Lashes slot) 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 bAdultOnly row: 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 ` --preview renders contact sheets (one per category; a column per piece, a row per view). --head takes any MetaHuman face FBX whose face Unreal has (a colonist face, or one in Sources/AgeFaces). Faces are exported to FBX from the editor UI or Scripts/export_face_reference.py in the editor; headless, the skeletal FBX exporter asserts. -run=FacialHairFit without -Save reports 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=HairFit in the head-change order above). The fits are git-ignored (Content/Characters/FacialHair/*/); the kit's source meshes and M_LyridiaHair are in git, so after a fresh checkout run -run=FacialHairFit -Save` once.
  • Designer tests: -ColonyDesignerBrows=BrowsSlit, -ColonyDesignerBeard=BeardFade, -ColonyDesignerLashes=LashLong pick 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: GetMutableParameters returns each slot's option, or Missing when its body part (or anything above it) is lost. A lost eye then sets LeftEye = Missing.
  • Panel: the colonist panel shows face, hair, brows, beard and eyes.
  • Customizable Objects: Scripts/build_colonist_mutable.mjs knows how to add one group per slot, with the CSV's options plus Missing. 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: Teeth was added to BodyParts.csv (under Jaw) 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::IsAnatomyShown requires FLifeStages::IsAdult: at least AdultAge (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.csv column AnatomyMesh (female rows: /Game/Characters/Wearables/Anatomy/SKM_Anatomy_<Type>_Female; male rows SKM_Anatomy_<Type>_Male, below).
  • Underwear: every female body wears underwear: Roxy's lingerie bottoms fitted onto that body, in the ColonistBodies.csv column UnderwearMesh (/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 as StreamerMode under [/Script/ColonySim.ColonyOptions] in GameUserSettings.ini; on by default; AColonistCharacter::IsStreamerMode/SetStreamerMode; -ColonyStreamerOff turns it off for tests) (IsUnderwearOff uses 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).
  • No painted briefs: MetaHuman always paints briefs into the baked body textures; show_top_underwear is 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_NoBriefs and T_Body_SRMF_NoBriefs (4096) beside the originals and points MI_Body_Baked at 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 BodyGraft commandlet (BodyGraftCommandlet.h/.cpp, editor only) runs, per female body type: `` UnrealEditor-Cmd ColonySim.uproject -run=BodyGraft [-Lift=0.15] [-Save] ``
    1. Cuts small regions out of the donor (IdaFaber ROXY, SK_ROXY_BODY_FULL): around nipple_l and nipple_r (4.5 cm) and genital (4.8 cm).
    2. Keeps only their fine surface detail, measured against the donor's own smoothed skin (rim pinned).
    3. Fits them onto our body: size from nearby landmark bones (clamped: hip width doesn't size the groin), then position refined against our skin.
    4. 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.
    5. Skin weights come from the body vertex underneath, so it follows the body with leader pose.
    6. Vertex alpha fades the rim out, dithered by the material. Without -Save it only reports. Re-running replaces the 4 wearables.
  • 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's SkinColor (set by ApplyColors, 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 (DonorSkinLum 0.49, times AreolaGain 2.5) blends toward SkinColor x NippleColor (0.5, 0.3, 0.28, the procedural nipples' tint). The donor normal gives the form (NormalStrength 0.6). SkinTint and SkinMatch remain 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 (-Raise moves 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>_Male with MI_LyridiaSkin_Body, so it takes the skin tone and shading of the body (ApplyColors sets its SkinColor). 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.
  • Test: -ColonyDesignerTest -ColonyDesignerSex=Female -ColonyDesignerBody=FemaleHeavy -ColonyDesignerAnatomy removes the underwear (adult check logged), with a torso framing for the second screenshot; -ColonyDesignerTorso gives the torso framing with the underwear on. -ColonyAnatomyUntinted leaves 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.

FeatureRegion (face bones whose names contain)
NoseNose, Nasal
Brow ridgeBrow, Forehead
Eyes (lids, sockets, lashes)Eye, Lid, Lash
Mouth (lips, corners, teeth)Lip, Mouth, Teeth, Tongue
EarsEar
Jaw and chinJaw, Chin
CheeksCheek
Head shapeSkull, 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) and Ref6 (any sex) come from three MetaHuman character packages unpacked into /Game/CharacterAssemblies/ (Sentinel, SentinelB, and the child SentinelC), 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 .mhpkg is a zip whose Manifest.json names its package root (/Game/CharacterAssemblies/<Name>/...). Extract each one's .uasset files into Content/CharacterAssemblies/<Name>/, where <Name> is the zip's top folder (Sentinel, SentinelB, SentinelC); each package carries its own Common/ 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. PolyartF and PolyartM (the Polyart pack's SK_Female_Head / SK_Male_Head) are the defaults; Kavya (Ref2) is version 1 for women. PolyartM gives no Eyes or Cheeks: its stylized eyes have no eyeball skinning to fit by, and its lids landed as heavy bags. -ColonyNoDefaultFace turns 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 its Weight, 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.
  • How it works: each reference is a set of morph targets on every head, one per feature (Ref2_Nose, Ref2_Eyes...). AColonistCharacter::ApplyFaceFeatures sets their weights on the face. Data: FColonistAppearance::Features, morph name (Ref2_Nose) to weight 0-1; missing means 0.
  • Building the morphs: the FaceMorph commandlet (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|Ears writes 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 -Source needed) rewrites only the Shape_* 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 -Save it only reports. Re-running replaces the morphs. To add a face: put its built MetaHuman face mesh in a new FaceRefs.csv row (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:Ref2 sets 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):

SliderLowHigh
FaceWidth, FaceLengthnarrow, shortbroad, long
JawWidth, JawAngle, JawDepthtapered, soft, backsquare, angular, forward
ChinProjection, ChinWidthrecessed, pointedprominent, broad
Cheekbones, CheekFullnessflat, hollowpronounced, full
EyeSpacing, EyeTiltclose, outer corners downwide, up
BrowRidgesoftprominent
NoseLength, NoseWidth, NoseHeight, NoseProfileshort, narrow, low, concavelong, 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, MouthDepthnarrow, backwide, forward
UpperLip, LowerLip, LipFullness, LipBalancethin, thin, thin, lower fullerlarge, large, full, upper fuller
EarProminenceflatsticking out
NoseRidgesharpsoft
EarRotationown angleturned 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/r bone scale in UpdateFace). 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; -ColonyDesignerNumbers turns 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::ApplyBodySkinDetail puts the body's own baked MetaHuman normal map (Normal Baked on the body asset's material) into the stylized skin's NormalMap and sets NormalStrength to the slider, so the muscle lines come back as shading while Shape_Smooth keeps the mesh smooth. Sentinels get at least half (Lerp(slider, 1, 0.5)). The face mesh stays flat.
  • Nipples (not morphs): ApplyBodySkinDetail also sets the stylized body skin's NippleStrength, NippleL/NippleR (u, v, radius in UV) and NippleColor (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 -NippleUV measures 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. UpdateAnatomy refreshes them when the underwear changes. Women with the underwear off also show the anatomy wearable's donor nipples on top (darker).
    • Tests: -ColonySkinLog logs the parameters per body slot; -ColonySkinDebugUV paints the body's UVs (material scalar DebugUV: R = u, G = v, B = half the UDIM tile).
  • 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 the spine_05 bone'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). -ColonyBreastLog logs 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_MassDown toward the same sex's Slim body, Shape_MassUp toward 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 -Save folds it into the base mesh of every body, anatomy and underwear (all LODs) and removes the morph, so it isn't blended every frame; ApplyBodyShapes blends it only into a mesh that still has it. After the bake, -BodyShapes writes it again only with -Smooth (it would smooth twice). The skin materials also get flat normal maps (T_Flat_N).
  • Shape_Sentinel (the Sentinel shape, 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 -BodyShapes run.
    • 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.csv BodyShape names a shape a row always wears at full weight (adults only): MaleAthletic wears Mercenary, 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; -Slots picks material slots by name, default Body) 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. The MuscleNormal column (MaleAthletic, MaleSentinel) makes it the map the Muscle definition slider shows.
    • His face: FaceRefs.csv Ref8 (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.csv rows MaleSentinel and FemaleSentinel (the female variant) use the athletic meshes with bSentinel TRUE: 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).
  • "Barbie" neck socket (-run=BodyGraft -NeckSocket [-Save]): bodies get Shape_NeckPlug (their own neck reaching up into the skull) and faces get Shape_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 in ColonistBodies.csv and points their materials at them. The originals stay, so pointing the parameters back undoes it.
  • -run=BodyGraft -Probe logs 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):

SliderWhat it does
ShoulderWidthstretches the upper arms' offset from the clavicles (±18%)
HipWidthstretches the thighs' offset from the pelvis (±20%)
TorsoLegspine 2-5 longer and lower legs shorter (+12% / -10%), or the reverse; the mesh moves so the feet stay on the ground
Posturebends 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), then BodyGraft and BodyGraft -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 a BP_<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 morph Seam_<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. ApplyBodyShapes turns 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): -WeldSeam also 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 the NeckHide scalar. FColonistBodyRow::HidesFaceCollar decides 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.
  • Neck seam colour: BodyGraft -SkinSeam measures 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 a FaceSeamTint for 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.

ColumnMeaning
NamePart id (Arm)
DisplayNameShown name; sided parts become "Left arm" / "Right arm"
ParentParent part id; empty = the root (Torso). A sided part under a sided parent attaches to the same side
SidesLR = one part per side; empty = one part
KindExternal, Internal (organ), Bone, or Slot (equipment slot like Waist, no health)
MaxHealthDamage before the part is destroyed
SexAny, Female or Male: which bodies have it (Breasts is female only)
RegionWhere the health diagram draws it: Head, Neck, Torso, Shoulder, Arm, Hand, Leg, Foot
bVitalLosing 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.

ColumnMeaning
GroupFor lists only
ArmorWhich armor stops it: Sharp (slashing), Piercing, Blunt, Heat, or None
WoundLeaves a wound on a body part. FALSE for effects and utilities (stun, EMP, smoke, extinguish, mining, rotting...)
InjuryLabelThe wound's name in the health tab (Gunshot, Stab wound)
ScarChanceChance (0-1) the wound leaves a permanent scar when it heals
NotesFree 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 -ColonyHealthTest use 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 (Colony or Undead) 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.
  • 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::UpdateAnimation checks it.
    • Downed colonists hold their death pose.
  • A blow, step by step (LandBlow, at the move's HitAt):
    1. Reach: the target must be within the move's Range and in front, or it's a Miss.
    2. Parry: a defender that faces the attacker and isn't swinging turns it aside with ParryChance (Parried).
    3. Crit: with CritChance, blunt blows stun (StunSeconds: no attacking or dashing) and sharp or piercing ones deal CritDamage times more.
    4. Armor: not yet; colonists wear none. The step is marked in the code.
    5. 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.
  • Outcomes:
    • Colonists go down when a vital part is lost, overall health falls below DownedHealth, or their fresh (not old, permanent) wounds add up to DownedDamage. 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 GetUp move if CombatMoves.csv has one; the packs have none yet, so the fall clip plays backwards (a little slower).
      • Movement returns when the clip ends. Until then IsBusy stays true, so the taskmaster waits.
    • 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 head on 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.
    • 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 there ZombieCorpseSeconds, then go.
  • 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 (Severed in the attack log).
    • Lost limbs vanish from the body: each lost limb's top bone is scaled to nothing at its joint (ApplyLimbLoss, LimbBone maps body parts to MetaHuman bones). These mods live in the anim instance's own LimbMods list, applied after the colonist's BoneMods, 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).
  • 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 as Attack ... 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 FleeHealth they dash away and run.
    • While fighting or downed, UColonyCombatComponent::IsBusy(Colonist) is true, so the taskmaster should leave them alone.
  • 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, optional Target and Move), Combat.Dash (Agent, Dir), Combat.SpawnZombies (Count, Near cell).
  • Console: colony.SpawnZombies [count] (around the possessed, selected or first colonist), colony.KillZombies.
  • Zombies (AColonyZombie, a subclass of AColonistCharacter):
    • Body: a colonist body of the Zombie xenotype (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 through ShapeOverrides).
    • 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 AggroRadius on the colonists' own voxel paths, with at most ZombiePathsPerFrame path 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). UColonyCombatSubsystem keeps it and saves it in its own "Combat" save section.
      • SpawnColonistAt with the Zombie xenotype respects it too: bAdultOnly in Xenotypes.csv applies only while the option is off.
      • The test flag -ColonyChildZombies turns it on in -ColonyCombatTest, which logs each zombie's age and stage and takes a close shot of the youngest.
    • Not colonists: zombies are never the colony's to command (IsBusy is 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.
      • -ColonyCombatTest checks 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.
  • 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 columns User (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, Cooldown and Weight.
    • 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.json for tuning HitAt.
    • The RamsterZ clips use the old engine-default bone compression, whose codecs UE 5.8 lacks: compressing a copy asserts on RotationCodec and crashes the editor. The script points the sources at DefaultRecorderBoneCompression in 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 (PlayRate 1.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_Dash is the only quick move.
  • 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 in GameUserSettings.ini as [ColonyUI] Accent=RRGGBB. -ColonyAccent=RRGGBB overrides 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: SCyFrame draws 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 (with Active and Primary states), SCyWindow (a // 01 TITLE header with a close key; it flies in each time it is shown), CySection (/// TITLE rule), CyKey (key chip), CyReadout, SCySegmentBar and SCyHex (swatches). Stock sliders, checkboxes, spin boxes, text boxes, scroll bars and combo boxes take ColonyUI::*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. -ColonyNoMotion makes every flight instant.
  • 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=N opens 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.
  • AColonyGameMode spawns 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::Tick moves 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() and GetLoadingSeconds() 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.
  • RegenerateWorld(Seed) rebuilds the world and moves colonists back to the spawn.
  • AColonyPlayerController handles input and holds the state the UI reads. AColonyCameraPawn is the top-down camera.
InputAction
WASD / arrow keysPan
Middle mouse dragLeft/right rotates the view, up/down tilts it (-85° to -25°)
Q / ERotate
Mouse wheelZoom
Left clickSelect colonist, or apply the active dev tool
Right clickMove selected colonist
FFocus camera on selected colonist
SpacePause / unpause
1 / 2 / 3Game speed 1×, 2×, 3×
F1Toggle dev panel
EscBack to the Select tool
PageDown / PageUpCutaway view: slice lower / higher by 4 voxels
HomeClear the cutaway
VTake 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×.
  • 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) and ShoulderOffset (45 cm).
    • The jump: JumpZVelocity 350, about 2.5 blocks high, with AirControl 0.35.
    • AColonyPlayerController::LookSensitivity (2° per mouse unit).
  • The camera:
    • The camera is a ViewArm spring 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.
  • 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: AColonyGameMode counts 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 (bPreview colonists 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: -ColonyDesignerTest screenshots both framings (-ColonyDesignerWhole makes the second one whole body too, and -ColonyDesignerYaw=<degrees> turns the colonist); -ColonyDesignerSex=Female picks the sex, and the face test flags are under "Face features" in section 6. Also:
      • -ColonyDesignerFrames=N takes N more shots 0.4 s apart, to watch the idle animation;
      • -ColonyDesignerLiveHead=<scale> changes head size mid-idle the way the slider does;
      • -ColonyFaceDebug logs 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 as ColonyDesignerSweep: 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|B runs just those sliders, -ColonyDesignerSweepStart=<s> waits before the first, and -ColonyDesignerSweepKeepHair keeps 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 at GetFaceCenter from 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 0 turns 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 in GameUserSettings.ini, section [ColonyPortraits] (OffsetX, OffsetY, Zoom, Size, Animated).
    • Test flags: -ColonyPortraitCustomize starts with the popup open; -ColonyPortraitsAnimated starts with animated portraits.
  • 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 (Floor category). 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 (or Colonists, 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):

  • -ColonyAutoTest skips 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.
    • -ColonyCaveTest puts the colonist on a cave floor below the meshed depth (after EnsureMeshed) and logs whether it stays standing.
    • -ColonyCollapseTest builds a granite pillar with an arm 8 voxels from the spawn, digs out its base and checks that it falls.
    • -ColonyFocusWater points the camera at the surface water nearest the spawn instead of the colonist.
    • -ColonyHealthTest spawns 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.
  • -ColonyDevTest skips 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; -ColonySplash forces it and -ColonyNoSplash turns it off.
    • Credits (Main menu, the button above Quit; -ColonyMenuCredits opens it at once): the title, then every row of Data/Credits.csv by 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.md is 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 (see Docs/Credits.md).
    • Mods: a mod's Data/Credits.csv is listed under "Mod: <folder>".
  • -ColonyMenuTest screenshots the start menu's first stop, flies to the map size stop and screenshots it, then quits. Add -ColonyMenuStop=N to begin on stop N, and -ColonyAccent=RRGGBB to shoot another accent.
  • -ColonyLoadingShot screenshots the loading screen a second into loading (use with -ColonyAutoTest and a large -ColonySizeX/-ColonySizeY so 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 AddViewportWidgetContent cover the whole screen. Give their root .Visibility(EVisibility::SelfHitTestInvisible). A root left at the default Visible takes 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.
  • StartGame sets 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): OnGameStarted fires when loading is done, OnDayChanged at 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:

ActionArgumentsFrom
Time.SetSpeedSpeed 0–3Space, 1/2/3, the HUD's speed buttons (SetGameSpeed)
Colonist.Possess / Colonist.ReleaseColonist idTab (PossessColonist / ReleaseColonist)
Colonist.MoveColonist id, Target (world position)Right click
Dev.TeleportColonist id, CellTeleport tool, dev panel (TeleportSelected)
Dev.SpawnColonistCellSpawn tool (selects the new colonist for the host player)
World.PlacePlantCell, Plant type idPlace plant tool
World.Dig, World.PaintTerrain, World.Raise, World.RemovePlantsCell, Radius (0–16), Terrain id, CutawayZBrush 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 RegisterHandler on 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.

World edits.

  • AVoxelWorld::SetTerrain and collapse moves (CollapseUnsupported) record SetTerrain edits.
  • The plant layer's PlacePlant and RemovePlantAt record PlacePlant / RemovePlant edits, 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 a RemoveRock edit 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 Colonists save 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> loads Saved/SaveGames/<slot>.sav at startup; colony.Net.Save <slot> saves.
  • -ColonySaveTest is the round trip:
    • with -ColonyDevTest it digs through World.Dig, logs the clock, edit count and every colonist, saves to SaveTest and quits;
    • with -ColonyLoad=SaveTest it logs the same lines after loading, for comparison.

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.

PieceFileThread
Data (jobs, needs, whims, work types, work lists, command words)ColonyTaskDefs.h/.cpploaded once, read-only
The brain: needs, mood, whims, tasks, job board, assignmentColonyTaskmaster.h/.cpp (FColonyTaskmaster)worker
The driver: walks colonists to jobs, times the work, applies effects; runs typed ordersColonyTaskmasterSubsystem.h/.cpp (UColonyTaskmasterSubsystem)game
Typed order parserColonyCommands.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:

  1. A need below OrdersYieldBelow (nearly empty). This is the only thing that interrupts a direct order.
  2. Its direct orders (forced tasks), in priority order. A new order interrupts whatever the colonist was doing.
  3. An urgent need (below UrgentBelow). This also interrupts ordinary work. The job keeps its progress for whoever picks it up next.
  4. A need below SeekBelow.
  5. A whim, sometimes. The chance is WhimChance + WhimChanceBored × how empty Recreation is.
  6. 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.
  7. 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 by and and 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 as dig, excavate, go to, take a break) plus every personal job's Id. Mine digs an area, Goto walks to a spot, and Stop drops the orders. Any personal job (Sleep, Stargaze...) is done once. Before another verb, go is 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.
  • Chains: steps split at then, after that, and at and or 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 on 127.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 is qwen2.5-1.5b-instruct-q4_k_m.gguf from huggingface.co/Qwen/Qwen2.5-1.5B-Instruct-GGUF (1.12 GB, Apache 2.0; SHA-256 6a1a2eb6…e434e9407e). Models are git-ignored, so fetch the file yourself. Without it, or with colony.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: ModelPrompt uses Qwen chat markup. It covers the verbs (with the phrases from CommandWords.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 -ColonyLLMTest sends 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.cs stages llama-server.exe and only the libraries it loads as loose files next to the game: llama-server-impl, llama, llama-common, mtmd, ggml, ggml-base, the ggml-cpu-* backends (one is picked for the CPU at run time) and libomp. 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, and THIRD_PARTY_NOTICES.txt with 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.

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.json as "word": "phonemes". The bake script lists missing words in unknown_words.txt.
  • Packaged: the int8 model (92 MB), the voice pack (28 MB), the lexicons, NOTICE.txt and the Apache 2.0 text. ThirdParty/TTS/models/ is git-ignored like the language model.
  • Per colonist (FColonistAppearance): VoiceId (a Data/Voices.csv row), VoicePitch (semitones, up to +-1.7, about +-10%) and VoiceSpeed (0.9 to 1.1). They're rolled last in Roll, by sex, from the Adult rows by Weight, 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.csv Name,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, plus Child_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.csv Id,Label. File order is where new tasks land by default. Only Mining has jobs today.
  • WorkLists.csv Id,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. Default is what new colonists get.
  • Jobs.csv Id,Label,WorkType,WorkAmount,Effect,Need,Radius. WorkAmount is in game seconds at normal speed. WorkType is empty for personal jobs (needs, whims, idling, orders). Effect is one of:
    • Dig: removes the target voxel, or the whole rock it belongs to, then lets unsupported terrain fall.
    • RestoreNeed: refills Need while working, and ends when it's full.
    • Wander: walks to a random spot within Radius blocks.
    • Visit: walks to a colonist within Radius blocks.
    • Goto: walks to the order's spot.
    • None: just spends the time on the spot. Idle, Wander, Mine and Goto must exist.
  • Needs.csv Id,Label,DecayPerDay,SeekBelow,UrgentBelow,Job,RestorePerSecond,MoodWeight. Values are 0..1 and drain per calendar day. A need named Recreation drives the whim chance.
  • Whims.csv Id,Label,Job,Weight,MinMood,MaxMood,MoodBonus,MoodSeconds,Need,NeedAmount.
  • CommandWords.csv Phrase,Verb. Typed-order words: a mod adds synonyms, or other languages' words, here.
  • Taskmaster.csv Key,Value: WhimChance, WhimChanceBored, BaseMood, OrdersYieldBelow.

Code hooks

  • UColonyTaskmasterSubsystem::Get(World), then:
    • DesignateMine / CancelDesignation
    • ExecuteCommand(Text, Selected, Cursor), GetCommandLog
    • GetTasks, MoveTask, SetTaskPaused, CancelTask, AssignTask
    • OrderMove, ClearOrders
    • SetWorkList / NextWorkList
    • GetStatus / DescribeActivity / DescribeColonist
    • PlanDig (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 EColonyJobEffect and ParseEffect. 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 in ExecuteStep that 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_ColonyBrushDecal in 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 1 draws a wireframe box per designated voxel (orange open, red direct order, green someone on it).
  • colony.Taskmaster.Enabled 0 stops 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):
      1. Logs how the parser reads a set of phrases.
      2. Gives a refused order, then two colonists a 7x7, 4-deep pit and a third a go-then-stargaze chain.
      3. Overrides one order with a new one and moves a task to the top.
      4. 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.

PieceFileThread
Data (event types, interaction types, bubble rules) and the brainColonySocial.h/.cpp (FColonySocialDefs, FColonySocial)worker
The bridge: snapshot in, mood and friends out, the logColonySocialSubsystem.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 FriendAbove make a colonist a friend and below AvoidBelow make 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.csv Id,Label,PastLabel,Radius,ActorMood,ActorMoodSeconds,WitnessMood,WitnessMoodSeconds,WitnessOpinion,MemoryDays. PastLabel is how a memory of the event reads ("finished a task"); it defaults to Label. Label reads after a name: "Bram brought the ground down". A mod adds an event with a row here plus a ReportEvent call where it happens.
  • SocialInteractions.csv Id,Label,Weight,MinOpinion,MaxOpinion,MinMood,MaxMood,OpinionOfRecipient,OpinionOfInitiator,InitiatorMood,RecipientMood,MoodSeconds. Label reads 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.csv Key,Value: BubbleRadius, BubbleHeight, ChatsPerMinute, PairCooldownSeconds, OpinionDecayPerDay, FriendAbove, AvoidBelow, MaxMemories, ThinkSeconds.

Code hooks

  • UColonySocialSubsystem::Get(World), then:
    • ReportEvent
    • GetOpinion(Who, Other)
    • DescribeRelations(Colonist) ("Bram: friend (+34) · Cass: −12")
    • DescribeMemories(Colonist)
    • GetLog() (recent interactions and noticed events)
  • colony.Social.Log 1 logs every interaction and noticed event.
  • Test hook: -ColonyDevTest -ColonySocialTest puts 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.

PieceFile
Species generation (names, looks), rules, fauna functionsColonySpecies.h/.cpp (FColonySpeciesCatalog)
Dressing the plants, identification, Study jobsColonySpeciesSubsystem.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..SizeMax times 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 DisplayName show that.
  • The species system keeps up to MaxStudyJobs Study jobs posted, one per unknown species, at the nearest plant of it within StudyRange blocks 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 StudiesToIdentify studies, 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 Identified social 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.csv Key,Value:
    • VaryTree, VaryWild, VaryDecorative, VaryCrop, VaryStump: 1 = that category gets new looks every playthrough.
    • SizeMin, SizeMax
    • StudiesToIdentify, StudyRange, MaxStudyJobs
  • SpeciesWords.csv Kind,Word,Value. These are the words names and descriptions are built from:
    • GenusStart + GenusEnd makes the genus ("Kor" + "rath").
    • FloraLeft + FloraRight makes a plant epithet ("glimmer" + "leaf").
    • FaunaLeft + FaunaRight makes an animal name ("dusk" + "strider").
    • Feature words tell apart two unknown species that would be described the same way.
    • Color words 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.csv Id,Label,Diet,Size,Temperament,Tamable,Habitat. Habitat is terrain names separated by ;. Size (Small, Medium, Large, Huge) picks the body plans a species can have.
  • The Study job is in Jobs.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) gives NumAnimals().
  • colony.Species.List logs this playthrough's species. colony.Species.IdentifyAll reveals them all (dev).
  • Test hook: -ColonyDevTest -ColonySpeciesTest logs 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.

PieceFile
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 tablesScripts/gen_kit_data.py → Content/Data/PlantParts.csv, Content/Data/CreatureParts.csv
Import and the kit materialScripts/import_kits.py (Unreal Python) → /Game/Art/Plants/Kit, /Game/Art/Fauna/Kit, /Game/Colony/Kit/M_ColonyKit
Part lookupColonyKits.h/.cpp (FColonyKits)
Plant assemblyColonyPlantKit.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_ColonyKit turns it into color with the parameters BodyColor, PatternColor, GlowColor, GlowStrength and Roughness, so one material serves every species.

PlantParts.csv Name,Slot,Family,Mesh,Shapes,Notes.

  • Slot is Stalk, Crown, Accent, Base (trees), Column (blocking plants), Tangle (slowing undergrowth) or Small (everything else).
  • Shapes lists 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.

  • Slot is Torso, Leg, Head, Tail, Feature or Eye.
  • Plan is 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).
  • Sockets are attachment points in part units: Name=x;y;z|Name=x;y;z. A torso has one Leg socket per leg, plus Head, Tail and Back. A head has Eyes, Top and Front.
  • A mod adds parts with its own CSV rows and meshes. A matching Name replaces a part.

Animals in the world

PieceFile
The animal actor (assembly, wandering, fleeing, animation)ColonyAnimal.h/.cpp (AColonyAnimal)
Spawning, savingColonyFaunaSubsystem.h/.cpp (UColonyFaunaSubsystem)
  • Spawning. On a new game, once the world and its species exist, every Fauna.csv function gets a few herds on ground its Habitat lists, at least 20 m from the colony. Herd size goes by Size: 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 UColonyEntityComponent colony id from the shared registry (never reused), so actions, the HUD and saves can name it (AColonyAnimal::GetColonyId). Its kind is its Fauna.csv row (GetKind, like a colonist's xenotype); this playthrough's look and name come from FindFauna(GetKind()). A load restores each animal's id.
  • Behavior. Animals idle and wander on the voxel pathfinder. Skittish ones flee from colonists within 9 m, Wary ones within 6 m, and Docile ones 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 0 turns that off).
  • Perf probe: -ColonyDevTest -ColonyPortraitsAnimated -ColonyPerfProbe measures the toggled optimizations off and on in one run (portrait frame/GPU time, the animals' Tick time, a remove-plants brush) and logs PerfProbe: lines.
  • Not yet: hunting, taming, eating, breeding and dying.
  • Test hook: -ColonyDevTest -ColonyFloraShot takes 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"):

  • Plants saves 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.
  • Species saves 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".
  • Animals saves 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.

PieceFile
Geodesic grid, climate, biomes, district registry (plain C++)ColonyPlanet.h/.cpp (FColonyPlanet)
Planet state for the game, edge rule, view switchingColonyPlanetSubsystem.h/.cpp (UColonyPlanetSubsystem)
The globe and its camera, district picking, the selection circleColonyPlanetGlobe.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 Frequency times 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, FaceCells 288 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):

  1. Elevation: continents, detail and mountain ridges from layered 3D noise on the sphere. Sea level is set so that LandFraction of the tiles are land, and land rises to MaxElevation.
  2. Temperature: from latitude (between EquatorTemp and PolarTemp, mild in the mid-latitudes), minus LapseRate per km of height, plus TemperatureOffset and a little noise.
  3. Rainfall: wet equator and mid-latitudes, dry subtropics and poles. It's patchy, drier far from the sea, and times RainfallScale.
  4. Hilliness and swampiness: hilliness (Flat, SmallHills, LargeHills, Mountains) comes from how steep a tile is against its neighbors plus the ridges, scaled by Mountains. Swampiness is high on wet, flat lowland.
  5. Biome (rules): each Biomes.csv row lists conditions: temperature, rainfall, elevation, swampiness and hilliness ranges. Among the biomes a tile qualifies for, the highest Priority wins, with ties broken by a stable roll. Patch biomes (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 get Water biomes.
  6. Geography:
    • Lakes: connected water bodies of at most LakeMaxTiles districts. 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: RiverFlow gives a creek, 2.5x a river, 6x a large river, 16x a huge river. River is the size and RiverTo the next district downstream.
  7. 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 (times LandmarkDensity) 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:odds pairs 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.

Districts.

  • State: each district is Unexplored (dimmed on the globe), Known, Visited or Colony. 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: River size, bCoast, bLakeshore
    • land features: Features (ids), Landmark with LandmarkName, and Tags (every feature's tags together: Caves, Lake, Island, Ruins, Lava, ...). Features' PlantDensity, HeightScale and WaterTable effects are already folded into the values above. AColonyGameMode::StartGame applies these when a game starts. It calls NewPlanet(Seed) with the game seed, then sets:
    • AVoxelWorld::Seed to the district's seed
    • HeightScale (multiplies the ground-height noise)
    • WaterTableOffset to -3 plus the district's water table
    • SurfaceTerrainSwap to the biome's swaps (a target terrain missing from Terrains.csv keeps the original)
    • UPlantLayerComponent::WildDensityScale to 0.5 × the plant density The log line Colony: district tile N, biome B, ... shows what was used. -ColonyNoPlanet skips 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) with PP_ColonyOutline draws a glowing white edge around it, plus a faint wash over it. It's the technique of Brick and Bones' PP_Highlight as an edge. The pass sets r.CustomDepth 3 at run time.
  • Markers: ColonyGlow::MakeGlow() gives a white additive, pulsing M_ColonyGlow instance, 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.py builds the three in /Game/Colony/FX (run it as an editor commandlet: UnrealEditor-Cmd <project> -run=pythonscript -script=<path>).

Data files

  • Planet.csv Key,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.csv Id,Label,Color,Water,Settleable,MinTemp,MaxTemp,MinRain,MaxRain,MinElev,MaxElev,MinSwamp,MaxSwamp,MinHills,MaxHills,Priority,Patch,PlantDensity,WaterTable,TerrainSwap:
    • Color is written (R=..;G=..;B=..;A=1), with semicolons because the file is comma-separated.
    • Empty conditions mean "any".
    • TerrainSwap is From=To;From=To using Terrains.csv names. Mods add or override biomes by Id. 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.csv Id,Label,Landmark,Category,Biomes,NotBiomes,MinHills,MaxHills,MinTemp,MaxTemp,MinRain,MaxRain,Needs,Forbids,Chance,Secondary,Excludes,PlantDensity,HeightScale,WaterTable,Tags,Description:
    • Landmark is 1 for landmarks.
    • Biomes and NotBiomes are ; lists of biome ids (empty means any land biome).
    • Needs and Forbids take geography from Coast, Lakeshore, River, LargeRiver, Headwater, Confluence, Delta, Inland.
    • Secondary is Id:odds;..., and Excludes lists features it can't share a district with.
    • The effects multiply plant density and height and add to the water table. Tags are 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), DescribeTile
    • GetCurrentDistrict, SetCurrentDistrict, SetDistrictState, GetCurrentDistrictParams
    • CanModifyColumn
    • ShowPlanet, TogglePlanet, IsPlanetViewActive, SelectTile
    • OnDistrictSelected (delegate)
  • FColonyPlanet::FindTile(direction, start), Distance(A, B) (in tiles), GetTile(i). A tile has Neighbors, Corners, its climate, River, RiverTo, bCoast, bLake, Features, Landmark and LandmarkName.
  • FColonyPlanet::Features() and FindFeature(Id).
  • Console:
    • colony.Planet: toggles the view.
    • colony.Planet.New <seed>
    • colony.Planet.Info [tile]
  • Test hook: -ColonyDevTest -ColonyPlanetTest generates 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 optional mod.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), client or host.
  • A mod can have Data/.csv (the same files and columns as Content/Data) and Content/.pak (packaged assets).
  • Load order:
    1. The player's saved order.
    2. Then loadOrder and id.
    3. Then dependencies, which always load first.
  • A mod is active when it's on, its mod.json reads, and everything it requires is 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=). -ColonyNoMods starts with none, and -ColonyMods=a,b with only those.
  • Content: at game start each active mod's Content/*.pak is mounted and /<id>/ maps to it. Paks only mount in packaged builds; the editor logs and skips them.
  • Validation (FColonyModRegistry::Validate) checks mod.json and every Data/*.csv against 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 the ColonyModValidate commandlet (UColonyModValidateCommandlet: -run=ColonyModValidate [-Mod=<id>], exit code 1 on errors).
  • colony.Mods.List rescans Mods/ 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 a mod.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, at Plugins/Mods/<id>/.
  • PackageMod.ps1 -ModId <id> [-ReleaseVersion <v>]:
    1. Validates.
    2. For mods with assets, cooks and paks the plugin as DLC against a released base game (BuildCookRun -DLCName).
    3. 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:

PieceFileRole
ActionsColonyActions.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.
AuthorityColonyNet.hColonyNet::HasSimAuthority: everything that simulates checks it.
Entity idsUColonyEntityComponent, UColonyEntityRegistryHost-allocated ids that replicate and save, never 0, never reused.
Game stateAColonyGameStateReplicated: speed, calendar day, world size and seed, aging, the host's mod fingerprint, and mod key/values (SetModValue).
EventsColonyEvents.h (UColonyEvents)OnActionExecuted, OnWorldEdited, OnEntitySpawned / OnEntityRemoved, OnGameStarted, OnDayChanged, OnModMessage (mod-to-mod).
World editsColonyWorldEdits.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 savesColonySave.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 matchingColonyMods.hGetNetFingerprint (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:

SectionOwnerHolds
Colonists (v3)AColonyGameModeevery colony member (see "Colonists" above): id, place, identity (names, xenotype), look, age, health
ItemsUColonyItemSubsystemstacks 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)UColonySpeciesSubsystemthe 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)UColonyFaunaSubsystemeach 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)

SystemOwnerStatus
Core (everything above)modding sessionDone, built and tested
Controller tools, speed, possession, moves → actionsdev1Done (see section 7, "Actions, saves and the multiplayer seams")
Voxel, collapse, plant and rock-mining edits recorded, plus appliersdev1Done. Gap: a rock that falls in a collapse moves its instance, and replay only moves the voxels
GameMode uses AColonyGameState, loads snapshots (-ColonyLoad=<slot>)dev1Done; the -ColonySaveTest round trip passes
Colonist entity id, seeded randomness, Colonists save sectiondev1Done
Taskmaster, Social, Species, Planet: host-only ticking, actions, save sectionsSystems DevPending
Seeded randomness in Taskmaster and SpeciesSystems DevPending

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.csv and health per part exist, section 6; see Docs/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:

PieceFile
The look's post-process volume: ink, grade, console settings, testColonyArcane.h/.cpp (UColonyArcaneSubsystem)
The character outline material /Game/Colony/FX/PP_ColonyInkScripts/build_ink_material.py
Inking meshes (custom stencil 10), handing over to selectionColonyGlow::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 by UColonyArcaneSubsystem::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.Width pixels at 1080p (5). The line is full width at RefDistance (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/1 and colony.Ink.Width.
  • colony.Look.Grade 0/1.
  • colony.Arcane 1 turns the old full-screen filter back on for comparison: Kuwahara strokes, edge ink, value bands and grain, tuned with colony.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 by Scripts/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 ShadowLift glow keep shadows warm rather than black.
  • Instances: colonists get runtime instances (dev1's AColonistCharacter):
    • head slots use MI_LyridiaSkin_Face, which has the static switch IsFace on;
    • body slots and the anatomy wearable use MI_LyridiaSkin_Body.
  • 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);
    • SubsurfaceColor and Subsurface (0.8);
    • Roughness (0.7) and Specular (0.35);
    • RimColor, RimStrength (0.12) and RimExponent (3.5);
    • ShadowLift (0.1);
    • NormalMap and NormalStrength (0 = flat);
    • AOMap (red channel) and AOStrength (0).
    • Face only: DetailMask (R = lips, G = blush), LipColor/LipStrength and BlushColor/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 at NeckHideStart (0.35) when fully on and ramps over NeckHideSoftness (0.15), dithered. MI_LyridiaSkin_Face is 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.

PieceFile
Item, material and quality definitions; stacks and their value, mass, hit points and statsColonyItems.h/.cpp
Stacks on the ground, their look, saving, console commands, testColonyItemWorld.h/.cpp (UColonyItemSubsystem, AColonyItemActor)
The Cut job: felling plants drops their yieldColonyTaskmasterSubsystem (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.csv row gives value and mass). Its Materials.csv row 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 QualityAllowed get 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.
    • RollQuality gives 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:

MaterialValue (normal / masterwork)Hit pointsSharp damage (normal / masterwork)
Wooden160 / 4006510.6 / 15.3
Steel230 / 57510020.7 / 30.0
Plasteel940 / 235028025.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 to StackLimit and spills the rest to the nearest free columns (dry ground, no trunk or rock, within 4).
  • Taking: Take(Column, Count). GetStackAt, GetStacks and CountOnGround read.
  • The look: the row's Mesh fitted so its longest side is SizeCm, a little bigger for big stacks. With no mesh, a box tinted by the material (or the item's Color), 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 Items save 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.csv gives PerMeter × the plant's height, clamped to Min..Max, matched by the plant's own name, else its category. Trees give WoodLog (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)", from Describe, GetMarketValue and GetMass). 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. -ColonyZoneTest scripts a stockpile drag and a remove drag with UI screenshots. The selected colonist's readout lists every need from Needs.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 (DecayPerDay 1.2). Below SeekBelow (0.3) a colonist goes to eat, and below UrgentBelow (0.12) it drops its work for it.
  • Choosing food: the nearest food on the ground (ColonyItems::IsEdible: has Nutrition and isn't AnimalFeed), 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 (ShowCarried holds 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 by Id. 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,Description
    • ItemClass: 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 use Damage, Range, Cooldown and Burst, or SharpDamage and BluntDamage. Apparel uses ArmorSharp, ArmorBlunt, ArmorHeat and Insulation. Medicine uses Potency.
    • 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) or Lyridia for our own rows.
  • Materials.csv Id,Label,Categories,Color,HitPointsFactor,BeautyFactor,WorkFactor,SharpArmor,BluntArmor,HeatArmor,SharpDamage,BluntDamage,Flammability. Id must be an item. There are 41 rows: wood, 7 metals, 5 stone blocks, 9 fabrics and wools, 19 leathers.
  • Qualities.csv Id,Label,ValueFactor,StatFactor,BeautyFactor: overrides the built-in RimWorld factors.
  • PlantYields.csv Plant,Item,PerMeter,Min,Max: Plant is 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), then Drop, Take, GetStackAt, GetStacks, CountOnGround.
  • UColonyTaskmasterSubsystem::DesignateCut(X, Y). When a plant is cut, the colony event PlantCut fires.
  • 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 -ColonyItemTest does 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.