NOTE: Non-English localisation files are not currently mirrored against English — full translation is deferred to a later translation project. Do not modify them, and do not flag non-English .yml files in reviews, audits, or branch checks for missing, stale, or diverging keys relative to English. They are expected to be out of sync; any absent key degrades gracefully to the English string or an empty value. Only English keys (and the script objects that reference them) are in scope for review.
Millennium Dawn is a Hearts of Iron IV mod (2000-present). Key directories: common/ (game data), events/, localisation/ (English .yml, UTF-8 BOM), history/, interface/, gfx/, tools/ (Python dev scripts).
IMPORTANT: The resources/ directory is for reference material only. Do NOT modify files under resources/ unless explicitly asked by the user. It holds the vanilla docs the validators read plus the unsorted art dumps (resources/README.md); everything else retired from the mod lives in the millennium-dawn-resources repo.
Validation runs on GitHub CI at PR time — don't run proactively. Standardization tools: tools/standardization/ (see its README). Diff summary: python3 tools/analysis/review_branch.py [base-branch].
Never run pre-commit run --all-files. The auto-fixers rewrite every matching file in the repo and leave hundreds of unrelated whitespace-only modifications in the worktree. Always scope runs to actually-modified files (pre-commit run --files <path1> <path2>) or rely on the normal git commit flow, which only feeds staged files to the hooks. If the branch already carries whitespace noise from a prior --all-files run, revert anything outside the task's scope before committing.
Pre-commit and CI run different hook sets — passing locally does not guarantee passing CI, and vice versa. Before wiring, judging, or debugging any validator, read .claude/docs/validation-pipeline.md (CI-only validators, pre-commit-only fixers, strictness divergences, vanilla-manifest regeneration, deprecation watch).
The validator test suite must stay green permanently. CI runs python -m pytest on every PR that touches tools/ (testpaths in pyproject.toml is tools/tests). Do not introduce regressions into the testing schema. When a validator behavior change breaks a regression test, fix it in the same change: update the affected *_test.py to match the new correct behavior, or fix the validator if the test is right. Never delete or weaken a regression test to hide a failure — the suite is a gate, not a suggestion. Before merging any tools/ change, run python -m pytest and confirm zero failures.
- Tabs for indentation;
{on same line,}on own line at outer indent; 1 blank line between elements - Simple checks on one line:
available = { has_country_flag = some_flag } - Comments are small, targeted, and load-bearing — comment policy:
.claude/rules/general-rules.md(Python tooling:tools/COMMENT_STYLE.md) - Remove unused/commented-out code
* 0.01not/ 100;if/elsenot twoifwith complementary conditions- Prefix country-specific variables with tag;
snake_case - Flag naming:
TAG_single-nation,GLOBAL_global, bare domain prefix for any-nation - Flag/var casing:
<TAG/GLOBAL/SYSTEMACRONYM>_name_of_entity,snake_case - Do not add flags that duplicate authoritative state. Use
has_idea,has_completed_focus, variables, event targets, ideology, subject status, faction membership, and similar direct checks instead. Use a flag only for state that cannot be queried directly or must record a historical transition. - Markdown docs: tables must read aligned in plaintext — prettier-padded columns
(the repo
.mdhook settings) with the whole padded row within 100 characters. Tighten cells, factor a shared path prefix into a note above the table, drop or merge columns, or move detail into a terseDetails:list under the table (patterns:.claude/docs/documentation-references.md,.claude/docs/formable-reference.md). Content that cannot fit a 100-wide table becomes a bulleted list instead.
Every text-mode write in tools/ must pass newline="". Without it, Python's text mode turns each \n into \r\n on Windows, so a tool that rewrites a mod file hands back CRLF; git add normalises the index but the working tree stays CRLF, and the next commit touching that file gets bounced by the mixed-line-ending hook. Path.write_text is banned outright (its newline parameter only exists on 3.10+) — use an explicit open(..., newline=""). Writes to .txt use encoding="utf-8", never utf-8-sig, which would inject a BOM. tools/tests/text_write_newline_test.py enforces both and carries a documented allowlist for the rare write that genuinely needs platform-native endings. Repo-wide, .gitattributes (* text=auto eol=lf) and .editorconfig keep everything else on LF.
- Always
is_triggered_only = yes; useon_daily_TAGnot global triggers - Replace
every_country/random_countrywith array triggers - Use dynamic modifiers sparingly; avoid
force_update_dynamic_modifier
- ID:
TAG_focus_name; userelative_position_id - Always: logging,
ai_will_do = { base = N },search_filters(two-layer pattern, see.claude/docs/search-filters.md) - Omit defaults:
cancel_if_invalid = yes,continue_if_invalid = no,available_if_capitulated = no - No empty
mutually_exclusive/availableblocks; limit permanent effects to 5 - Never
available = { always = no }with abypass— use matching condition - Money-spending focuses need bankruptcy guard in
ai_will_do— see.claude/docs/focus-tree-reference.md - Ref:
.claude/docs/focus-tree-reference.md
- Logging:
log = "[GetDateText]: [Root.GetName]: Decision DECISION_ID"as the first statement of every effect block the engine runs (complete_effect,remove_effect,timeout_effect,cancel_effect). A log nested inside anif/hidden_effectrecords which branch ran and stays there ai_will_do = { base = N }—basenotfactorat root- Don't repeat category
allowedin decisions — put nation gate on category, dynamic checks inavailable/visible - AI-only decisions get no localisation and no tooltip wrappers. A decision is AI-only when an unconditional
is_ai = yessits at the top level of itsvisible/available/allowed, or its category is gated that way — no human sees it, so a loc key is dead weight and is flagged. An AI-only category's own key is flagged the same way. Writecheck_variablebare in such anavailableblock:custom_trigger_tooltip/custom_effect_tooltiprender to nobody, and theavailable-block tooltip checks skip AI-only decisions - A category that becomes visible mid-game (flag, completed focus, idea, variable) should get
unlock_decision_category_tooltip = <category>in whatever turns it on, orunlock_decision_tooltipon one of its decisions. Otherwise a whole tab appears with no indication of where it came from. Always-on and tag-gated categories need nothing. Audit withvalidate_decisions.py --unannounced-categories(opt-in, not in CI — 118 existing cases) - An effect that sets a flag another decision's
visible/availablewaits on has unlocked it. If the block already callsunlock_decision_tooltipfor some, it must call it for all of them (unannounced-decision-unlock). Gates only count at depth 0 — inside aNOTthe flag hides rather than unlocks - Ref:
.claude/docs/decision-reference.md
- Always
is_triggered_only = yes; log only if option has effects (validate_events→event-option-log-without-effect);major = yesfor news only. Never wrap amajor = yesevent inevery_country/every_other_country(one fire already broadcasts) - Date-based events: owner-guard pattern in
common/scripted_effects/00_yearly_effects.txt add_building_constructionfornaval_baserequiresprovince = XXXXX- New subideology parties: register in
common/scripted_localisation/00_MD_politicsview_scripted_localisation.txt - Pure notifications get
minor_flavor = yes. When many sources deliver to one country, batch them into a single report event instead of one event per delivery, and keep the payload at the delivery site (rules and traps:.claude/docs/event-reference.md) - Describe an effect with
effect_tooltip = { <the real effect> }before writing a newcustom_effect_tooltiploc key - Every
picture = GFX_*must resolve to a sprite defined ininterface/*.gfx— MD must not use vanilla event pictures. An undefined name is a commit blocker (validate_events→missing-event-picture), so grepinterface/for it before writing it - Match the picture to the window: news art is wide (
397x153), country art nearly square (217x163), and each window draws it at native size, so a swap overflows or under-fills the frame. Names do not tell them apart (GFX_china_trade_waris news art) — check the texture (event-picture-format-mismatch). Ahidden = yesevent renders nothing, so it takes nopicture(hidden-event-picture) - Ref:
.claude/docs/event-reference.md
- Always
picture = sprite_name(no picture = blank icon);original_tagnottaginallowedblocks; noavailableincountry/hidden_ideas - Category-specific
allowed/available-block scoping and removable defaults (cancel,on_add,allowed_civil_war):.claude/docs/idea-reference.md
- ID:
TAG_organization_name; alwaysallowed = { original_tag = TAG }; sizing, trait grid, andinitial_traitrules:.claude/docs/mio-reference.md
New upgrades require wiring across five files — read common/intelligence_agency_upgrades/README.md before touching them.
Unit production has three layers — threat gate (ai_is_threatened), role ratios, templates: .claude/docs/ai-strategy-reference.md. Equipment variants (role coverage, target_variant, CV-plane ai_types, penalty cascades): .claude/docs/ai-equipment-reference.md. Both dirs have pre-commit-validated naming (role_ratio ↔ ai_templates roles, case-sensitive unit names, nation coverage) — read the doc before editing common/ai_strategy/, common/ai_equipment/, or common/ai_templates/.
Never reset the working directory. No cd to another repo, drive, or temp path — the cwd is fixed for the session, and relative paths, follow-up edits, and tool snapshots assume it. Use absolute paths or per-command flags (git -C <dir>, grep <path>, pre-commit run --files <path>).
- Do NOT add
Co-Authored-Byor sign commits — the project does not use commit signing - Do NOT write
Changelog.txtentries unless explicitly asked. A system new in 2.0.0 never needs an entry for its own changes - Dev builds may invalidate saves — no legacy migration needed
Keep all output token-efficient: conversation replies, agent hand-back reports, PR/issue/Changelog text, and commit messages alike.
- Lead with the conclusion (the answer, what changed, what was found). Cut preamble and restating the request.
- Report facts, not process. Skip "I read X, then I...", tool-by-tool narration, and self-congratulation.
- No padding confirmations ("As requested, I have successfully..."). State the result plainly.
- Prefer terse bullets and
file:linereferences over prose paragraphs. Drop empty sections rather than writing "N/A". - Be complete, not verbose: never drop a real finding, caveat, path, or identifier to save space. Trim words, not information.
- HOI4 Scripting | Documentation Index (complete doc catalog)
- Focus Trees | Events | Decisions
- Ideas | MIOs | Search Filters
- AI Strategy | AI Equipment
- OOB & Equipment Variants | Namelists
- Diplomatic Actions | Content Guidelines
- UN System (read before editing UN voting, elections, or recognition, or adding a Security Council / General Assembly resolution type)
- Formables (read before editing
formable_nation_decisions.txt, the EU end-states/EFS, UAR, or any union cosmetic — every formation path, the AI commitment ratchet, and the special-formable sentinel) - Faction Rules | Typo Watchlist
- Localisation Rules (read when editing any
*_l_english.yml) - Scripted GUI Rules + Patterns (read when editing
interface/*.guiorcommon/scripted_guis/) - MD Custom Modifiers — non-vanilla modifier keys in
common/modifier_definitions/ - Loading Screens (read before touching
gfx/loadingscreens/or the menu background picker)