Skip to main content

rpfm_extensions/startpos/
missions.rs

1//---------------------------------------------------------------------------//
2// Copyright (c) 2017-2026 Ismael Gutiérrez González. All rights reserved.
3//
4// This file is part of the Rusted PackFile Manager (RPFM) project,
5// which can be found here: https://github.com/Frodo45127/rpfm.
6//
7// This file is licensed under the MIT license, which can be found here:
8// https://github.com/Frodo45127/rpfm/blob/master/LICENSE.
9//---------------------------------------------------------------------------//
10
11//! Mission scripts of a campaign (`victory_objectives.txt`, `missions.txt`) and the victory objectives they define.
12//!
13//! A script is a list of statements: words ended by `;` (`total 80;`), or words followed by a block of statements
14//! (`objective { ... }`). `//` starts a comment that runs to the end of the line. [`write_victory_objectives`] writes
15//! victory objectives back as a script.
16
17use getset::{CopyGetters, Getters};
18
19use rpfm_lib::error::{RLibError, Result};
20
21#[cfg(test)] #[path = "missions_test.rs"] mod missions_test;
22
23//---------------------------------------------------------------------------//
24//                              Types
25//---------------------------------------------------------------------------//
26
27/// A statement of a mission script.
28#[derive(Debug, Clone, PartialEq, Eq, Default, Getters)]
29#[getset(get = "pub")]
30pub struct ScriptItem {
31
32    /// Words of the statement: its key, then its values.
33    words: Vec<String>,
34
35    /// Statements of its block, if it has one.
36    block: Option<Vec<ScriptItem>>,
37}
38
39/// The victory missions of a faction in a kind of campaign (a `victory_objective` block).
40#[derive(Debug, Clone, PartialEq, Getters, CopyGetters)]
41pub struct VictoryObjectives {
42
43    /// Faction key.
44    #[getset(get = "pub")]
45    faction: String,
46
47    /// Kind of campaign the missions are for.
48    #[getset(get_copy = "pub")]
49    campaign_type: CampaignType,
50
51    /// Missions, in script order.
52    #[getset(get = "pub")]
53    missions: Vec<VictoryMission>,
54}
55
56/// Kinds of campaign with their own victory missions.
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum CampaignType {
59
60    /// Single player campaigns (`sp_normal`).
61    SinglePlayer,
62
63    /// Multiplayer campaigns against other players (`mp_vs_normal`).
64    MultiplayerVersus,
65
66    /// Cooperative multiplayer campaigns (`mp_co_op_normal`).
67    MultiplayerCoop,
68}
69
70/// A victory mission. Completing its objectives wins the game.
71#[derive(Debug, Clone, PartialEq, Getters)]
72#[getset(get = "pub")]
73pub struct VictoryMission {
74
75    /// Mission key.
76    key: String,
77
78    /// Kind of victory (`att_vc_1_short`...).
79    victory_type: String,
80
81    /// Who gives the mission (`CLAN_ELDERS`...).
82    issuer: String,
83
84    /// Objectives, in script order.
85    objectives: Vec<MissionObjective>,
86}
87
88/// An objective of a mission, by its script `type`. `total` is the amount the objective asks for.
89#[derive(Debug, Clone, PartialEq)]
90pub enum MissionObjective {
91
92    /// Reach a date (`year`, `week_of_year`).
93    ReachSpecifiedDate { year: u32, week: u32 },
94
95    /// Control a number of regions among some (`region`).
96    ControlNRegionsIncluding { total: u32, regions: Vec<String> },
97
98    /// Control a number of provinces among some (`province`).
99    ControlNProvincesIncluding { total: u32, provinces: Vec<String> },
100
101    /// Research a number of technologies of some categories (`technology_category`), with an optional text
102    /// (`override_text`).
103    ResearchNTechsOfTypeX { total: u32, categories: Vec<String>, text: Option<String> },
104
105    /// Earn an amount of money from buildings.
106    EarnXAmountFromBuildingWealth { total: u32 },
107
108    /// Earn an amount of money from raiding.
109    EarnXAmountFromRaiding { total: u32 },
110
111    /// Own a number of units (`OWN_N_UNITS`), or of naval units (`OWN_N_NAVAL_UNITS`), optionally of some kinds (`unit`).
112    OwnNUnits { total: u32, naval: bool, units: Vec<String> },
113
114    /// Own a number of provinces where the faction's culture dominates.
115    OwnNProvincesWithCulturalDominance { total: u32 },
116
117    /// Construct a number of buildings among some (`building_level`), for a faction.
118    ConstructNBuildingsIncluding { total: u32, buildings: Vec<String>, faction: String },
119
120    /// Subjugate a number of factions, optionally including one (`faction`).
121    SubjugateFactions { total: u32, faction: Option<String> },
122
123    /// Keep a number of client states.
124    MaintainNClientStates { total: u32 },
125
126    /// Loot or sack a number of settlements (`region` lists some; the game doesn't keep them).
127    LootOrSackNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
128
129    /// Raze or sack a number of settlements (`region` lists some; the game doesn't keep them).
130    RazeOrSackNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
131
132    /// Have a religion at a percentage in every province.
133    AtLeastXReligionInAllProvinces { total: f32, religion: String },
134
135    /// Reclaim a number of settlements among some (`region`).
136    ReclaimNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
137
138    /// Leave some factions (`faction`) without armies.
139    EnsureFactionsHaveNoMilitaryPresence { factions: Vec<String> },
140
141    /// Have public order at a level in some provinces (`province`).
142    AtLeastXPublicOrderInProvinces { total: f32, provinces: Vec<String> },
143
144    /// Stay a horde.
145    StayHorde,
146
147    /// Keep a number of alliances.
148    MaintainNAlliances { total: u32 },
149
150    /// Have an income of at least an amount (`income`).
151    IncomeAtLeastX { income: u32 },
152
153    /// Kill a faction's leader in battle (`faction`).
154    EliminateCharacterInBattle { faction: String },
155
156    /// Confederate a number of factions.
157    ConfederateFactions { total: u32 },
158
159    /// Reach a fame level (`fame_level`), with an optional text for its completion (`custom_completion_status`).
160    AttainFameLevel { level: u32, status: Option<String> },
161
162    /// Complete a script's objective (`script_key`), with an optional text (`override_text`).
163    Scripted { script_key: String, text: Option<String> },
164
165    /// Control a number of ports, including some regions' (`region`).
166    ControlNPortsIncluding { total: u32, regions: Vec<String> },
167
168    /// Own a port next to one of some sea regions (`region`).
169    OwnAPortAdjoiningSeaRegionsIncluding { regions: Vec<String> },
170
171    /// Hold every region of a number of provinces among some (`province`).
172    HoldEntiretyOfNProvincesIncluding { total: u32, provinces: Vec<String> },
173
174    /// Construct a number of buildings from some (`building_level`), for a faction.
175    ConstructNBuildingsFrom { total: u32, buildings: Vec<String>, faction: String },
176
177    /// Construct a number of one building (`building_level`), for a faction.
178    ConstructNOfABuilding { total: u32, building: String, faction: String },
179
180    /// Have an amount of money.
181    HaveAtLeastXMoney { total: u32 },
182
183    /// Trade with a number of factions.
184    MaintainTradeWithNFactions { total: u32 },
185
186    /// Make an alliance with a faction.
187    MakeAlliance { faction: String },
188
189    /// Construct one of some buildings (`building_level`), for a faction (Rome 2).
190    ConstructBuilding { buildings: Vec<String>, faction: String },
191
192    /// Win a multiplayer versus campaign (Rome 2).
193    MpCompetitive,
194
195    /// Own a source of every resource but some (`exclude_resource`) (Rome 2).
196    OwnAtLeastOneSourceOfEachResource { excluded: Vec<String> },
197
198    /// Own a number of regions among some (`region`) (Rome 2).
199    OwnNRegionsIncluding { total: u32, regions: Vec<String> },
200}
201
202//---------------------------------------------------------------------------//
203//                              Implementations
204//---------------------------------------------------------------------------//
205
206impl ScriptItem {
207
208    /// Returns the key of the statement (its first word).
209    pub fn key(&self) -> &str {
210        self.words.first().map(String::as_str).unwrap_or_default()
211    }
212
213    /// Returns the values of the statement (the words after its key).
214    pub fn values(&self) -> &[String] {
215        self.words.get(1..).unwrap_or_default()
216    }
217
218    /// Returns the statements of the block, or none for statements without one.
219    pub fn children(&self) -> &[ScriptItem] {
220        self.block.as_deref().unwrap_or_default()
221    }
222
223    /// Returns the first value of each child statement with a key, in order.
224    pub fn child_values(&self, key: &str) -> Vec<String> {
225        self.children().iter()
226            .filter(|child| child.key() == key)
227            .filter_map(|child| child.values().first().cloned())
228            .collect()
229    }
230
231    /// Returns the first value of the first child statement with a key.
232    ///
233    /// # Errors
234    ///
235    /// Returns an error if there's no such statement, or it has no value.
236    pub fn child_value(&self, key: &str) -> Result<String> {
237        self.child_values(key).into_iter().next().ok_or_else(|| script_error(format!("{} lacks {key}", self.key())))
238    }
239
240    /// Returns the first value of the first child statement with a key, as a number.
241    ///
242    /// # Errors
243    ///
244    /// Returns an error if there's no such statement, or its value isn't a number.
245    pub fn child_number<T: std::str::FromStr>(&self, key: &str) -> Result<T> {
246        let value = self.child_value(key)?;
247        value.parse().map_err(|_| script_error(format!("{key} of {} isn't a number: {value}", self.key())))
248    }
249}
250
251impl VictoryObjectives {
252
253    /// Creates the victory objectives of a faction for a kind of campaign.
254    pub fn new(faction: String, campaign_type: CampaignType, missions: Vec<VictoryMission>) -> Self {
255        Self { faction, campaign_type, missions }
256    }
257}
258
259impl VictoryMission {
260
261    /// Creates a victory mission.
262    pub fn new(key: String, victory_type: String, issuer: String, objectives: Vec<MissionObjective>) -> Self {
263        Self { key, victory_type, issuer, objectives }
264    }
265}
266
267impl CampaignType {
268
269    /// Every campaign type.
270    const ALL: [Self; 3] = [Self::SinglePlayer, Self::MultiplayerVersus, Self::MultiplayerCoop];
271
272    /// Returns the campaign type with an id, as `VICTORY_MISSIONS_BY_TYPE_BLOCK` stores it.
273    pub fn from_id(id: u32) -> Option<Self> {
274        Self::ALL.into_iter().find(|campaign_type| campaign_type.id() == id)
275    }
276
277    /// Returns the script key of the campaign type.
278    pub fn key(&self) -> &'static str {
279        match self {
280            Self::SinglePlayer => "sp_normal",
281            Self::MultiplayerVersus => "mp_vs_normal",
282            Self::MultiplayerCoop => "mp_co_op_normal",
283        }
284    }
285
286    /// Returns the id of the campaign type, as `VICTORY_MISSIONS_BY_TYPE_BLOCK` stores it.
287    pub fn id(&self) -> u32 {
288        match self {
289            Self::SinglePlayer => 0,
290            Self::MultiplayerVersus => 2,
291            Self::MultiplayerCoop => 4,
292        }
293    }
294
295    /// Returns the campaign type of a script key (`sp_normal`, `mp_vs_normal`, `mp_co_op_normal`).
296    ///
297    /// # Errors
298    ///
299    /// Returns an error for unknown keys.
300    pub fn from_key(key: &str) -> Result<Self> {
301        Self::ALL.into_iter()
302            .find(|campaign_type| campaign_type.key() == key)
303            .ok_or_else(|| script_error(format!("unknown campaign type {key}")))
304    }
305}
306
307impl MissionObjective {
308
309    /// Returns the statements of the objective's script block, as (key, value) pairs in the order
310    /// [`Self::from_item`] reads them.
311    pub fn statements(&self) -> Vec<(&'static str, String)> {
312        let mut statements = vec![("type", self.type_key().to_owned())];
313        let mut add = |key: &'static str, values: &[String]| statements.extend(values.iter().map(|value| (key, value.to_owned())));
314        match self {
315            Self::ReachSpecifiedDate { year, week } => {
316                add("year", &[year.to_string()]);
317                add("week_of_year", &[week.to_string()]);
318            },
319            Self::ControlNRegionsIncluding { total, regions } |
320            Self::LootOrSackNDifferentSettlementsIncluding { total, regions } |
321            Self::RazeOrSackNDifferentSettlementsIncluding { total, regions } |
322            Self::ReclaimNDifferentSettlementsIncluding { total, regions } => {
323                add("total", &[total.to_string()]);
324                add("region", regions);
325            },
326            Self::ControlNProvincesIncluding { total, provinces } => {
327                add("total", &[total.to_string()]);
328                add("province", provinces);
329            },
330            Self::ResearchNTechsOfTypeX { total, categories, text } => {
331                add("override_text", text.as_slice());
332                add("total", &[total.to_string()]);
333                add("technology_category", categories);
334            },
335            Self::EarnXAmountFromBuildingWealth { total } |
336            Self::EarnXAmountFromRaiding { total } |
337            Self::OwnNProvincesWithCulturalDominance { total } |
338            Self::MaintainNClientStates { total } |
339            Self::MaintainNAlliances { total } |
340            Self::ConfederateFactions { total } => add("total", &[total.to_string()]),
341            Self::OwnNUnits { total, units, .. } => {
342                add("total", &[total.to_string()]);
343                add("unit", units);
344            },
345            Self::SubjugateFactions { total, faction } => {
346                add("total", &[total.to_string()]);
347                add("faction", faction.as_slice());
348            },
349            Self::ConstructNBuildingsIncluding { total, buildings, faction } => {
350                add("total", &[total.to_string()]);
351                add("building_level", buildings);
352                add("faction", std::slice::from_ref(faction));
353            },
354            Self::AtLeastXReligionInAllProvinces { total, religion } => {
355                add("total", &[total.to_string()]);
356                add("religion", std::slice::from_ref(religion));
357            },
358            Self::EnsureFactionsHaveNoMilitaryPresence { factions } => add("faction", factions),
359            Self::AtLeastXPublicOrderInProvinces { total, provinces } => {
360                add("total", &[total.to_string()]);
361                add("province", provinces);
362            },
363            Self::StayHorde => {},
364            Self::IncomeAtLeastX { income } => add("income", &[income.to_string()]),
365            Self::EliminateCharacterInBattle { faction } => add("faction", std::slice::from_ref(faction)),
366            Self::AttainFameLevel { level, status } => {
367                add("fame_level", &[level.to_string()]);
368                add("custom_completion_status", status.as_slice());
369            },
370            Self::Scripted { script_key, text } => {
371                add("script_key", std::slice::from_ref(script_key));
372                add("override_text", text.as_slice());
373            },
374            Self::ControlNPortsIncluding { total, regions } => {
375                add("total", &[total.to_string()]);
376                add("region", regions);
377            },
378            Self::OwnAPortAdjoiningSeaRegionsIncluding { regions } => add("region", regions),
379            Self::HoldEntiretyOfNProvincesIncluding { total, provinces } => {
380                add("total", &[total.to_string()]);
381                add("province", provinces);
382            },
383            Self::ConstructNBuildingsFrom { total, buildings, faction } => {
384                add("total", &[total.to_string()]);
385                add("building_level", buildings);
386                add("faction", std::slice::from_ref(faction));
387            },
388            Self::ConstructNOfABuilding { total, building, faction } => {
389                add("total", &[total.to_string()]);
390                add("building_level", std::slice::from_ref(building));
391                add("faction", std::slice::from_ref(faction));
392            },
393            Self::HaveAtLeastXMoney { total } |
394            Self::MaintainTradeWithNFactions { total } => add("total", &[total.to_string()]),
395            Self::MakeAlliance { faction } => add("faction", std::slice::from_ref(faction)),
396            Self::ConstructBuilding { buildings, faction } => {
397                add("building_level", buildings);
398                add("faction", std::slice::from_ref(faction));
399            },
400            Self::MpCompetitive => {},
401            Self::OwnAtLeastOneSourceOfEachResource { excluded } => add("exclude_resource", excluded),
402            Self::OwnNRegionsIncluding { total, regions } => {
403                add("total", &[total.to_string()]);
404                add("region", regions);
405            },
406        }
407        statements
408    }
409
410    /// Returns the script key of the objective's type.
411    pub fn type_key(&self) -> &'static str {
412        match self {
413            Self::ReachSpecifiedDate { .. } => "REACH_SPECIFIED_DATE",
414            Self::ControlNRegionsIncluding { .. } => "CONTROL_N_REGIONS_INCLUDING",
415            Self::ControlNProvincesIncluding { .. } => "CONTROL_N_PROVINCES_INCLUDING",
416            Self::ResearchNTechsOfTypeX { .. } => "RESEARCH_N_TECHS_OF_TYPE_X",
417            Self::EarnXAmountFromBuildingWealth { .. } => "EARN_X_AMOUNT_FROM_BUILDING_WEALTH",
418            Self::EarnXAmountFromRaiding { .. } => "EARN_X_AMOUNT_FROM_RAIDING",
419            Self::OwnNUnits { naval: false, .. } => "OWN_N_UNITS",
420            Self::OwnNUnits { naval: true, .. } => "OWN_N_NAVAL_UNITS",
421            Self::OwnNProvincesWithCulturalDominance { .. } => "OWN_N_PROVINCES_WITH_CULTURAL_DOMINANCE",
422            Self::ConstructNBuildingsIncluding { .. } => "CONSTRUCT_N_BUILDINGS_INCLUDING",
423            Self::SubjugateFactions { .. } => "SUBJUGATE_FACTIONS",
424            Self::MaintainNClientStates { .. } => "MAINTAIN_N_CLIENT_STATES",
425            Self::LootOrSackNDifferentSettlementsIncluding { .. } => "LOOT_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING",
426            Self::RazeOrSackNDifferentSettlementsIncluding { .. } => "RAZE_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING",
427            Self::AtLeastXReligionInAllProvinces { .. } => "AT_LEAST_X_RELIGION_IN_ALL_PROVINCES",
428            Self::ReclaimNDifferentSettlementsIncluding { .. } => "RECLAIM_N_DIFFERENT_SETTLEMENTS_INCLUDING",
429            Self::EnsureFactionsHaveNoMilitaryPresence { .. } => "ENSURE_FACTIONS_HAVE_NO_MILITARY_PRESENCE",
430            Self::AtLeastXPublicOrderInProvinces { .. } => "AT_LEAST_X_PUBLIC_ORDER_IN_PROVINCES",
431            Self::StayHorde => "STAY_HORDE",
432            Self::MaintainNAlliances { .. } => "MAINTAIN_N_ALLIANCES",
433            Self::IncomeAtLeastX { .. } => "INCOME_AT_LEAST_X",
434            Self::EliminateCharacterInBattle { .. } => "ELIMINATE_CHARACTER_IN_BATTLE",
435            Self::ConfederateFactions { .. } => "CONFEDERATE_FACTIONS",
436            Self::AttainFameLevel { .. } => "ATTAIN_FAME_LEVEL",
437            Self::Scripted { .. } => "SCRIPTED",
438            Self::ControlNPortsIncluding { .. } => "CONTROL_N_PORTS_INCLUDING",
439            Self::OwnAPortAdjoiningSeaRegionsIncluding { .. } => "OWN_A_PORT_ADJOINING_SEA_REGIONS_INCLUDING",
440            Self::HoldEntiretyOfNProvincesIncluding { .. } => "HOLD_ENTIRETY_OF_N_PROVINCES_INCLUDING",
441            Self::ConstructNBuildingsFrom { .. } => "CONSTRUCT_N_BUILDINGS_FROM",
442            Self::ConstructNOfABuilding { .. } => "CONSTRUCT_N_OF_A_BUILDING",
443            Self::HaveAtLeastXMoney { .. } => "HAVE_AT_LEAST_X_MONEY",
444            Self::MaintainTradeWithNFactions { .. } => "MAINTAIN_TRADE_WITH_N_FACTIONS",
445            Self::MakeAlliance { .. } => "MAKE_ALLIANCE",
446            Self::ConstructBuilding { .. } => "CONSTRUCT_BUILDING",
447            Self::MpCompetitive => "MP_COMPETITIVE",
448            Self::OwnAtLeastOneSourceOfEachResource { .. } => "OWN_AT_LEAST_ONE_SOURCE_OF_EACH_RESOURCE",
449            Self::OwnNRegionsIncluding { .. } => "OWN_N_REGIONS_INCLUDING",
450        }
451    }
452
453    /// Reads an objective from its script block.
454    ///
455    /// # Errors
456    ///
457    /// Returns an error if the objective's type is unsupported, or a value it needs is missing or malformed.
458    pub fn from_item(item: &ScriptItem) -> Result<Self> {
459        let total = || item.child_number::<u32>("total");
460        Ok(match item.child_value("type")?.as_str() {
461            "REACH_SPECIFIED_DATE" => Self::ReachSpecifiedDate { year: item.child_number("year")?, week: item.child_number("week_of_year")? },
462            "CONTROL_N_REGIONS_INCLUDING" => Self::ControlNRegionsIncluding { total: total()?, regions: item.child_values("region") },
463            "CONTROL_N_PROVINCES_INCLUDING" => Self::ControlNProvincesIncluding { total: total()?, provinces: item.child_values("province") },
464            "RESEARCH_N_TECHS_OF_TYPE_X" => Self::ResearchNTechsOfTypeX {
465                total: total()?,
466                categories: item.child_values("technology_category"),
467                text: item.child_values("override_text").into_iter().next(),
468            },
469            "EARN_X_AMOUNT_FROM_BUILDING_WEALTH" => Self::EarnXAmountFromBuildingWealth { total: total()? },
470            "EARN_X_AMOUNT_FROM_RAIDING" => Self::EarnXAmountFromRaiding { total: total()? },
471            "OWN_N_UNITS" => Self::OwnNUnits { total: total()?, naval: false, units: item.child_values("unit") },
472            "OWN_N_NAVAL_UNITS" => Self::OwnNUnits { total: total()?, naval: true, units: item.child_values("unit") },
473            "OWN_N_PROVINCES_WITH_CULTURAL_DOMINANCE" => Self::OwnNProvincesWithCulturalDominance { total: total()? },
474            "CONSTRUCT_N_BUILDINGS_INCLUDING" => Self::ConstructNBuildingsIncluding {
475                total: total()?,
476                buildings: item.child_values("building_level"),
477                faction: item.child_value("faction")?,
478            },
479            "SUBJUGATE_FACTIONS" => Self::SubjugateFactions { total: total()?, faction: item.child_values("faction").into_iter().next() },
480            "MAINTAIN_N_CLIENT_STATES" => Self::MaintainNClientStates { total: total()? },
481            "LOOT_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::LootOrSackNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
482            "RAZE_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::RazeOrSackNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
483            "AT_LEAST_X_RELIGION_IN_ALL_PROVINCES" => Self::AtLeastXReligionInAllProvinces { total: item.child_number("total")?, religion: item.child_value("religion")? },
484            "RECLAIM_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::ReclaimNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
485            "ENSURE_FACTIONS_HAVE_NO_MILITARY_PRESENCE" => Self::EnsureFactionsHaveNoMilitaryPresence { factions: item.child_values("faction") },
486            "AT_LEAST_X_PUBLIC_ORDER_IN_PROVINCES" => Self::AtLeastXPublicOrderInProvinces { total: item.child_number("total")?, provinces: item.child_values("province") },
487            "STAY_HORDE" => Self::StayHorde,
488            "MAINTAIN_N_ALLIANCES" => Self::MaintainNAlliances { total: total()? },
489            "INCOME_AT_LEAST_X" => Self::IncomeAtLeastX { income: item.child_number("income")? },
490            "ELIMINATE_CHARACTER_IN_BATTLE" => Self::EliminateCharacterInBattle { faction: item.child_value("faction")? },
491            "CONFEDERATE_FACTIONS" => Self::ConfederateFactions { total: total()? },
492            "ATTAIN_FAME_LEVEL" => Self::AttainFameLevel {
493                level: item.child_number("fame_level")?,
494                status: item.child_values("custom_completion_status").into_iter().next(),
495            },
496            "SCRIPTED" => Self::Scripted { script_key: item.child_value("script_key")?, text: item.child_values("override_text").into_iter().next() },
497            "CONTROL_N_PORTS_INCLUDING" => Self::ControlNPortsIncluding { total: total()?, regions: item.child_values("region") },
498            "OWN_A_PORT_ADJOINING_SEA_REGIONS_INCLUDING" => Self::OwnAPortAdjoiningSeaRegionsIncluding { regions: item.child_values("region") },
499            "HOLD_ENTIRETY_OF_N_PROVINCES_INCLUDING" => Self::HoldEntiretyOfNProvincesIncluding { total: total()?, provinces: item.child_values("province") },
500            "CONSTRUCT_N_BUILDINGS_FROM" => Self::ConstructNBuildingsFrom {
501                total: total()?,
502                buildings: item.child_values("building_level"),
503                faction: item.child_value("faction")?,
504            },
505            "CONSTRUCT_N_OF_A_BUILDING" => Self::ConstructNOfABuilding {
506                total: total()?,
507                building: item.child_value("building_level")?,
508                faction: item.child_value("faction")?,
509            },
510            "HAVE_AT_LEAST_X_MONEY" => Self::HaveAtLeastXMoney { total: total()? },
511            "MAINTAIN_TRADE_WITH_N_FACTIONS" => Self::MaintainTradeWithNFactions { total: total()? },
512            "MAKE_ALLIANCE" => Self::MakeAlliance { faction: item.child_value("faction")? },
513            "CONSTRUCT_BUILDING" => Self::ConstructBuilding { buildings: item.child_values("building_level"), faction: item.child_value("faction")? },
514            "MP_COMPETITIVE" => Self::MpCompetitive,
515            "OWN_AT_LEAST_ONE_SOURCE_OF_EACH_RESOURCE" => Self::OwnAtLeastOneSourceOfEachResource { excluded: item.child_values("exclude_resource") },
516            "OWN_N_REGIONS_INCLUDING" => Self::OwnNRegionsIncluding { total: total()?, regions: item.child_values("region") },
517            other => return Err(script_error(format!("unsupported objective type {other}"))),
518        })
519    }
520}
521
522/// Parses a mission script into its statements.
523///
524/// # Errors
525///
526/// Returns an error if a block isn't closed, or a `}` closes nothing.
527pub fn parse_script(text: &str) -> Result<Vec<ScriptItem>> {
528    let mut tokens = vec![];
529    for line in text.lines() {
530        let line = line.split("//").next().unwrap_or_default();
531        for word in line.replace('{', " { ").replace('}', " } ").replace(';', " ; ").split_whitespace() {
532            tokens.push(word.to_owned());
533        }
534    }
535
536    parse_block(&mut tokens.into_iter(), false)
537}
538
539/// Parses the victory objectives of a `victory_objectives.txt` script.
540///
541/// # Errors
542///
543/// Returns an error if the script is malformed, or uses an objective or payload that isn't supported.
544pub fn parse_victory_objectives(text: &str) -> Result<Vec<VictoryObjectives>> {
545    let script = parse_script(text)?;
546    let mut objectives = vec![];
547    for block in script.iter().filter(|item| item.key() == "victory_objectives").flat_map(ScriptItem::children) {
548        if block.key() != "victory_objective" {
549            continue;
550        }
551
552        let mut missions = vec![];
553        for mission in block.children().iter().filter(|item| item.key() == "mission") {
554            let primary = mission.children().iter()
555                .find(|item| item.key() == "primary_objectives_and_payload")
556                .ok_or_else(|| script_error(format!("mission {} has no primary objectives", mission.child_value("key").unwrap_or_default())))?;
557            for payload in primary.children().iter().filter(|item| item.key() == "payload").flat_map(ScriptItem::children) {
558                if payload.key() != "game_victory" {
559                    return Err(script_error(format!("unsupported victory payload {}", payload.key())));
560                }
561            }
562
563            missions.push(VictoryMission {
564                key: mission.child_value("key")?,
565                victory_type: mission.child_value("victory_type")?,
566                issuer: mission.child_value("issuer")?,
567                objectives: primary.children().iter().filter(|item| item.key() == "objective").map(MissionObjective::from_item).collect::<Result<_>>()?,
568            });
569        }
570
571        objectives.push(VictoryObjectives {
572            faction: block.child_value("faction")?,
573            campaign_type: CampaignType::from_key(&block.child_value("campaign_type")?)?,
574            missions,
575        });
576    }
577    Ok(objectives)
578}
579
580/// Parses statements until the end of the current block (or of the script, at the top level).
581fn parse_block(tokens: &mut impl Iterator<Item = String>, nested: bool) -> Result<Vec<ScriptItem>> {
582    let mut items = vec![];
583    let mut words = vec![];
584    while let Some(token) = tokens.next() {
585        match token.as_str() {
586            ";" => items.push(ScriptItem { words: std::mem::take(&mut words), block: None }),
587            "{" => {
588                let block = parse_block(tokens, true)?;
589                items.push(ScriptItem { words: std::mem::take(&mut words), block: Some(block) });
590            },
591            "}" if nested => return Ok(items),
592            "}" => return Err(script_error("a } closes no block".to_owned())),
593            _ => words.push(token),
594        }
595    }
596    if nested {
597        return Err(script_error("a block isn't closed".to_owned()));
598    }
599    Ok(items)
600}
601
602/// Writes victory objectives as a `victory_objectives.txt` script, which [`parse_victory_objectives`] reads back.
603///
604/// # Arguments
605///
606/// * `objectives` - The victory objectives of each faction and kind of campaign.
607///
608/// # Returns
609///
610/// The script, with tabs for indentation.
611pub fn write_victory_objectives(objectives: &[VictoryObjectives]) -> String {
612    let mut script = String::from("victory_objectives\n{\n");
613    for faction_objectives in objectives {
614        script.push_str("\tvictory_objective\n\t{\n");
615        script.push_str(&format!("\t\tfaction {};\n\t\tcampaign_type {};\n", faction_objectives.faction, faction_objectives.campaign_type.key()));
616        for mission in &faction_objectives.missions {
617            script.push_str("\n\t\tmission\n\t\t{\n");
618            script.push_str(&format!("\t\t\tvictory_type {};\n\t\t\tkey {};\n\t\t\tissuer {};\n", mission.victory_type, mission.key, mission.issuer));
619            script.push_str("\t\t\tprimary_objectives_and_payload\n\t\t\t{\n");
620            for objective in &mission.objectives {
621                script.push_str("\t\t\t\tobjective\n\t\t\t\t{\n");
622                for (key, value) in objective.statements() {
623                    script.push_str(&format!("\t\t\t\t\t{key} {value};\n"));
624                }
625                script.push_str("\t\t\t\t}\n");
626            }
627            script.push_str("\t\t\t\tpayload\n\t\t\t\t{\n\t\t\t\t\tgame_victory;\n\t\t\t\t}\n");
628            script.push_str("\t\t\t}\n\t\t}\n");
629        }
630        script.push_str("\t}\n");
631    }
632    script.push_str("}\n");
633    script
634}
635
636/// Builds a script error.
637fn script_error(message: String) -> RLibError {
638    RLibError::StartposScriptError(message)
639}