Architecture and edit rules for the United Nations voting, membership, and election systems. Read this before touching any of the files below; the system is a distributed state machine and most of its historical bugs came from editing one piece without honoring the invariants.
What each file owns:
common/scripted_effects/01_international_systems_effects.txt— vote queues, finishers, UNSC elections, aborts, sanctions, recognition helpersevents/UN_Voting_Events.txt— generic vote events (UN.6, UNSC.1), result event (UN.410), sway chains (UN/UNSC.200-204)events/International Recognition.txt— type-5 membership chain (recognition.10-15, 71-73)common/decisions/un_voting_decisions.txt— finisher missions (UN_GA/SC_voting_mission + result timers)common/scripted_triggers/01_international_triggers.txt—un_ga_2_3_required,has_passed_sc_vote, auto-vote triggerscommon/scripted_guis/00_missiles_scripted_guis.txt— UN windows: propose, tallies, sway, queue reorder, budgetcommon/on_actions/MD_on_actions.txt— weekly queue pulse, monthly AI actors, June election start, January rotation
- Anything wanting a vote sets
sc_new_vote_type/sc_new_vote_nation(orga_*) temp vars and callsupdate_sc_vote/update_ga_vote. Busy or cooling down means the pair is appended to the parallel queue arrays (*_vote_type_tracker/*_vote_nation_tracker); GA types 6, 7, and 22 are front-loaded. un_check_for_next_queued_vote(weekly pulse, ~4 days) pops one SC and one GA vote when free. Type 5 routes to the recognition chain (recognition.10 for SC, recognition.13 for GA); everything else fires UN.100/UNSC.100 to the vote subject.- The start event picks ONE random eligible member, gives it
has_ga_mission/has_sc_missionplus the finisher mission, then fires the generic vote event (UN.6/UNSC.1) to every eligible non-auto-vote member. For Security Council votes, the subject is not eligible. Voters land in the*_vote_yes/no/abstainarrays. - The mission holder's complete/timeout runs the finisher (
general_assembly_vote_finished/security_council_vote_finished), which applies the outcome, clears arrays and globals, clears the ongoing flag, and sets the cooldown.
Key state: global.current_ga_vote_type/nation, current_ongoing_ga_vote, un_ga_vote_cooldown (50d GA / 20d SC), global.un_2_3_number (GA yes votes required, ceil of 2n/3), and global.un_sc_required_yes (SC yes votes required, ceil of 60% of eligible Council voters). Both are recomputed once at game start (recompute_un_2_3_number / recompute_un_sc_required_yes in the ABK startup block) and again whenever their chamber starts a vote.
- Exactly one mission holder per live vote. The finisher runs only from that mission, guarded by
has_ga_mission/has_sc_mission; the else branch is a tooltip no-op by design. - The Security Council vote subject is recused.
is_eligible_current_sc_voterexcludes nonexistent countries andglobal.current_sc_vote_nationfrom mission-holder selection, manual vote events, and automatic voting in both UNSC.100 and recognition.11.recompute_un_sc_required_yescounts the same eligible set, so a seated subject neither votes nor raises the passage threshold. The subject still receives the result notification. current_ongoing_*_voteis cleared ONLY by the finisher orun_abort_ga_vote/un_abort_sc_vote. Any new path that kills a vote must go through the abort effects; they also disarm surviving mission holders, clear sway lockouts, and bump the GUI before dropping the flag.- The queue pairs are parallel arrays. Never remove entries in place; rebuild via the scratch-array effects
un_purge_queued_ga_votes_for_this/un_purge_queued_sc_votes_for_this, which drop every queued vote about THIS whenmd_purge_vote_typeis 0 and only that one type otherwise.un_remove_dying_nation_queued_votes(all types) andremove_stale_ga_membership_votes_for_this(type 5) are the named wrappers. - Vote events (UN.6/UNSC.1) fire with no delay and may read the current-type globals in triggered titles and ai_chance. The result event (UN.410) fires
days = 1after the finisher cleared those globals, so it reads per-recipientga_resolution_vote_type/ga_resolution_proposerset byfire_ga_resolution_result_event, which skips recipients with an unanswered window. Clear both vars in every option. - General Assembly membership effects and non-voting iteration use
un_general_assembly_members_collection, derived fromunited_nations_member.global.UN_general_assemblyremains the voting/random-selection registry and dead-member backstop; keep both updated on admission and removal. - Annexation cleanup lives in
clear_united_nations_member_state(mission reassignment, vote abort, queue purge, array pruning,sc_action_against@THIS). Extend it, not the call sites. It runs fromon_annex; as a backstop, the weekly pulse (un_check_for_next_queued_vote) sweeps the GA and Council for dead entries and runs the cleanup on any it finds (P5 arrays handled inline, sincep5_member'son_removecannot fire on a dead country), catching annexation paths that skipon_annexand recovering a vote stalled by a dead mission holder. - Sway effects (
un_sway_direction_effect/sc_sway_direction_effect) are guarded on the ongoing-vote flags so pending sway events accepted after teardown cannot write into cleared tallies. - The same guard wraps every vote-option array write (and the P5 veto set) in UN.6/UNSC.1/recognition.12/recognition.15: a human mission holder can complete the finisher early, and late answers must not pollute the next vote's tally or leak a stale veto.
global.security_council_membersis positional: elected seats at 0-9, permanent members from 10. Never remove an elected entry by value;clear_united_nations_member_staterefills a dead elected member's slot with a qualified GA member so the January rotation stays aligned. Appended permanent entries may be removed by value.- A veto is a flag, not a tally.
has_passed_sc_voteis "nosecurity_council_vetoAND at leastglobal.un_sc_required_yesyes votes". The threshold is ceil(60% of eligible Council voters), preserving 9 required votes for a standard 15-member Council whether or not one seated member is the subject. The manual and automatic voting paths set the flag for an eligible permanent member's "no"; a permanent-member subject is recused and cannot veto.sc_sway_direction_effectrecomputes the flag from all current permanent no-voters after moving a vote. Never infer it from the total no tally. - Aborts must clear
sc_action_against@<subject>before wipingglobal.current_sc_vote_nation. That flag is what stops a nation being the subject of two proposals at once; leak it and the nation is immune to every future Security Council proposal for the rest of the campaign. The finisher already clears it;un_abort_sc_votemust too, and ordering matters because the abort clears the subject variable a few lines later. - The Assembly and the Council have separate sway lockouts (
has_had_current_un_ga_vote_swayed/has_had_current_un_sc_vote_swayed). They shared one flag, so a GA vote concluding during a live SC vote lifted the Council's lockout and let a member be swayed twice. Do not re-merge them. - Known narrow hole:
update_security_council_peeps(January rotation) rebuildsglobal.security_council_memberswith no ongoing-vote guard, so a mission holder rotated off mid-vote keepshas_sc_missionandun_abort_sc_vote's disarm sweep cannot find it. Harmless today only because the 20-day cooldown outlasts the 5-day mission window. Shorten that cooldown and this becomes a live bug. - Election candidate pools are never pruned. Candidates come from the power-ranking regional arrays (
asian_nations,sub_saharan_nations,middle_eastern_nations,american_nations) plussc_updates_euro_america, all built once at startup — an annexed or capitulated country stays in them forever. Every candidate pick (start_unsc_next_term_voting,un_sc_new_members_re_roll), the seat confirmation inun_sc_new_members_step_next, the January seating prune inupdate_security_council_peeps, and the dead-seat backfill inclear_united_nations_member_statemust keep theirexists = yes/has_capitulated = noguards (#2428, #2429). - Mission-holder picks must guard voter eligibility (
is_eligible_current_sc_voterfor UNSC.100 and recognition.11;exists = yesfor UN.100, recognition.14, and the reassignment sweeps inclear_united_nations_member_state). A dead holder never completes the finisher mission, socurrent_ongoing_*_voteis never cleared and all voting stalls for the rest of the campaign. - Permanent-seat applications are serialized by
unsc_permanent_seat_application_activeandglobal.unsc_permanent_seat_applicant. The lock begins when SC type 8 is submitted and is released only by rejection, canonical promotion, annexation cleanup, or the weekly stale-state watchdog. Never start a second formal application while it is set. - GA type 22 approval does not add
p5_memberdirectly. If an unrelated SC vote is live, promotion waits until that vote finishes or aborts.unsc_promote_permanent_memberis the only non-startup ascension seam: it removes elected-seat candidacy/confirmation and queued type-6 votes, backfills an occupied elected slot 0-9, then grantsp5_member. - Rejection in either chamber applies
unsc_permanent_seat_retry_cooldownfor 365 days and releases the formal lock. Lobbying records (recognition_campaign_UNSC@<applicant>) persist, so a retry only needs to lobby permanent members admitted since the previous attempt. Successful promotion and applicant annexation clear those records from current permanent members.
June (start_unsc_next_term_voting): clears new_council_members, seeds 5 regional candidates, sets electing_new_unsc_members (timed 200d). Each candidate gets a type-6 GA vote. On 2/3 pass, un_sc_new_members_step_next confirms the seat and queues the next candidate; on fail, un_sc_new_members_re_roll swaps in a replacement (capped by global.unsc_reroll_count, 15 per election). January (update_security_council_peeps): rotates security_council_members (elected seats 0-9, P5 at 10+), clamps confirmed to 5, and backfills unfilled seats from the departing cohort so no duplicates and no P5 clobber.
Rules:
- The chain advances only when the finished vote's subject equals
new_council_members^0ANDelecting_new_unsc_membersis set. Standalone type-6 votes only add toconfirmed_new_council_members; they never re-roll or advance the chain. - Stalls self-heal: the weekly pulse re-queues
new_council_members^0when the election flag is up with no live or queued type-6 vote, or dissolves the election when no candidates remain. Do not add other termination paths; route them here. - The GA cooldown strip during elections requires a type-6 entry somewhere in the queue. Removing that gate re-creates the #2305 vote storm.
A country with an approved focus unlocks UNSC_permanent_seat_campaign. UNSC_lobby_permanent_member is a targeted decision over global.p5_sc_members, so every current permanent member must be lobbied and a newly promoted member automatically becomes a target for later applicants. Each lobby costs 250 PP and requires that member's opinion of the applicant to be above 50, which is what keeps the AI from burning PP on members that would never back it. Submission sets the serialized-application lock and queues SC type 8 through update_sc_vote.
The application lock serializes applications only. target_root_trigger on the lobby decision gates on global.unsc_permanent_seat_applicant = THIS.id, not on unsc_permanent_seat_application_active — gating on the global flag hides the decision from every other eligible country for the whole length of someone else's bid.
A passed type-8 vote changes the applicant stage from unsc_permanent_seat_sc_pending to unsc_permanent_seat_ga_pending and queues GA type 22 through update_ga_vote. GA types 6, 7, and 22 are front-loaded; type 22 also strips the GA cooldown while queued. A two-thirds pass calls the canonical promotion seam; either chamber's rejection applies the 365-day retry cooldown. Aborts call the same failure path, while annexation clears the dead applicant without preserving a meaningless retry state.
The weekly queue pulse validates that the stage flag still has a matching live or queued type-8/type-22 vote. Missing state fails the application and releases the lock. An approved applicant waiting behind a live SC vote is promoted as soon as that vote finishes or aborts, via unsc_try_pending_promotion (called from security_council_vote_finished and un_abort_sc_vote). Never gate the pulse's unsc_permanent_seat_approved_pending_promotion branch on current_ongoing_sc_vote: unsc_promote_permanent_member already self-guards, and adding the check drops the applicant into the chain's else and silently cancels a won application.
SC and GA type ids live in separate namespaces (global.current_sc_vote_type vs global.current_ga_vote_type) and are dispatched by separate finishers. The numbers overlap and that is fine: SC type 6 (disarmament) and GA type 6 (UNSC seat confirmation) coexist because nothing ever reads them from the same variable. Type 5 is the one deliberate exception — a passed SC recognition vote calls update_ga_vote and hands the same id across to the Assembly. Do not add another cross-namespace type without going through that handoff.
| Type | Resolution | On pass, the subject gets |
|---|---|---|
| 1 | Cease offensive operations | has_end_war_mission + UNSC_end_all_offensive_wars |
| 2 | Economic sanctions | apply_united_nations_sanctions |
| 3 | Restrict foreign volunteers | unsc_restricted_volunteers_too |
| 4 | Arms embargo | unsc_arms_embargo |
| 5 | Recognition | routes to the GA via update_ga_vote |
| 6 | Disarmament / material breach | unsc_material_breach |
| 7 | Demand surrender of a terrorist | unsc_extradition_demanded + UN sanctions |
| 8 | Recommend permanent membership | routes the serialized applicant to GA type 22 |
- Proposable by: types 1, 2, 4 — AI and GUI; type 3 — GUI only; types 5-8 — script only.
- On fail: type 6 applies
unsc_disarmament_rejected; type 7 appliesunsc_extradition_rejected.
Type 7 only calls apply_united_nations_sanctions if the subject doesn't already carry united_nations_security_council_sanctions (it may predate the demand via a type 2 vote), and records that ownership with unsc_extradition_sanctions_applied. The extradition chain (wot.11.a) clears unsc_extradition_demanded unconditionally but only strips the dynamic modifier when it owns that flag, so it never lifts sanctions a separate type-2 vote applied.
| Type | Resolution |
|---|---|
| 5 | Recognition (arrives from SC type 5) |
| 6 | Confirm a UNSC seat — front-loaded in the queue |
| 7 | Non-binding condemnation — front-loaded in the queue |
| 8 / 9 | Decrease / increase the UN operating budget contribution |
| 10-21 | Standing resolutions (poverty, food security, health, …) |
| 22 | Final approval of permanent UNSC membership |
| 23+ | Free |
- AI:
un_ai_sc_consider_resolutionsetssc_ai_action_typefrom an explicitif/else_ifchain that only ever yields 1, 2 or 4. There is no random pick over a type range and no fallback branch — therandom = { chance = 40 }decides whether to propose, never what. GA proposals come fromun_ai_ga_consider_resolution. - Player: the propose window in
00_missiles_scripted_guis.txthas hardcoded buttons writingsc_selected_action= 1, 2, 3 or 4. - Script only: everything else. A type that has no branch in the AI chain and no GUI button cannot be proposed against an arbitrary country, and some types must stay that way — a randomly proposed "demand this nation surrender a terrorist" is nonsense. If you add a type that should stay script-only, add nothing to either path and say so in the proposable-by list above.
SC outcomes apply to the subject (var:global.current_sc_vote_nation). There is no sc_resolution_proposer and you should not add one — the GA has ga_resolution_proposer because its result event fans out to every member, but the Council's does not need it. If a petitioner needs to react to the outcome, set a flag on the subject in the finisher and have the petitioner read it (this is how the Iraq chain works: usa_petition_invasion_of_iraq queues the vote, and wot.15 triggers on IRQ = { has_country_flag = unsc_material_breach }).
Passage is has_passed_sc_vote: no veto, and at least global.un_sc_required_yes yes votes. The threshold is recomputed as ceil(60% of current Council membership), preserving 9 of 15 while scaling with permanent-member expansion. Any permanent member voting no sets security_council_veto in its own option, so a veto is not a tally, it is a flag. Do not try to infer it by counting.
- Take the next free SC id (9+). Add the outcome branch in
security_council_vote_finished, in the pass chain and in the failelsechain if the petitioner needs to distinguish "rejected" from "still voting". Both branches set a flag onvar:global.current_sc_vote_nation. - Add triggered
title/descblocks in UNSC.1 keyed onglobal.current_sc_vote_type, withUNSC.<id>.t/.dloc keys. - Add the resolution name to
UNSCGetResolutionTypePassDescin01_ledger_localisation.txt(UNSC_pass_type_<id>_desc), or UNSC.10/11 render a blank where the resolution should be. - Weight it in UNSC.1's yes/no/abstain
ai_chance.FROMinside UNSC.1 is the subject, not the proposer, sohas_opinion = { target = FROM }reads how the voter feels about the country being voted on. The genericcheck_variable = { global.current_sc_vote_type > 1 }modifiers already apply to any new type — check whether that is what you want before adding more. - Queue it with
set_temp_variable = { sc_new_vote_type = <id> }+{ sc_new_vote_nation = <TAG>.id }+update_sc_vote = yes. Never setglobal.current_sc_vote_typedirectly;update_sc_votedecides between starting the vote and queueing it behind a live one.update_sc_votedoes not setsc_action_against@<subject>— the GUI and AI proposer paths set it themselves, and a script caller must too (<TAG> = { set_global_flag = sc_action_against@THIS }), or the one-proposal-per-subject invariant breaks. Gate the proposing decision onNOT = { has_global_flag = sc_action_against@<TAG> }(tooltipTT_UNSC_NO_PENDING_SUBJECT_VOTE) so it cannot double-queue behind someone else's proposal. - Decide whether the AI and the propose GUI should be able to raise it, and wire (or deliberately do not wire) them. Record the decision in the proposable-by list above.
- A subject that can die mid-vote needs teardown.
clear_united_nations_member_stateaborts a live vote whose subject is the dying nation and purges the queue viaun_remove_dying_nation_queued_votes, and it does that for any subject, GA member or not — the membership pruning is just one part of it. It runs fromon_annex. But a country removed by a scriptedannex_countryin a decision or event is not worth betting on, so callclear_united_nations_member_stateexplicitly on the dying nation before you remove it. Any SC type whose subject is not a normal member (TAL, for instance) is exposed to this.
- Take the next free type id (23+). Add triggered
title/descblocks in UN.6 with newUN.<id>.t/.dloc keys, and gate any type-specific ai_chance modifiers. - Add the outcome branch in
general_assembly_vote_finished(inside the pass branch unless it genuinely applies on failure). - If yes-voters get a timed idea and no-voters get a choice, extend UN.410: triggered title/desc on
ga_resolution_vote_type, a branch in the accept option'sadd_timed_ideachain, and the type range checks in ai_chance (they assume 10-21 today). - Add scripted loc
un_ga_vote_on_desc_<id>andun_ga_vote_track_<id>plus the AI proposal weighting inun_ai_ga_consider_resolution. - Queue it via the
ga_new_vote_type/ga_new_vote_nationtemp vars andupdate_ga_vote.
The 2026-07 overhaul (issue #2305) fixed the vote storm (election flag never expiring plus unconditional cooldown strip), the O(N^2) resolution reject sweep, per-member meta_effect dispatch, the 2/3 rounding, and consolidated 32 near-identical events into 3. The consolidation recipe is documented in simplification-patterns.md under "Consolidate Near-Identical Event Families".