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, units: Vec<String> },
113
114 OwnNProvincesWithCulturalDominance { total: u32 },
116
117 ConstructNBuildingsIncluding { total: u32, buildings: Vec<String>, faction: String },
119
120 SubjugateFactions { total: u32, faction: Option<String> },
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, status: Option<String> },
161
162 Scripted { script_key: String, text: Option<String> },
164
165 ControlNPortsIncluding { total: u32, regions: Vec<String> },
167
168 OwnAPortAdjoiningSeaRegionsIncluding { regions: Vec<String> },
170
171 HoldEntiretyOfNProvincesIncluding { total: u32, provinces: Vec<String> },
173
174 ConstructNBuildingsFrom { total: u32, buildings: Vec<String>, faction: String },
176
177 ConstructNOfABuilding { total: u32, building: String, faction: String },
179
180 HaveAtLeastXMoney { total: u32 },
182
183 MaintainTradeWithNFactions { total: u32 },
185
186 MakeAlliance { faction: String },
188
189 ConstructBuilding { buildings: Vec<String>, faction: String },
191
192 MpCompetitive,
194
195 OwnAtLeastOneSourceOfEachResource { excluded: Vec<String> },
197
198 OwnNRegionsIncluding { total: u32, regions: Vec<String> },
200}
201
202impl ScriptItem {
207
208 pub fn key(&self) -> &str {
210 self.words.first().map(String::as_str).unwrap_or_default()
211 }
212
213 pub fn values(&self) -> &[String] {
215 self.words.get(1..).unwrap_or_default()
216 }
217
218 pub fn children(&self) -> &[ScriptItem] {
220 self.block.as_deref().unwrap_or_default()
221 }
222
223 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 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 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 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 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 const ALL: [Self; 3] = [Self::SinglePlayer, Self::MultiplayerVersus, Self::MultiplayerCoop];
271
272 pub fn from_id(id: u32) -> Option<Self> {
274 Self::ALL.into_iter().find(|campaign_type| campaign_type.id() == id)
275 }
276
277 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 pub fn id(&self) -> u32 {
288 match self {
289 Self::SinglePlayer => 0,
290 Self::MultiplayerVersus => 2,
291 Self::MultiplayerCoop => 4,
292 }
293 }
294
295 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 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 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 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
522pub 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
539pub 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
580fn 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
602pub 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
636fn script_error(message: String) -> RLibError {
638 RLibError::StartposScriptError(message)
639}