Config File Format
Buildings, figures, UI windows and most tunable game data are described in script files under src/scripts/. They are JavaScript run by the embedded MuJS engine, with a few syntax extensions that make them read like a config language. This page explains how those files are written and how the C++ side picks them up.
Where Configs Live
| Path | Contents |
|---|---|
src/scripts/building/*.js | One file per building or building family: static params, ghost preview, spawn logic. |
src/scripts/figure/*.js | One file per figure type: animations, sounds, phrases. |
src/scripts/ui_*.js | Window layouts (ui { … } trees) and their handlers. |
src/scripts/mission/*.js | Mission scripts. |
src/scripts/game.js | The game_features list and other game-wide objects. |
The scripts are embedded into the executable at build time (each becomes a generated *.js.cpp), so an edit needs a rebuild. Two command-line options help during development:
--mixed PATHloads scripts from disk and hot-reloads them when they change.--unpack_scriptsextracts the embedded scripts to the user directory, so you can read and edit the exact copy the build carries.
How C++ Reads a Config
Every building and figure class is registered with a metainfo macro. Its class name is also the name of the script object that configures it:
// src/building/building_mortuary.h
class building_mortuary : public building_impl {
public:
BUILDING_METAINFO(BUILDING_MORTUARY, building_mortuary, building_impl)
...
};
// src/building/building_mortuary.cpp
REPLICATE_STATIC_PARAMS_FROM_CONFIG(building_mortuary);
At startup, REPLICATE_STATIC_PARAMS_FROM_CONFIG makes the model loader (buildings::model_t::static_params_load in building_model.h) look up the global script object named building_mortuary and read it twice:
- into the shared
building_static_params(cost, size, labor, animations, flags…), then - into the class's own
static_paramsstruct, if the class declares extra fields.
If the object is missing, the loader logs Failed to load building static params TYPE=… CLSID=… and stops. Figures work the same way through FIGURE_METAINFO and figures::model_t (figure_impl.h).
Which fields C++ reads is decided by ANK_CONFIG_STRUCT(type, field1, field2, …). Only listed names are read, matched by exact spelling. ANK_CONFIG_PROPERTY exposes the same fields to scripts for reading and writing. Both macros must be used at global scope.
Syntax Extensions
The MuJS fork (src/mujs/jsparse.cpp) accepts a few forms that plain JavaScript does not. They all produce ordinary objects and arrays.
| Written | Means |
|---|---|
building_x { … } | Top-level statement: building_x = { … }. Both forms are used in the codebase. |
flags { … } inside an object | flags: { … }, a nested object without the colon. |
cost [ 20, 30 ], laborers[8] | cost: [20, 30], an array without the colon. |
Newline or ; between properties | Same as a comma. Commas are optional when each property is on its own line. |
@name { } inside an object | A computed property. An empty body routes reads to the object's __property_getter(name) (and writes to __property_setter); @name { get: f, set: g } names the accessors directly. |
[es=building] before an object | Tags the object with an event family: events for it fall back to [es=(building, <event>)] handlers. |
[es=(a, b)] before a function | Registers the function as the handler of event b on object a. Several tuples can be listed, comma-separated. |
emit event_name{ … } | Statement that raises a script event. |
Annotated Building
The mortuary (src/scripts/building/mortuary.js), shortened:
building_mortuary {
animations {
preview { pack:PACK_GENERAL, id:175 }, // image shown while placing
base { pack:PACK_GENERAL, id:175 }, // idle image
work { pos[35, -20], pack:PACK_GENERAL, id:175, // working animation, offset from the base
offset:1, max_frames:12, duration:4 },
}
input : { resource : RESOURCE_LINEN } // what cart pushers deliver
walker_cost : [5, 10, 20, 40, 60] // read by the script only (see below)
spawn_delays : [3, 7, 15, 29, 44]
linen_required_for_animation : 100 // building_mortuary::static_params
min_houses_coverage : 50
building_size : 2
labor_category : LABOR_CATEGORY_WATER_HEALTH
meta { text_id:82, help_link:"message_building_mortuary" }
info_sound : "Wavs/EMBALM.WAV"
cost [ 20, 30, 50, 100, 200 ] // one value per difficulty
desirability { value[-3], step[2], step_size[1], range[2] }
laborers[8]
fire_risk[4]
damage_risk[2]
flags {
work_anim: true
}
}
[es=(building_mortuary, spawn_figure)] // C++ spawn_figure() raises this event
function building_mortuary_spawn_figure(ev) {
building_spawn_stock_walker(city.get_building(ev.bid), building_mortuary,
FIGURE_EMBALMER, RESOURCE_LINEN, building_mortuary_walker_cost())
}
Constants such as PACK_GENERAL, RESOURCE_LINEN or LABOR_CATEGORY_WATER_HEALTH are C++ enum values registered as script globals.
Difficulty Arrays
Fields of the *_dcy types (cost, laborers, fire_risk, damage_risk, production_rate_dcy) and every member of desirability and crime are arrays indexed by difficulty:
| Index | 0 | 1 | 2 | 3 | 4 |
|---|---|---|---|---|---|
| Difficulty | Very Easy | Easy | Normal | Hard | Very Hard |
A shorter array repeats its last value for higher difficulties, so laborers[8] means 8 on every difficulty (value_dcy::get in game/difficulty.h). Scripts read their own per-difficulty fields with dcy_get(array) from common.js, which applies the same rule.
Images and Animations
An image reference is either an object or a string:
{ pack: PACK_GENERAL, id: 61, offset: 4 } // image group 61 of the pack, 5th image
"path/to/image" // a named image path
An animation (animation_t) is an image reference plus playback fields. A building or figure keeps them in animations { key { … } } and C++ looks them up by key (first_img("ground"), anim(animkeys().work)).
| Field | Meaning |
|---|---|
pack, id, offset, path | Where the first frame is. |
pos | Pixel offset [x, y] from the building's base image. |
max_frames | Number of frames. |
duration | Ticks per frame. |
start_frame, can_reverse, reverse | Playback control. |
An _pack { pack: … } entry inside animations sets the default pack for the other entries (see fort.js). Buildings can also declare overlay_anims, animations tied to a stored resource, with the extra fields resource, stack, step, max_count and default_active.
Building Fields
Read for every building (ANK_CONFIG_STRUCT(building_static_params, …) in building_static_params.h):
| Field | Type | Meaning |
|---|---|---|
building_size | int | Footprint edge in tiles. |
cost | difficulty array | Construction cost. |
laborers | difficulty array | Workers needed. |
labor_category | enum | Labor advisor category. |
fire_risk, damage_risk | difficulty array | Risk increments. |
fire_proof, damage_proof | bool | Immune to fire or collapse. |
desirability | { value, step, step_size, range } | Effect on nearby tiles; each member is a difficulty array. |
crime | { value, step, step_size, range } | Effect on the crime map. |
input, output | { resource, resource_second } | Consumed and produced resources. |
production_rate, production_rate_dcy, output_resource_second_rate | int / array | Industry output. |
min_houses_coverage | int | Houses covered below which a labor seeker is sent. |
max_service, max_serve_clients, max_storage_amount, max_gatherers, max_walkers | int | Per-type limits; their meaning is up to the building class. |
progress_max | int | Production progress needed for one unit. |
animations, overlay_anims | objects | See Images and animations. |
overlay | enum | Overlay the building belongs to. |
sound_channel, info_sound | enum / string | City ambience and info-window sound. |
meta | { text_id, help_link } | Text group for names and the help page. |
meta_id, info_title_id, build_menu_text | string | Text keys. |
flags | object of bools | Classification and behaviour switches, listed below. |
needs | object of bools | Placement requirements, listed below. |
planner_update_rule | object | Planner behaviour, listed below. |
flags (building_flags_rule): classification flags is_monument, is_extractor, is_harvester, is_farm, is_fort, is_education, is_palace, is_temple, is_shrine, is_tax_collector, is_statue, is_administration, is_water_crossing, is_infrastructure, is_beautification, is_guild, is_industry, is_workshop, is_house, is_wall, is_defense, is_temple_complex, is_religion, is_military, is_entertainment, is_food; behaviour flags allow_rotate, no_road_access, non_deletable, keeps_visitor_paths, perimeter_access, draw_normal_anim, work_anim.
needs (building_planner_need_rule): meadow, rock, ore, altar, oracle, nearby_water, groundwater, shoreline, canals, floodplain_shoreline, water_access.
planner_update_rule (building_planner_update_rule): canals, roads, ferries, is_draggable, relative_orientation, unique_building.
Type-Specific Fields
A class that needs more parameters declares its own static_params and lists the new fields:
// src/building/building_mortuary.h
struct static_params : public building_static_params {
uint16_t linen_required_for_animation;
uint16_t monthly_linen_consumption;
} BUILDING_STATIC_DATA_T;
...
ANK_CONFIG_STRUCT(building_mortuary::static_params,
linen_required_for_animation, monthly_linen_consumption)
C++ reads them with current_params().linen_required_for_animation.
Fields that no ANK_CONFIG_STRUCT lists are still part of the script object, so script code can use them: the mortuary's walker_cost and spawn_delays are read only by its spawn handler, through building_mortuary.walker_cost. That is the usual place for values that only script logic needs.
Figure Configs
A figure config follows the same rules. The embalmer (src/scripts/figure/embalmer.js), shortened:
figure_embalmer {
overlay : OVERLAY_MORTUARY
animations {
walk { pack:PACK_SPR_MAIN, id:195, max_frames:12 }
death { pack:PACK_SPR_MAIN, id:196, max_frames:8, loop:false }
big_image { pack:PACK_UNLOADED, id:25, offset:FIGURE_EMBALMER }
}
sounds {
embalmer_city_is_ok { sound:"embalmer_g09.WAV", text: "#embalmer_city_is_ok" }
...
}
category: figure_category_citizen
max_damage : 10
terrain_usage : TERRAIN_USAGE_ROADS
max_roam_length : 384
permission : epermission_medicine
}
Fields read for every figure (figure_static_params.h): terrain_usage, animations, sounds, max_roam_length, speed_mult, meta, permission, is_enemy, is_soldier, use_cart, category, max_damage, attack_value, defense_value, missile_defense_value, missile_attack_value, missile_delay, missile_range, corpse_time_delay, render_on_flat_tiles, record_path, can_cross_ferry, boards_ferry, overlay.
Each sounds entry is a phrase key. Script code picks a key in the figure's setup_phrase handler; the key gives both the speech file and the text shown in the info window.
Shared Objects and Globals
A config may point at another script object instead of repeating it. The three forts share one ghost description:
base_fort_ghost {
main_view_offset [[-55, 20], [-55, -35], [-55, -35], [-60, -40]]
...
}
building_fort_charioteers {
ghost : base_fort_ghost
...
}
Plain C++ globals can be configured too. std::array<vec2i, 8> ANK_VARIABLE(cartpusher_sled_offsets); fills the C++ variable from the script global of the same name (cartpusher_sled_offsets = [ … ] in images.js). ANK_VARIABLE_N takes a different script name, and ANK_ARRAY_VARIABLE reads an array of objects.
Event Handlers
Behaviour lives next to the data. C++ raises named events on the config object, and a function tagged [es=(object, event)] handles them. Common building events:
| Event | Raised when |
|---|---|
spawn_figure | The building may send out a walker (once per game day). |
update_animation, update_graphic | The building picks what to draw. |
add_resource | A cart pusher delivers goods. |
on_place_checks | The building has been placed; show advisor warnings. |
ghost_preview, ghost_blocked | The planner draws the placement ghost. |
ghost_allow_tile | The planner asks whether one footprint tile is usable (set city_planner.preview_allow_result). |
can_place, finalize_check | The planner decides whether the site is valid, every frame and on the final click (set city_planner.finalize_check_result). |
setup_building_variant, next_building_variant, update_building_variant, update_relative_orientation | Variant and orientation handling while placing. |
One function can serve several objects: [es=(building_fort_charioteers, can_place), es=(building_fort_infantry, can_place)]. When no handler is registered, C++ keeps its default behaviour.
Feature Flags
Behaviour that differs from the original game sits behind a feature flag that is off by default. A flag is declared in three places:
// src/game/game_config.h
extern game_feature gameplay_change_fort_rotation;
// src/game/game_config.cpp
game_feature gameplay_change_fort_rotation{ "gameplay_change_fort_rotation", "#TR_CONFIG_FORT_ROTATION", false, false };
// src/scripts/game.js, inside game_features { … }
@gameplay_change_fort_rotation {}
The #TR_CONFIG_… key is the settings label; add it to localization_en.js and the other localization files you maintain. C++ checks the flag with game_features::gameplay_change_fort_rotation.to_bool(), scripts with game_features.gameplay_change_fort_rotation.
Pitfalls
- A misspelt field is not an error. C++ reads only the names listed in
ANK_CONFIG_STRUCT; any other name silently stays a script-only field, and the C++ value keeps its default. - A new C++ field must be listed. Adding a member to
static_paramswithout adding it toANK_CONFIG_STRUCTleaves it zero. - The object name must match the class name. Renaming the class without renaming the script object stops the game at startup with
Failed to load building static params. - Script edits need a rebuild unless the game runs with
--mixed. Integral tests intests/*.jsare the exception: they load from disk. - A broken handler name fails silently. A typo in
[es=(object, event)]registers a handler nobody calls.