1use getset::{CopyGetters, Getters};
18
19use rpfm_lib::error::{RLibError, Result};
20
21#[cfg(test)] #[path = "missions_test.rs"] mod missions_test;
22
23#[derive(Debug, Clone, PartialEq, Eq, Default, Getters)]
29#[getset(get = "pub")]
30pub struct ScriptItem {
31
32 words: Vec<String>,
34
35 block: Option<Vec<ScriptItem>>,
37}
38
39#[derive(Debug, Clone, PartialEq, Getters, CopyGetters)]
41pub struct VictoryObjectives {
42
43 #[getset(get = "pub")]
45 faction: String,
46
47 #[getset(get_copy = "pub")]
49 campaign_type: CampaignType,
50
51 #[getset(get = "pub")]
53 missions: Vec<VictoryMission>,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum CampaignType {
59
60 SinglePlayer,
62
63 MultiplayerVersus,
65
66 MultiplayerCoop,
68}
69
70#[derive(Debug, Clone, PartialEq, Getters)]
72#[getset(get = "pub")]
73pub struct VictoryMission {
74
75 key: String,
77
78 victory_type: String,
80
81 issuer: String,
83
84 objectives: Vec<MissionObjective>,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub enum MissionObjective {
91
92 ReachSpecifiedDate { year: u32, week: u32 },
94
95 ControlNRegionsIncluding { total: u32, regions: Vec<String> },
97
98 ControlNProvincesIncluding { total: u32, provinces: Vec<String> },
100
101 ResearchNTechsOfTypeX { total: u32, categories: Vec<String>, text: Option<String> },
104
105 EarnXAmountFromBuildingWealth { total: u32 },
107
108 EarnXAmountFromRaiding { total: u32 },
110
111 OwnNUnits { total: u32, naval: bool },
113
114 OwnNProvincesWithCulturalDominance { total: u32 },
116
117 ConstructNBuildingsIncluding { total: u32, buildings: Vec<String>, faction: String },
119
120 SubjugateFactions { total: u32 },
122
123 MaintainNClientStates { total: u32 },
125
126 LootOrSackNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
128
129 RazeOrSackNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
131
132 AtLeastXReligionInAllProvinces { total: f32, religion: String },
134
135 ReclaimNDifferentSettlementsIncluding { total: u32, regions: Vec<String> },
137
138 EnsureFactionsHaveNoMilitaryPresence { factions: Vec<String> },
140
141 AtLeastXPublicOrderInProvinces { total: f32, provinces: Vec<String> },
143
144 StayHorde,
146
147 MaintainNAlliances { total: u32 },
149
150 IncomeAtLeastX { income: u32 },
152
153 EliminateCharacterInBattle { faction: String },
155
156 ConfederateFactions { total: u32 },
158
159 AttainFameLevel { level: u32 },
161}
162
163impl ScriptItem {
168
169 pub fn key(&self) -> &str {
171 self.words.first().map(String::as_str).unwrap_or_default()
172 }
173
174 pub fn values(&self) -> &[String] {
176 self.words.get(1..).unwrap_or_default()
177 }
178
179 pub fn children(&self) -> &[ScriptItem] {
181 self.block.as_deref().unwrap_or_default()
182 }
183
184 pub fn child_values(&self, key: &str) -> Vec<String> {
186 self.children().iter()
187 .filter(|child| child.key() == key)
188 .filter_map(|child| child.values().first().cloned())
189 .collect()
190 }
191
192 pub fn child_value(&self, key: &str) -> Result<String> {
198 self.child_values(key).into_iter().next().ok_or_else(|| script_error(format!("{} lacks {key}", self.key())))
199 }
200
201 pub fn child_number<T: std::str::FromStr>(&self, key: &str) -> Result<T> {
207 let value = self.child_value(key)?;
208 value.parse().map_err(|_| script_error(format!("{key} of {} isn't a number: {value}", self.key())))
209 }
210}
211
212impl VictoryObjectives {
213
214 pub fn new(faction: String, campaign_type: CampaignType, missions: Vec<VictoryMission>) -> Self {
216 Self { faction, campaign_type, missions }
217 }
218}
219
220impl VictoryMission {
221
222 pub fn new(key: String, victory_type: String, issuer: String, objectives: Vec<MissionObjective>) -> Self {
224 Self { key, victory_type, issuer, objectives }
225 }
226}
227
228impl CampaignType {
229
230 const ALL: [Self; 3] = [Self::SinglePlayer, Self::MultiplayerVersus, Self::MultiplayerCoop];
232
233 pub fn from_id(id: u32) -> Option<Self> {
235 Self::ALL.into_iter().find(|campaign_type| campaign_type.id() == id)
236 }
237
238 pub fn key(&self) -> &'static str {
240 match self {
241 Self::SinglePlayer => "sp_normal",
242 Self::MultiplayerVersus => "mp_vs_normal",
243 Self::MultiplayerCoop => "mp_co_op_normal",
244 }
245 }
246
247 pub fn id(&self) -> u32 {
249 match self {
250 Self::SinglePlayer => 0,
251 Self::MultiplayerVersus => 2,
252 Self::MultiplayerCoop => 4,
253 }
254 }
255
256 pub fn from_key(key: &str) -> Result<Self> {
262 Self::ALL.into_iter()
263 .find(|campaign_type| campaign_type.key() == key)
264 .ok_or_else(|| script_error(format!("unknown campaign type {key}")))
265 }
266}
267
268impl MissionObjective {
269
270 pub fn statements(&self) -> Vec<(&'static str, String)> {
273 let mut statements = vec![("type", self.type_key().to_owned())];
274 let mut add = |key: &'static str, values: &[String]| statements.extend(values.iter().map(|value| (key, value.to_owned())));
275 match self {
276 Self::ReachSpecifiedDate { year, week } => {
277 add("year", &[year.to_string()]);
278 add("week_of_year", &[week.to_string()]);
279 },
280 Self::ControlNRegionsIncluding { total, regions } |
281 Self::LootOrSackNDifferentSettlementsIncluding { total, regions } |
282 Self::RazeOrSackNDifferentSettlementsIncluding { total, regions } |
283 Self::ReclaimNDifferentSettlementsIncluding { total, regions } => {
284 add("total", &[total.to_string()]);
285 add("region", regions);
286 },
287 Self::ControlNProvincesIncluding { total, provinces } => {
288 add("total", &[total.to_string()]);
289 add("province", provinces);
290 },
291 Self::ResearchNTechsOfTypeX { total, categories, text } => {
292 add("override_text", text.as_slice());
293 add("total", &[total.to_string()]);
294 add("technology_category", categories);
295 },
296 Self::EarnXAmountFromBuildingWealth { total } |
297 Self::EarnXAmountFromRaiding { total } |
298 Self::OwnNUnits { total, .. } |
299 Self::OwnNProvincesWithCulturalDominance { total } |
300 Self::SubjugateFactions { total } |
301 Self::MaintainNClientStates { total } |
302 Self::MaintainNAlliances { total } |
303 Self::ConfederateFactions { total } => add("total", &[total.to_string()]),
304 Self::ConstructNBuildingsIncluding { total, buildings, faction } => {
305 add("total", &[total.to_string()]);
306 add("building_level", buildings);
307 add("faction", std::slice::from_ref(faction));
308 },
309 Self::AtLeastXReligionInAllProvinces { total, religion } => {
310 add("total", &[total.to_string()]);
311 add("religion", std::slice::from_ref(religion));
312 },
313 Self::EnsureFactionsHaveNoMilitaryPresence { factions } => add("faction", factions),
314 Self::AtLeastXPublicOrderInProvinces { total, provinces } => {
315 add("total", &[total.to_string()]);
316 add("province", provinces);
317 },
318 Self::StayHorde => {},
319 Self::IncomeAtLeastX { income } => add("income", &[income.to_string()]),
320 Self::EliminateCharacterInBattle { faction } => add("faction", std::slice::from_ref(faction)),
321 Self::AttainFameLevel { level } => add("fame_level", &[level.to_string()]),
322 }
323 statements
324 }
325
326 pub fn type_key(&self) -> &'static str {
328 match self {
329 Self::ReachSpecifiedDate { .. } => "REACH_SPECIFIED_DATE",
330 Self::ControlNRegionsIncluding { .. } => "CONTROL_N_REGIONS_INCLUDING",
331 Self::ControlNProvincesIncluding { .. } => "CONTROL_N_PROVINCES_INCLUDING",
332 Self::ResearchNTechsOfTypeX { .. } => "RESEARCH_N_TECHS_OF_TYPE_X",
333 Self::EarnXAmountFromBuildingWealth { .. } => "EARN_X_AMOUNT_FROM_BUILDING_WEALTH",
334 Self::EarnXAmountFromRaiding { .. } => "EARN_X_AMOUNT_FROM_RAIDING",
335 Self::OwnNUnits { naval: false, .. } => "OWN_N_UNITS",
336 Self::OwnNUnits { naval: true, .. } => "OWN_N_NAVAL_UNITS",
337 Self::OwnNProvincesWithCulturalDominance { .. } => "OWN_N_PROVINCES_WITH_CULTURAL_DOMINANCE",
338 Self::ConstructNBuildingsIncluding { .. } => "CONSTRUCT_N_BUILDINGS_INCLUDING",
339 Self::SubjugateFactions { .. } => "SUBJUGATE_FACTIONS",
340 Self::MaintainNClientStates { .. } => "MAINTAIN_N_CLIENT_STATES",
341 Self::LootOrSackNDifferentSettlementsIncluding { .. } => "LOOT_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING",
342 Self::RazeOrSackNDifferentSettlementsIncluding { .. } => "RAZE_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING",
343 Self::AtLeastXReligionInAllProvinces { .. } => "AT_LEAST_X_RELIGION_IN_ALL_PROVINCES",
344 Self::ReclaimNDifferentSettlementsIncluding { .. } => "RECLAIM_N_DIFFERENT_SETTLEMENTS_INCLUDING",
345 Self::EnsureFactionsHaveNoMilitaryPresence { .. } => "ENSURE_FACTIONS_HAVE_NO_MILITARY_PRESENCE",
346 Self::AtLeastXPublicOrderInProvinces { .. } => "AT_LEAST_X_PUBLIC_ORDER_IN_PROVINCES",
347 Self::StayHorde => "STAY_HORDE",
348 Self::MaintainNAlliances { .. } => "MAINTAIN_N_ALLIANCES",
349 Self::IncomeAtLeastX { .. } => "INCOME_AT_LEAST_X",
350 Self::EliminateCharacterInBattle { .. } => "ELIMINATE_CHARACTER_IN_BATTLE",
351 Self::ConfederateFactions { .. } => "CONFEDERATE_FACTIONS",
352 Self::AttainFameLevel { .. } => "ATTAIN_FAME_LEVEL",
353 }
354 }
355
356 pub fn from_item(item: &ScriptItem) -> Result<Self> {
362 let total = || item.child_number::<u32>("total");
363 Ok(match item.child_value("type")?.as_str() {
364 "REACH_SPECIFIED_DATE" => Self::ReachSpecifiedDate { year: item.child_number("year")?, week: item.child_number("week_of_year")? },
365 "CONTROL_N_REGIONS_INCLUDING" => Self::ControlNRegionsIncluding { total: total()?, regions: item.child_values("region") },
366 "CONTROL_N_PROVINCES_INCLUDING" => Self::ControlNProvincesIncluding { total: total()?, provinces: item.child_values("province") },
367 "RESEARCH_N_TECHS_OF_TYPE_X" => Self::ResearchNTechsOfTypeX {
368 total: total()?,
369 categories: item.child_values("technology_category"),
370 text: item.child_values("override_text").into_iter().next(),
371 },
372 "EARN_X_AMOUNT_FROM_BUILDING_WEALTH" => Self::EarnXAmountFromBuildingWealth { total: total()? },
373 "EARN_X_AMOUNT_FROM_RAIDING" => Self::EarnXAmountFromRaiding { total: total()? },
374 "OWN_N_UNITS" => Self::OwnNUnits { total: total()?, naval: false },
375 "OWN_N_NAVAL_UNITS" => Self::OwnNUnits { total: total()?, naval: true },
376 "OWN_N_PROVINCES_WITH_CULTURAL_DOMINANCE" => Self::OwnNProvincesWithCulturalDominance { total: total()? },
377 "CONSTRUCT_N_BUILDINGS_INCLUDING" => Self::ConstructNBuildingsIncluding {
378 total: total()?,
379 buildings: item.child_values("building_level"),
380 faction: item.child_value("faction")?,
381 },
382 "SUBJUGATE_FACTIONS" => Self::SubjugateFactions { total: total()? },
383 "MAINTAIN_N_CLIENT_STATES" => Self::MaintainNClientStates { total: total()? },
384 "LOOT_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::LootOrSackNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
385 "RAZE_OR_SACK_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::RazeOrSackNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
386 "AT_LEAST_X_RELIGION_IN_ALL_PROVINCES" => Self::AtLeastXReligionInAllProvinces { total: item.child_number("total")?, religion: item.child_value("religion")? },
387 "RECLAIM_N_DIFFERENT_SETTLEMENTS_INCLUDING" => Self::ReclaimNDifferentSettlementsIncluding { total: total()?, regions: item.child_values("region") },
388 "ENSURE_FACTIONS_HAVE_NO_MILITARY_PRESENCE" => Self::EnsureFactionsHaveNoMilitaryPresence { factions: item.child_values("faction") },
389 "AT_LEAST_X_PUBLIC_ORDER_IN_PROVINCES" => Self::AtLeastXPublicOrderInProvinces { total: item.child_number("total")?, provinces: item.child_values("province") },
390 "STAY_HORDE" => Self::StayHorde,
391 "MAINTAIN_N_ALLIANCES" => Self::MaintainNAlliances { total: total()? },
392 "INCOME_AT_LEAST_X" => Self::IncomeAtLeastX { income: item.child_number("income")? },
393 "ELIMINATE_CHARACTER_IN_BATTLE" => Self::EliminateCharacterInBattle { faction: item.child_value("faction")? },
394 "CONFEDERATE_FACTIONS" => Self::ConfederateFactions { total: total()? },
395 "ATTAIN_FAME_LEVEL" => Self::AttainFameLevel { level: item.child_number("fame_level")? },
396 other => return Err(script_error(format!("unsupported objective type {other}"))),
397 })
398 }
399}
400
401pub fn parse_script(text: &str) -> Result<Vec<ScriptItem>> {
407 let mut tokens = vec![];
408 for line in text.lines() {
409 let line = line.split("//").next().unwrap_or_default();
410 for word in line.replace('{', " { ").replace('}', " } ").replace(';', " ; ").split_whitespace() {
411 tokens.push(word.to_owned());
412 }
413 }
414
415 parse_block(&mut tokens.into_iter(), false)
416}
417
418pub fn parse_victory_objectives(text: &str) -> Result<Vec<VictoryObjectives>> {
424 let script = parse_script(text)?;
425 let mut objectives = vec![];
426 for block in script.iter().filter(|item| item.key() == "victory_objectives").flat_map(ScriptItem::children) {
427 if block.key() != "victory_objective" {
428 continue;
429 }
430
431 let mut missions = vec![];
432 for mission in block.children().iter().filter(|item| item.key() == "mission") {
433 let primary = mission.children().iter()
434 .find(|item| item.key() == "primary_objectives_and_payload")
435 .ok_or_else(|| script_error(format!("mission {} has no primary objectives", mission.child_value("key").unwrap_or_default())))?;
436 for payload in primary.children().iter().filter(|item| item.key() == "payload").flat_map(ScriptItem::children) {
437 if payload.key() != "game_victory" {
438 return Err(script_error(format!("unsupported victory payload {}", payload.key())));
439 }
440 }
441
442 missions.push(VictoryMission {
443 key: mission.child_value("key")?,
444 victory_type: mission.child_value("victory_type")?,
445 issuer: mission.child_value("issuer")?,
446 objectives: primary.children().iter().filter(|item| item.key() == "objective").map(MissionObjective::from_item).collect::<Result<_>>()?,
447 });
448 }
449
450 objectives.push(VictoryObjectives {
451 faction: block.child_value("faction")?,
452 campaign_type: CampaignType::from_key(&block.child_value("campaign_type")?)?,
453 missions,
454 });
455 }
456 Ok(objectives)
457}
458
459fn parse_block(tokens: &mut impl Iterator<Item = String>, nested: bool) -> Result<Vec<ScriptItem>> {
461 let mut items = vec![];
462 let mut words = vec![];
463 while let Some(token) = tokens.next() {
464 match token.as_str() {
465 ";" => items.push(ScriptItem { words: std::mem::take(&mut words), block: None }),
466 "{" => {
467 let block = parse_block(tokens, true)?;
468 items.push(ScriptItem { words: std::mem::take(&mut words), block: Some(block) });
469 },
470 "}" if nested => return Ok(items),
471 "}" => return Err(script_error("a } closes no block".to_owned())),
472 _ => words.push(token),
473 }
474 }
475 if nested {
476 return Err(script_error("a block isn't closed".to_owned()));
477 }
478 Ok(items)
479}
480
481pub fn write_victory_objectives(objectives: &[VictoryObjectives]) -> String {
491 let mut script = String::from("victory_objectives\n{\n");
492 for faction_objectives in objectives {
493 script.push_str("\tvictory_objective\n\t{\n");
494 script.push_str(&format!("\t\tfaction {};\n\t\tcampaign_type {};\n", faction_objectives.faction, faction_objectives.campaign_type.key()));
495 for mission in &faction_objectives.missions {
496 script.push_str("\n\t\tmission\n\t\t{\n");
497 script.push_str(&format!("\t\t\tvictory_type {};\n\t\t\tkey {};\n\t\t\tissuer {};\n", mission.victory_type, mission.key, mission.issuer));
498 script.push_str("\t\t\tprimary_objectives_and_payload\n\t\t\t{\n");
499 for objective in &mission.objectives {
500 script.push_str("\t\t\t\tobjective\n\t\t\t\t{\n");
501 for (key, value) in objective.statements() {
502 script.push_str(&format!("\t\t\t\t\t{key} {value};\n"));
503 }
504 script.push_str("\t\t\t\t}\n");
505 }
506 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");
507 script.push_str("\t\t\t}\n\t\t}\n");
508 }
509 script.push_str("\t}\n");
510 }
511 script.push_str("}\n");
512 script
513}
514
515fn script_error(message: String) -> RLibError {
517 RLibError::StartposScriptError(message)
518}