1use std::fmt::Write;
17
18use crate::catalog::{Catalog, Kind, Locale};
19use crate::error::{Result, WorkshopError};
20use crate::format::format_number;
21use crate::settings::table::{self, KeyKind, PathPart};
22use crate::settings::{Settings as SettingsTree, SettingsNode};
23use crate::wir;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct ActionLayout {
29 pub width: usize,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub enum ActionLayoutError {
36 InvalidWIR(crate::wir::error::IrError),
38 Emission(WorkshopError),
40}
41
42impl std::fmt::Display for ActionLayoutError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 match self {
45 Self::InvalidWIR(error) => write!(f, "invalid WIR: {error}"),
46 Self::Emission(error) => write!(f, "action layout emission failed: {error}"),
47 }
48 }
49}
50
51impl std::error::Error for ActionLayoutError {}
52
53pub fn action_width(
61 program: &wir::Program,
62 catalog: &Catalog,
63 locale: &Locale,
64 actions: &[wir::ActionId],
65) -> std::result::Result<ActionLayout, ActionLayoutError> {
66 program.validate().map_err(ActionLayoutError::InvalidWIR)?;
67 let mut emitter = Emitter {
68 program,
69 catalog,
70 locale: locale.clone(),
71 fallback: None,
72 fallback_ids: Vec::new(),
73 force_hero_constructors: false,
74 out: String::new(),
75 line_count: 0,
76 };
77 for action in actions {
78 emitter
79 .action(*action, 0, false)
80 .map_err(ActionLayoutError::Emission)?;
81 }
82 Ok(ActionLayout {
83 width: emitter.line_count,
84 })
85}
86
87#[derive(Debug, Clone, Default, PartialEq, Eq)]
89pub struct EmitOptions {
90 pub fallback_locale: Option<Locale>,
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct EmitOutput {
100 pub text: String,
102 pub fallback_ids: Vec<String>,
106}
107
108pub fn emit(program: &wir::Program, catalog: &Catalog, locale: &Locale) -> Result<String> {
111 emit_with_options(program, catalog, locale, &EmitOptions::default()).map(|out| out.text)
112}
113
114pub fn emit_with_options(
117 program: &wir::Program,
118 catalog: &Catalog,
119 locale: &Locale,
120 options: &EmitOptions,
121) -> Result<EmitOutput> {
122 emit_with_options_inner(program, catalog, locale, options, false)
123}
124
125pub(crate) fn emit_with_options_for_conversion(
126 program: &wir::Program,
127 catalog: &Catalog,
128 locale: &Locale,
129 options: &EmitOptions,
130) -> Result<EmitOutput> {
131 emit_with_options_inner(program, catalog, locale, options, true)
132}
133
134fn emit_with_options_inner(
135 program: &wir::Program,
136 catalog: &Catalog,
137 locale: &Locale,
138 options: &EmitOptions,
139 force_hero_constructors: bool,
140) -> Result<EmitOutput> {
141 let mut emitter = Emitter {
142 program,
143 catalog,
144 locale: locale.clone(),
145 fallback: options.fallback_locale.clone(),
146 force_hero_constructors,
147 fallback_ids: Vec::new(),
148 out: String::new(),
149 line_count: 0,
150 };
151 emitter.run()?;
152 Ok(EmitOutput {
153 text: emitter.out,
154 fallback_ids: emitter.fallback_ids,
155 })
156}
157
158struct Emitter<'a> {
159 program: &'a wir::Program,
160 catalog: &'a Catalog,
161 locale: Locale,
162 fallback: Option<Locale>,
164 fallback_ids: Vec<String>,
166 force_hero_constructors: bool,
167 out: String,
168 line_count: usize,
169}
170
171impl Emitter<'_> {
172 fn run(&mut self) -> Result<()> {
173 if let Some(settings) = &self.program.settings {
175 self.emit_settings(settings)?;
176 self.out.push('\n');
177 }
178 if !self.program.global_variables.is_empty() || !self.program.player_variables.is_empty() {
179 let variables = self.structural("variables")?;
180 self.line(0, &format!("{variables} {{"))?;
181 if !self.program.global_variables.is_empty() {
182 let global = self.structural("global")?;
183 self.line(1, &format!("{global}:"))?;
184 for variable in self.program.global_variables.iter() {
185 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
186 }
187 }
188 if !self.program.player_variables.is_empty() {
189 let player = self.structural("player")?;
190 self.line(1, &format!("{player}:"))?;
191 for variable in self.program.player_variables.iter() {
192 self.line(2, &format!("{}: {}", variable.index, variable.name))?;
193 }
194 }
195 self.line(0, "}")?;
196 self.out.push('\n');
197 }
198 if !self.program.subroutines.is_empty() {
199 let subroutines = self.structural("subroutines")?;
200 self.line(0, &format!("{subroutines} {{"))?;
201 for subroutine in self.program.subroutines.iter() {
202 self.line(1, &format!("{}: {}", subroutine.index, subroutine.name))?;
203 }
204 self.line(0, "}")?;
205 self.out.push('\n');
206 }
207 for (emitted_rules, rule) in self.program.rules.iter().enumerate() {
208 if emitted_rules > 0 {
209 self.out.push('\n');
210 }
211 self.rule(rule)?;
212 }
213 if !self.out.is_empty() && !self.out.ends_with("\n\n") {
217 self.out.push('\n');
218 }
219 Ok(())
220 }
221
222 fn emit_settings(&mut self, settings: &SettingsTree) -> Result<()> {
226 let settings_keyword = self.structural("settings")?;
227 self.line(0, &format!("{settings_keyword} {{"))?;
228 for child in &settings.children {
229 if let SettingsNode::Workshop { children, .. } = child {
230 self.emit_workshop_settings(children, 1)?;
231 continue;
232 }
233 let SettingsNode::Group { name, children, .. } = child else {
234 return Err(self.malformed("settings block children must be groups"));
235 };
236 match name.as_str() {
237 "main" | "lobby" => {
238 self.line(1, &format!("{name} {{"))?;
239 for member in children {
240 self.settings_member(member, 2, &[PathPart::Part(name)], None)?;
241 }
242 self.line(1, "}")?;
243 }
244 "gamemodes" => self.emit_modes(children)?,
245 "heroes" => self.emit_heroes(children)?,
246 "extensions" => {
247 self.line(1, "extensions {")?;
248 for member in children {
249 self.settings_member(member, 2, &[PathPart::Part("extensions")], None)?;
250 }
251 self.line(1, "}")?;
252 }
253 _ => self.emit_opaque_group(children, name, 1)?,
254 }
255 }
256 self.line(0, "}")?;
257 Ok(())
258 }
259
260 fn emit_workshop_settings(&mut self, children: &[SettingsNode], level: usize) -> Result<()> {
261 let workshop = self.structural("workshop")?;
262 self.line(level, &format!("{workshop} {{"))?;
263 for child in children {
264 self.emit_workshop_node(child, level + 1)?;
265 }
266 self.line(level, "}")?;
267 Ok(())
268 }
269
270 fn emit_workshop_node(&mut self, node: &SettingsNode, level: usize) -> Result<()> {
271 match node {
272 SettingsNode::Group { name, children, .. } => {
273 self.line(level, &format!("{name} {{"))?;
274 for child in children {
275 self.emit_workshop_node(child, level + 1)?;
276 }
277 self.line(level, "}")?;
278 Ok(())
279 }
280 SettingsNode::Workshop { children, .. } => self.emit_workshop_settings(children, level),
281 SettingsNode::Raw { name, value, .. } => {
282 if value.is_empty() {
283 self.line(level, name)
284 } else {
285 self.line(level, &format!("{name}: {value}"))
286 }
287 }
288 _ => Err(self.malformed("settings.workshop contains a typed builtin setting")),
289 }
290 }
291
292 fn emit_modes(&mut self, modes: &[SettingsNode]) -> Result<()> {
294 self.line(1, "modes {")?;
295 for mode in modes {
296 let SettingsNode::Group { name, children, .. } = mode else {
297 return Err(self.malformed("mode entries must be groups"));
298 };
299 let display = match table::mode_name(name) {
300 Some(english) => self.setting_name("modes", english, &format!("mode.{name}"))?,
301 None => name.clone(),
302 };
303 let disabled = children.iter().any(|member| {
306 matches!(
307 member,
308 SettingsNode::Bool { name: n, value: false, .. } if n == "enabled"
309 )
310 });
311 let header = if disabled {
312 let disabled_name = self.setting_name("tokens", "disabled", "token.disabled")?;
313 format!("{disabled_name} {display}")
314 } else {
315 display
316 };
317 self.line(2, &format!("{header} {{"))?;
318 for member in children {
319 if matches!(member, SettingsNode::Bool { name: n, .. } if n == "enabled") {
320 continue;
321 }
322 self.settings_member(
323 member,
324 3,
325 &[PathPart::Part("gamemodes"), PathPart::Part(name)],
326 None,
327 )?;
328 }
329 self.line(2, "}")?;
330 }
331 self.line(1, "}")?;
332 Ok(())
333 }
334
335 fn emit_heroes(&mut self, teams: &[SettingsNode]) -> Result<()> {
337 self.line(1, "heroes {")?;
338 for team in teams {
339 let SettingsNode::Group { name, children, .. } = team else {
340 return Err(self.malformed("team entries must be groups"));
341 };
342 let english = table::team_name(name)
343 .ok_or_else(|| self.malformed(format!("unknown team '{name}'")))?;
344 let display = self.setting_name("teams", english, &format!("team.{name}"))?;
345 self.line(2, &format!("{display} {{"))?;
346 for member in children {
347 match member {
348 SettingsNode::Group { name, children, .. } => {
349 let english = table::hero_name(name)
350 .ok_or_else(|| self.malformed(format!("unknown hero '{name}'")))?;
351 let hero = self.setting_name("heroes", english, &format!("hero.{name}"))?;
352 self.line(3, &format!("{hero} {{"))?;
353 for inner in children {
354 self.settings_member(
355 inner,
356 4,
357 &[PathPart::Part("heroes"), PathPart::Team, PathPart::Hero],
358 Some(name),
359 )?;
360 }
361 self.line(3, "}")?;
362 }
363 other => self.settings_member(
364 other,
365 3,
366 &[PathPart::Part("heroes"), PathPart::Team],
367 None,
368 )?,
369 }
370 }
371 self.line(2, "}")?;
372 }
373 self.line(1, "}")?;
374 Ok(())
375 }
376
377 fn settings_member(
379 &mut self,
380 node: &SettingsNode,
381 level: usize,
382 path: &[PathPart],
383 hero: Option<&str>,
384 ) -> Result<()> {
385 if let SettingsNode::Raw { name, value, .. } = node {
386 if value.is_empty() {
387 self.line(level, name)?;
388 } else {
389 self.line(level, &format!("{name}: {value}"))?;
390 }
391 return Ok(());
392 }
393 let name = node.name();
394 let mut full = path.to_vec();
395 full.push(PathPart::Part(name));
396 let entry = table::lookup(&full).ok_or_else(|| {
397 self.malformed(format!(
398 "settings key '{}' is outside the emission table",
399 table::path_string(&full)
400 ))
401 })?;
402 let display_name = if let (Some(hero), Some(key)) = (
403 hero,
404 full.last().and_then(|part| match part {
405 PathPart::Part(key) => Some(*key),
406 _ => None,
407 }),
408 ) {
409 if let Some(name) = table::hero_setting_name(hero, key, self.locale.as_str()) {
410 name.to_string()
411 } else if !matches!(
412 key,
413 "enableAbility1" | "enableAbility2" | "enableAbility3" | "enableSecondaryFire"
414 ) {
415 if let Some(slot) = table::ability_slot_for_path(&full) {
416 self.gameplay_setting_name(hero, slot, &table::path_string(&full))?
417 } else {
418 self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
419 }
420 } else {
421 self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
422 }
423 } else if let (Some(hero), Some(slot)) = (hero, table::ability_slot_for_path(&full)) {
424 self.gameplay_setting_name(hero, slot, &table::path_string(&full))?
425 } else {
426 self.setting_name("labels", entry.workshop_name, &table::path_string(&full))?
427 };
428 match (node, &entry.kind) {
429 (SettingsNode::Flag { .. }, KeyKind::Flag) => {
430 self.line(level, &display_name)?;
431 }
432 (SettingsNode::String { value, .. }, KeyKind::String) => {
433 self.line(
434 level,
435 &format!("{}: \"{}\"", display_name, escape_settings_string(value)),
436 )?;
437 }
438 (SettingsNode::String { value, .. }, KeyKind::Enum(domain)) => {
439 let english = table::enum_name(domain, value).ok_or_else(|| {
440 self.malformed(format!("unknown value '{value}' for settings key '{name}'"))
441 })?;
442 let display =
443 self.setting_name("enums", english, &format!("enum.{domain}.{value}"))?;
444 self.line(level, &format!("{display_name}: {display}"))?;
445 }
446 (SettingsNode::Number { value, .. }, KeyKind::Number) => {
447 self.line(level, &format!("{display_name}: {}", format_number(*value)))?;
448 }
449 (SettingsNode::Number { value, .. }, KeyKind::Percent) => {
450 self.line(
451 level,
452 &format!("{display_name}: {}%", format_number(*value)),
453 )?;
454 }
455 (SettingsNode::Bool { value, .. }, KeyKind::Bool) => {
456 let rendered = self.setting_name(
457 "tokens",
458 if *value { "On" } else { "Off" },
459 if *value { "token.on" } else { "token.off" },
460 )?;
461 self.line(level, &format!("{display_name}: {rendered}"))?;
462 }
463 (SettingsNode::Bool { value, .. }, KeyKind::BoolEnum(domain)) => {
464 if !*value {
465 return Err(self
466 .malformed(format!("unsupported false value for settings key '{name}'")));
467 }
468 let english = table::enum_name(domain, "enabled").ok_or_else(|| {
469 self.malformed(format!("unknown value 'enabled' for settings key '{name}'"))
470 })?;
471 let rendered =
472 self.setting_name("enums", english, &format!("enum.{domain}.enabled"))?;
473 self.line(level, &format!("{display_name}: {rendered}"))?;
474 }
475 (SettingsNode::List { elements, .. }, KeyKind::ListMap) => {
476 self.line(level, &format!("{display_name} {{"))?;
477 for element in elements {
478 let english = table::map_name(&element.value).ok_or_else(|| {
479 self.malformed(format!(
480 "unknown map '{}' in settings list '{name}'",
481 element.value
482 ))
483 })?;
484 let display =
485 self.setting_name("maps", english, &format!("map.{}.name", element.value))?;
486 self.line(level + 1, &display)?;
487 }
488 self.line(level, "}")?;
489 }
490 (SettingsNode::List { elements, .. }, KeyKind::ListHero) => {
491 self.line(level, &format!("{display_name} {{"))?;
492 for element in elements {
493 let english = table::hero_name(&element.value).ok_or_else(|| {
494 self.malformed(format!(
495 "unknown hero '{}' in settings list '{name}'",
496 element.value
497 ))
498 })?;
499 let display = self.setting_name(
500 "heroes",
501 english,
502 &format!("hero.{}.name", element.value),
503 )?;
504 self.line(level + 1, &display)?;
505 }
506 self.line(level, "}")?;
507 }
508 _ => {
509 return Err(self.malformed(format!(
510 "settings key '{name}' does not match its table kind"
511 )));
512 }
513 }
514 Ok(())
515 }
516
517 fn emit_opaque_group(
518 &mut self,
519 children: &[SettingsNode],
520 name: &str,
521 level: usize,
522 ) -> Result<()> {
523 self.line(level, &format!("{name} {{"))?;
524 for child in children {
525 match child {
526 SettingsNode::Group { name, children, .. } => {
527 self.emit_opaque_group(children, name, level + 1)?;
528 }
529 _ => self.settings_member(child, level + 1, &[], None)?,
530 }
531 }
532 self.line(level, "}")?;
533 Ok(())
534 }
535
536 fn gameplay_setting_name(&mut self, hero: &str, slot: &str, id: &str) -> Result<String> {
540 let resolve = |locale: &Locale| {
541 crate::gameplay_data::builtin().ok().and_then(|catalog| {
542 catalog
543 .query()
544 .ability_name(hero, slot, None, locale.as_str())
545 .ok()
546 .map(str::to_string)
547 })
548 };
549 if let Some(name) = resolve(&self.locale) {
550 return Ok(name);
551 }
552 if let Some(fallback) = &self.fallback {
553 if let Some(name) = resolve(fallback) {
554 if !self.fallback_ids.iter().any(|value| value == "settings") {
555 self.fallback_ids.push("settings".to_string());
556 }
557 return Ok(name);
558 }
559 }
560 Err(WorkshopError::MissingMapping {
561 kind: "setting",
562 id: id.to_string(),
563 locale: self.locale.clone(),
564 })
565 }
566
567 fn setting_name(&mut self, section: &str, english: &str, id: &str) -> Result<String> {
568 let en_us = Locale::new("en-US");
569 if self.locale == en_us {
570 return Ok(english.to_string());
571 }
572 if let Some(spelling) = table::localized_name(self.locale.as_str(), section, english) {
573 return Ok(spelling.to_string());
574 }
575 if let Some(fallback) = &self.fallback {
576 if *fallback == en_us {
577 if !self.fallback_ids.iter().any(|value| value == "settings") {
578 self.fallback_ids.push("settings".to_string());
579 }
580 return Ok(english.to_string());
581 }
582 }
583 Err(WorkshopError::MissingMapping {
584 kind: "setting",
585 id: id.to_string(),
586 locale: self.locale.clone(),
587 })
588 }
589
590 fn malformed(&self, message: impl Into<String>) -> WorkshopError {
591 WorkshopError::Malformed {
592 message: message.into(),
593 span: None,
594 }
595 }
596
597 fn rule(&mut self, rule: &wir::Rule) -> Result<()> {
598 let disabled = if rule.disabled {
599 format!("{} ", self.structural("disabled")?)
600 } else {
601 String::new()
602 };
603 let rule_keyword = self.structural("rule")?;
604 self.line(
605 0,
606 &format!(
607 "{disabled}{rule_keyword} (\"{}\") {{",
608 escape_string(&rule.name)
609 ),
610 )?;
611 let event = self.structural("event")?;
612 self.line(1, &format!("{event} {{"))?;
613 match &rule.event {
614 wir::Event::Global => {
615 let spelling = self.spelling(Kind::Event, "global")?;
616 self.line(2, &format!("{spelling};"))?;
617 }
618 wir::Event::EachPlayer => {
619 let spelling = self.spelling(Kind::Event, "eachPlayer")?;
620 self.line(2, &format!("{spelling};"))?;
621 self.event_filters(wir::EventTeam::All, &wir::EventTarget::All)?;
622 }
623 wir::Event::EachPlayerWithFilters { team, target } => {
624 let spelling = self.spelling(Kind::Event, "eachPlayer")?;
625 self.line(2, &format!("{spelling};"))?;
626 self.event_filters(*team, target)?;
627 }
628 wir::Event::Player { kind, team, target } => {
629 let spelling = self.spelling(Kind::Event, kind.catalog_id())?;
630 self.line(2, &format!("{spelling};"))?;
631 self.event_filters(*team, target)?;
632 }
633 wir::Event::Subroutine(subroutine) => {
634 let spelling = self.spelling(Kind::Event, "subroutine")?;
635 self.line(2, &format!("{spelling};"))?;
636 let name = self
637 .program
638 .subroutines
639 .get(*subroutine)
640 .map(|s| s.name.clone())
641 .unwrap_or_else(|| "<dangling>".to_string());
642 self.line(2, &format!("{name};"))?;
643 }
644 }
645 self.line(1, "}")?;
646 if !rule.conditions.is_empty() {
647 let conditions = self.structural("conditions")?;
648 self.line(1, &format!("{conditions} {{"))?;
649 for condition in &rule.conditions {
650 let mut text = String::new();
651 if let Some(wir::Value::Call { name, args }) =
654 self.program.values.get(*condition).map(|node| &node.value)
655 {
656 if is_comparison_operator(name) && args.len() == 2 {
657 self.value(args[0], &mut text)?;
658 write!(text, " {name} ").unwrap();
659 self.value(args[1], &mut text)?;
660 } else {
661 self.value(*condition, &mut text)?;
662 text.push_str(" == True");
663 }
664 } else {
665 self.value(*condition, &mut text)?;
666 text.push_str(" == True");
667 }
668 self.line(2, &format!("{text};"))?;
669 }
670 self.line(1, "}")?;
671 }
672 if !rule.actions.is_empty() {
673 let actions = self.structural("actions")?;
674 self.line(1, &format!("{actions} {{"))?;
675 for (index, action) in rule.actions.iter().enumerate() {
676 let rule_final = index + 1 == rule.actions.len();
677 self.action(*action, 2, rule_final)?;
678 }
679 self.line(1, "}")?;
680 }
681 self.line(0, "}")?;
682 Ok(())
683 }
684
685 fn event_filters(&mut self, team: wir::EventTeam, target: &wir::EventTarget) -> Result<()> {
686 let team = match team {
687 wir::EventTeam::All => "ALL",
688 wir::EventTeam::Team1 => "TEAM_1",
689 wir::EventTeam::Team2 => "TEAM_2",
690 };
691 let team = self.enum_spelling("EventTeam", team)?;
692 self.line(2, &format!("{team};"))?;
693 let target = match target {
694 wir::EventTarget::All => self.enum_spelling("EventPlayer", "ALL")?,
695 wir::EventTarget::Slot(slot) => {
696 self.enum_spelling("EventPlayer", &format!("SLOT_{slot}"))?
697 }
698 wir::EventTarget::Hero(hero) => self.enum_spelling("Hero", hero)?,
699 };
700 self.line(2, &format!("{target};"))?;
701 Ok(())
702 }
703
704 fn action(&mut self, id: wir::ActionId, level: usize, rule_final: bool) -> Result<()> {
708 let Some(action) = self.program.actions.get(id) else {
709 return Err(WorkshopError::Malformed {
710 message: format!("dangling action {id}"),
711 span: None,
712 });
713 };
714 match action {
715 wir::Action::SetGlobalVariable {
716 variable, value, ..
717 } => {
718 let name = self.global_name(*variable)?;
719 let mut value_text = String::new();
720 self.value(*value, &mut value_text)?;
721 let keyword = self.spelling(Kind::Structural, "setGlobalVariable")?;
722 self.line(level, &format!("{keyword}({name}, {value_text});"))?;
723 }
724 wir::Action::ModifyGlobalVariable {
725 variable,
726 op,
727 value,
728 ..
729 } => {
730 let name = self.global_name(*variable)?;
731 let op = self.modify_op_spelling(*op)?;
732 let mut value_text = String::new();
733 self.value(*value, &mut value_text)?;
734 let keyword = self.spelling(Kind::Structural, "modifyGlobalVariable")?;
735 self.line(level, &format!("{keyword}({name}, {op}, {value_text});"))?;
736 }
737 wir::Action::SetPlayerVariable {
738 player,
739 variable,
740 value,
741 ..
742 } => {
743 let mut player_text = String::new();
744 self.value(*player, &mut player_text)?;
745 let name = self.player_name(*variable)?;
746 let mut value_text = String::new();
747 self.value(*value, &mut value_text)?;
748 let keyword = self.spelling(Kind::Structural, "setPlayerVariable")?;
749 self.line(
750 level,
751 &format!("{keyword}({player_text}, {name}, {value_text});"),
752 )?;
753 }
754 wir::Action::ModifyPlayerVariable {
755 player,
756 variable,
757 op,
758 value,
759 ..
760 } => {
761 let mut player_text = String::new();
762 self.value(*player, &mut player_text)?;
763 let name = self.player_name(*variable)?;
764 let op = self.modify_op_spelling(*op)?;
765 let mut value_text = String::new();
766 self.value(*value, &mut value_text)?;
767 let keyword = self.spelling(Kind::Structural, "modifyPlayerVariable")?;
768 self.line(
769 level,
770 &format!("{keyword}({player_text}, {name}, {op}, {value_text});"),
771 )?;
772 }
773 wir::Action::CallSubroutine { subroutine, .. } => {
774 let name = self
775 .program
776 .subroutines
777 .get(*subroutine)
778 .map(|s| s.name.clone())
779 .ok_or_else(|| WorkshopError::Unknown {
780 kind: "subroutine",
781 spelling: format!("<{subroutine}>"),
782 locale: self.locale.clone(),
783 span: None,
784 })?;
785 let keyword = self.spelling(Kind::Structural, "callSubroutine")?;
786 self.line(level, &format!("{keyword}({name});"))?;
787 }
788 wir::Action::If {
789 branches,
790 else_body,
791 ..
792 } => {
793 for (index, branch) in branches.iter().enumerate() {
794 let mut condition = String::new();
795 self.value(branch.condition, &mut condition)?;
796 let keyword =
797 self.spelling(Kind::Structural, if index == 0 { "if" } else { "elseIf" })?;
798 self.line(level, &format!("{keyword}({condition});"))?;
799 for action in &branch.body {
800 self.action(*action, level + 1, false)?;
801 }
802 }
803 if let Some(else_body) = else_body {
804 let keyword = self.spelling(Kind::Structural, "else")?;
805 self.line(level, &format!("{keyword};"))?;
806 for action in else_body {
807 self.action(*action, level + 1, false)?;
808 }
809 }
810 if !rule_final {
813 let keyword = self.spelling(Kind::Structural, "end")?;
814 self.line(level, &format!("{keyword};"))?;
815 }
816 }
817 wir::Action::While {
818 condition, body, ..
819 } => {
820 let mut text = String::new();
821 self.value(*condition, &mut text)?;
822 let keyword = self.spelling(Kind::Structural, "while")?;
823 self.line(level, &format!("{keyword}({text});"))?;
824 for action in body {
825 self.action(*action, level + 1, false)?;
826 }
827 let end = self.spelling(Kind::Structural, "end")?;
828 self.line(level, &format!("{end};"))?;
829 }
830 wir::Action::ForGlobalVariable {
831 variable,
832 start,
833 stop,
834 step,
835 body,
836 ..
837 } => {
838 let name = self.global_name(*variable)?;
839 let mut start_text = String::new();
840 let mut stop_text = String::new();
841 let mut step_text = String::new();
842 self.value(*start, &mut start_text)?;
843 self.value(*stop, &mut stop_text)?;
844 self.value(*step, &mut step_text)?;
845 let keyword = self.spelling(Kind::Structural, "forGlobalVariable")?;
846 self.line(
847 level,
848 &format!("{keyword}({name}, {start_text}, {stop_text}, {step_text});"),
849 )?;
850 for action in body {
851 self.action(*action, level + 1, false)?;
852 }
853 let end = self.spelling(Kind::Structural, "end")?;
854 self.line(level, &format!("{end};"))?;
855 }
856 wir::Action::ForPlayerVariable {
857 player,
858 variable,
859 start,
860 stop,
861 step,
862 body,
863 ..
864 } => {
865 let keyword = self.structural("forPlayerVariable")?;
866 let mut player_text = String::new();
867 let mut start_text = String::new();
868 let mut stop_text = String::new();
869 let mut step_text = String::new();
870 self.value(*player, &mut player_text)?;
871 self.value(*start, &mut start_text)?;
872 self.value(*stop, &mut stop_text)?;
873 self.value(*step, &mut step_text)?;
874 let name = self.player_name(*variable)?;
875 self.line(
876 level,
877 &format!(
878 "{}({player_text}, {name}, {start_text}, {stop_text}, {step_text});",
879 keyword
880 ),
881 )?;
882 for action in body {
883 self.action(*action, level + 1, false)?;
884 }
885 let end = self.spelling(Kind::Structural, "end")?;
886 self.line(level, &format!("{end};"))?;
887 }
888 wir::Action::AssignMember {
889 target, op, value, ..
890 } => {
891 let mut target_text = String::new();
892 let mut value_text = String::new();
893 self.value(*target, &mut target_text)?;
894 self.value(*value, &mut value_text)?;
895 let operator = match op {
896 None => "=".to_string(),
897 Some(op) => {
898 let token = match op {
899 wir::ModifyOp::Add => "+",
900 wir::ModifyOp::Subtract => "-",
901 wir::ModifyOp::Multiply => "*",
902 wir::ModifyOp::Divide => "/",
903 wir::ModifyOp::Modulo => "%",
904 wir::ModifyOp::Min => "min",
905 wir::ModifyOp::Max => "max",
906 _ => {
907 return Err(WorkshopError::Unsupported {
908 message: format!(
909 "unsupported member assignment operator {op:?}"
910 ),
911 span: None,
912 });
913 }
914 };
915 format!("{token}=")
916 }
917 };
918 self.line(level, &format!("{target_text} {operator} {value_text};"))?;
919 }
920 wir::Action::Call { name, args, .. } => {
921 if matches!(name.as_str(), "chaseAtRate" | "chaseOverTime") {
929 let player_var = args.first().and_then(|id| {
930 self.program
931 .values
932 .get(*id)
933 .and_then(|node| match &node.value {
934 wir::Value::PlayerVariable { player, variable } => {
935 Some((*player, *variable))
936 }
937 _ => None,
938 })
939 });
940 let spelling = if let Some((player, variable)) = player_var {
941 let id = if name == "chaseAtRate" {
942 "chasePlayerVariableAtRate"
943 } else {
944 "chasePlayerVariableOverTime"
945 };
946 let spelling = self.spelling(Kind::Action, id)?;
947 let mut text = String::new();
951 self.value(player, &mut text)?;
952 let mut parts = vec![text, self.player_name(variable)?];
953 for arg in args.iter().skip(1) {
954 let mut part = String::new();
955 self.value(*arg, &mut part)?;
956 parts.push(part);
957 }
958 return self.line(level, &format!("{spelling}({});", parts.join(", ")));
959 } else {
960 self.spelling(Kind::Action, name)?
961 };
962 let mut args_text = String::new();
963 self.args(args, &mut args_text)?;
964 return self.line(level, &format!("{spelling}({args_text});"));
965 }
966 if name == "stopChasingPlayerVariable" {
967 let Some((player, variable)) = args.first().and_then(|id| {
968 self.program
969 .values
970 .get(*id)
971 .and_then(|node| match &node.value {
972 wir::Value::PlayerVariable { player, variable } => {
973 Some((*player, *variable))
974 }
975 _ => None,
976 })
977 }) else {
978 return Err(WorkshopError::Malformed {
979 message: "Stop Chasing Player Variable requires a player variable"
980 .into(),
981 span: None,
982 });
983 };
984 let spelling = self.spelling(Kind::Action, name)?;
985 let mut player_text = String::new();
986 self.value(player, &mut player_text)?;
987 return self.line(
988 level,
989 &format!(
990 "{spelling}({player_text}, {});",
991 self.player_name(variable)?
992 ),
993 );
994 }
995 let canonical = match name.as_str() {
998 "createBeam" => Some("createBeamEffect"),
999 _ => None,
1000 };
1001 let spelling = if let Some(canonical) = canonical {
1002 self.spelling(Kind::Action, canonical)?
1003 } else {
1004 self.spelling(Kind::Action, name)?
1005 };
1006 if args.is_empty() {
1007 self.line(level, &format!("{spelling};"))?;
1008 } else {
1009 let mut args_text = String::new();
1010 for (index, arg) in args.iter().enumerate() {
1011 if index > 0 {
1012 args_text.push_str(", ");
1013 }
1014 let variable_position = match name.as_str() {
1015 "setGlobalVariableAtIndex" | "modifyGlobalVariableAtIndex" => {
1016 index == 0
1017 }
1018 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex" => {
1019 index == 1
1020 }
1021 _ => false,
1022 };
1023 if variable_position {
1024 if let Some(node) = self.program.values.get(*arg) {
1025 match &node.value {
1026 wir::Value::GlobalVariable(variable) => {
1027 args_text.push_str(&self.global_name(*variable)?);
1028 continue;
1029 }
1030 wir::Value::PlayerVariable { variable, .. } => {
1031 args_text.push_str(&self.player_name(*variable)?);
1032 continue;
1033 }
1034 _ => {}
1035 }
1036 }
1037 }
1038 self.value(*arg, &mut args_text)?;
1039 }
1040 self.line(level, &format!("{spelling}({args_text});"))?;
1041 }
1042 }
1043 }
1044 Ok(())
1045 }
1046
1047 fn args(&mut self, args: &[wir::ValueId], out: &mut String) -> Result<()> {
1048 for (index, arg) in args.iter().enumerate() {
1049 if index > 0 {
1050 out.push_str(", ");
1051 }
1052 self.value(*arg, out)?;
1053 }
1054 Ok(())
1055 }
1056
1057 fn value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1058 let Some(node) = self.program.values.get(id) else {
1059 return Err(WorkshopError::Malformed {
1060 message: format!("dangling value {id}"),
1061 span: None,
1062 });
1063 };
1064 match &node.value {
1065 wir::Value::Number { text, .. } => {
1066 out.push_str(text);
1070 }
1071 wir::Value::String(value) => {
1072 self.emit_string_value(value, out)?;
1079 }
1080 wir::Value::LocalizedString(id) => {
1081 out.push_str(&self.spelling(Kind::Value, "string")?);
1082 out.push('(');
1083 let spelling = self.localized_string_spelling(id)?;
1084 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1085 out.push(')');
1086 }
1087 wir::Value::Bool(true) => out.push_str("True"),
1088 wir::Value::Bool(false) => out.push_str("False"),
1089 wir::Value::Null => out.push_str("Null"),
1090 wir::Value::Array(elements) => {
1091 if elements.is_empty() {
1092 out.push_str(&self.spelling(Kind::Value, "emptyArray")?);
1094 } else {
1095 out.push_str(&self.spelling(Kind::Value, "array")?);
1096 out.push('(');
1097 self.args(elements, out)?;
1098 out.push(')');
1099 }
1100 }
1101 wir::Value::Vector { x, y, z } => {
1102 out.push_str(&self.spelling(Kind::Value, "vector")?);
1103 out.push('(');
1104 self.value(*x, out)?;
1105 out.push_str(", ");
1106 self.value(*y, out)?;
1107 out.push_str(", ");
1108 self.value(*z, out)?;
1109 out.push(')');
1110 }
1111 wir::Value::Enum { value_type, value } => {
1112 let spelling = self.enum_spelling(value_type, value)?;
1113 if matches!(value_type.as_str(), "Color" | "Map" | "Team")
1123 || value_type == "Hero"
1124 && (spelling.contains('.')
1125 || self.locale != *self.catalog.primary_locale()
1126 || (self.force_hero_constructors
1127 && self
1128 .program
1129 .global_variables
1130 .iter()
1131 .any(|variable| variable.name == spelling)))
1132 {
1133 let domain = self
1134 .catalog
1135 .enum_domain(value_type)
1136 .and_then(|entry| entry.spelling(&self.locale))
1137 .unwrap_or(value_type);
1138 write!(out, "{domain}({spelling})").unwrap();
1139 } else {
1140 out.push_str(&spelling);
1141 }
1142 }
1143 wir::Value::GlobalVariable(variable) => {
1144 let name = self.global_name(*variable)?;
1145 write!(out, "Global.{name}").unwrap();
1146 }
1147 wir::Value::PlayerVariable { player, variable } => {
1148 out.push('(');
1151 self.value(*player, out)?;
1152 out.push(')');
1153 let name = self.player_name(*variable)?;
1154 write!(out, ".{name}").unwrap();
1155 }
1156 wir::Value::Subroutine(subroutine) => {
1157 let name = self
1158 .program
1159 .subroutines
1160 .get(*subroutine)
1161 .map(|value| value.name.clone())
1162 .ok_or_else(|| WorkshopError::Malformed {
1163 message: format!("dangling subroutine value {subroutine}"),
1164 span: None,
1165 })?;
1166 out.push_str(&name);
1167 }
1168 wir::Value::EventPlayer => out.push_str(&self.spelling(Kind::Value, "eventPlayer")?),
1169 wir::Value::Call { name, args } => {
1170 if name == "memberAccess" {
1171 if args.len() < 2 || args.len() > 3 {
1172 return Err(WorkshopError::Malformed {
1173 message: "memberAccess expects two or three arguments".to_string(),
1174 span: node.span,
1175 });
1176 }
1177 let Some(wir::ValueNode {
1178 value: wir::Value::String(member),
1179 ..
1180 }) = self.program.values.get(args[1])
1181 else {
1182 return Err(WorkshopError::Malformed {
1183 message: "memberAccess member must be a string".to_string(),
1184 span: node.span,
1185 });
1186 };
1187 let bare_event_player = self
1188 .program
1189 .values
1190 .get(args[0])
1191 .is_some_and(|node| matches!(node.value, wir::Value::EventPlayer));
1192 if !bare_event_player {
1193 out.push('(');
1194 }
1195 self.value(args[0], out)?;
1196 if !bare_event_player {
1197 out.push(')');
1198 }
1199 write!(out, ".{member}").unwrap();
1200 if let Some(index) = args.get(2) {
1201 out.push('[');
1202 self.value(*index, out)?;
1203 out.push(']');
1204 }
1205 return Ok(());
1206 }
1207 if is_comparison_operator(name) {
1208 if args.len() != 2 {
1210 return Err(WorkshopError::Malformed {
1211 message: format!("comparison call '{name}' must have 2 args"),
1212 span: None,
1213 });
1214 }
1215 out.push_str(&self.spelling(Kind::Value, "compare")?);
1216 out.push('(');
1217 self.value(args[0], out)?;
1218 write!(out, ", {name}, ").unwrap();
1219 self.value(args[1], out)?;
1220 out.push(')');
1221 return Ok(());
1222 }
1223 if name == "-" && args.len() == 1 {
1226 out.push_str(&self.spelling(Kind::Value, "multiply")?);
1227 out.push_str("(-1, ");
1228 self.value(args[0], out)?;
1229 out.push(')');
1230 return Ok(());
1231 }
1232 if name == "getAllPlayers" && args.is_empty() {
1234 out.push_str(&self.spelling(Kind::Value, "allPlayers")?);
1235 out.push('(');
1236 out.push_str(&self.enum_spelling("Team", "ALL")?);
1237 out.push(')');
1238 return Ok(());
1239 }
1240 let canonical = match name.as_str() {
1245 "+" => Some("add"),
1246 "-" => Some("subtract"),
1247 "*" => Some("multiply"),
1248 "/" => Some("divide"),
1249 "len" => Some("countOf"),
1250 "abs" => Some("absoluteValue"),
1251 "sqrt" => Some("squareRoot"),
1252 "createBeam" => Some("createBeamEffect"),
1253 "random.uniform" => Some("randomReal"),
1254 "random.choice" => Some("randomValueInArray"),
1255 "format" => Some("customString"),
1256 _ => None,
1257 };
1258 let spelling = if let Some(canonical) = canonical {
1259 self.spelling(Kind::Value, canonical)?
1260 } else {
1261 self.spelling(Kind::Value, name)?
1262 };
1263 let is_custom_string = canonical == Some("customString") || name == "customString";
1266 if name == "string" {
1267 out.push_str(&spelling);
1268 out.push('(');
1269 if let Some(first) = args.first() {
1270 self.localized_string_value(*first, out)?;
1271 if args.len() > 1 {
1272 out.push_str(", ");
1273 self.args(&args[1..], out)?;
1274 }
1275 }
1276 out.push(')');
1277 } else if args.is_empty() {
1278 out.push_str(&spelling);
1280 } else if is_custom_string {
1281 match self.canonicalize_format_call(args)? {
1289 Some((text, variable_args)) => {
1290 if variable_args.is_empty() {
1291 self.emit_string_value(&text, out)?;
1292 } else {
1293 out.push_str(&spelling);
1294 out.push('(');
1295 write!(out, "\"{}\"", escape_value_string(&text)).unwrap();
1296 if !variable_args.is_empty() {
1297 out.push_str(", ");
1298 }
1299 self.args(&variable_args, out)?;
1300 out.push(')');
1301 }
1302 }
1303 None => {
1304 out.push_str(&spelling);
1308 out.push('(');
1309 self.bare_string_value(args[0], out)?;
1310 if args.len() > 1 {
1311 out.push_str(", ");
1312 }
1313 self.args(&args[1..], out)?;
1314 out.push(')');
1315 }
1316 }
1317 } else {
1318 out.push_str(&spelling);
1319 out.push('(');
1320 self.args(args, out)?;
1321 out.push(')');
1322 }
1323 }
1324 }
1325 Ok(())
1326 }
1327
1328 fn localized_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1329 let Some(node) = self.program.values.get(id) else {
1330 return Err(WorkshopError::Malformed {
1331 message: format!("dangling value {id}"),
1332 span: None,
1333 });
1334 };
1335 let wir::Value::LocalizedString(id) = &node.value else {
1336 return Err(WorkshopError::Unsupported {
1337 message: "value 'string' argument 1 must be localized string text".to_string(),
1338 span: node.span,
1339 });
1340 };
1341 let spelling = self.localized_string_spelling(id)?;
1342 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1343 Ok(())
1344 }
1345
1346 fn canonicalize_format_call(
1357 &self,
1358 args: &[wir::ValueId],
1359 ) -> Result<Option<(String, Vec<wir::ValueId>)>> {
1360 if args.len() < 2 {
1361 return Ok(None);
1362 }
1363 let Some(text) = self.program.values.get(args[0]) else {
1364 return Ok(None);
1365 };
1366 let wir::Value::String(text) = &text.value else {
1367 return Ok(None);
1368 };
1369 let format_args = &args[1..];
1370 let mut has_implicit = false;
1373 let mut has_explicit = false;
1374 let mut out_of_range = false;
1375 let mut cursor = 0usize;
1376 let mut chars = text.chars().peekable();
1377 while let Some(ch) = chars.next() {
1378 if ch == '{' {
1379 let mut inner = String::new();
1380 let mut closed = false;
1381 for next in chars.by_ref() {
1382 if next == '}' {
1383 closed = true;
1384 break;
1385 }
1386 inner.push(next);
1387 }
1388 if !closed {
1389 break; }
1391 if inner.is_empty() {
1392 if cursor >= format_args.len() {
1393 out_of_range = true;
1394 }
1395 cursor += 1;
1396 has_implicit = true;
1397 } else if inner.chars().all(|c| c.is_ascii_digit()) {
1398 match inner.parse::<usize>() {
1399 Ok(index) if index < format_args.len() => has_explicit = true,
1400 _ => out_of_range = true,
1401 }
1402 } else {
1403 out_of_range = true;
1404 }
1405 }
1406 }
1407 if out_of_range || (has_implicit && has_explicit) {
1408 return Ok(None);
1409 }
1410 let mut any_constant = false;
1411 for id in format_args {
1412 let Some(node) = self.program.values.get(*id) else {
1413 return Ok(None);
1414 };
1415 if matches!(node.value, wir::Value::Number { .. }) {
1416 any_constant = true;
1417 }
1418 }
1419 if !has_implicit && !any_constant {
1420 return Ok(None);
1421 }
1422 let mut canonical = String::with_capacity(text.len());
1426 let mut variable_args = Vec::new();
1427 let mut variable_index = 0usize;
1428 let mut cursor = 0usize;
1429 let mut chars = text.chars().peekable();
1430 while let Some(ch) = chars.next() {
1431 if ch == '{' {
1432 let mut inner = String::new();
1433 let mut closed = false;
1434 for next in chars.by_ref() {
1435 if next == '}' {
1436 closed = true;
1437 break;
1438 }
1439 inner.push(next);
1440 }
1441 if !closed {
1442 canonical.push('{');
1443 canonical.push_str(&inner);
1444 break;
1445 }
1446 let index = if inner.is_empty() {
1447 let index = cursor;
1448 cursor += 1;
1449 index
1450 } else {
1451 match inner.parse::<usize>() {
1452 Ok(index) => index,
1453 Err(_) => {
1454 canonical.push('{');
1455 canonical.push_str(&inner);
1456 canonical.push('}');
1457 continue;
1458 }
1459 }
1460 };
1461 let Some(arg) = format_args.get(index).copied() else {
1462 canonical.push('{');
1463 canonical.push_str(&inner);
1464 canonical.push('}');
1465 continue;
1466 };
1467 let node = self.program.values.get(arg);
1468 if let Some(wir::Value::Number { value, .. }) = node.map(|node| &node.value) {
1469 canonical.push_str(&fold_number(*value));
1470 } else {
1471 write!(canonical, "{{{variable_index}}}").unwrap();
1472 variable_index += 1;
1473 variable_args.push(arg);
1474 }
1475 } else {
1476 canonical.push(ch);
1477 }
1478 }
1479 Ok(Some((canonical, variable_args)))
1480 }
1481
1482 fn modify_op_spelling(&mut self, op: wir::ModifyOp) -> Result<String> {
1485 self.spelling(Kind::Operator, op.catalog_id())
1486 }
1487
1488 fn spelling(&mut self, kind: Kind, id: &str) -> Result<String> {
1493 let Some(entry) = self.catalog.entry(kind, id) else {
1494 return Err(WorkshopError::Unknown {
1495 kind: kind.as_str(),
1496 spelling: id.to_string(),
1497 locale: self.locale.clone(),
1498 span: None,
1499 });
1500 };
1501 if let Some(spelling) = entry.spelling(&self.locale) {
1502 return Ok(spelling.to_string());
1503 }
1504 if let Some(fallback) = &self.fallback {
1505 if let Some(spelling) = entry.spelling(fallback) {
1506 self.fallback_ids.push(id.to_string());
1507 return Ok(spelling.to_string());
1508 }
1509 }
1510 Err(WorkshopError::MissingMapping {
1511 kind: kind.as_str(),
1512 id: id.to_string(),
1513 locale: self.locale.clone(),
1514 })
1515 }
1516
1517 fn localized_string_spelling(&mut self, id: &str) -> Result<String> {
1518 if let Some(spelling) = self.catalog.localized_string_spelling(&self.locale, id) {
1519 return Ok(spelling.to_string());
1520 }
1521 if let Some(fallback) = &self.fallback {
1522 if let Some(spelling) = self.catalog.localized_string_spelling(fallback, id) {
1523 self.fallback_ids.push(format!("localizedString.{id}"));
1524 return Ok(spelling.to_string());
1525 }
1526 }
1527 if self.catalog.localized_strings().any(|entry| entry.id == id) {
1528 return Err(WorkshopError::MissingMapping {
1529 kind: "localized string",
1530 id: id.to_string(),
1531 locale: self.locale.clone(),
1532 });
1533 }
1534 Err(WorkshopError::Unknown {
1535 kind: "localized string",
1536 spelling: id.to_string(),
1537 locale: self.locale.clone(),
1538 span: None,
1539 })
1540 }
1541
1542 fn structural(&mut self, id: &str) -> Result<String> {
1543 self.spelling(Kind::Structural, id)
1544 }
1545
1546 fn enum_spelling(&mut self, domain: &str, member: &str) -> Result<String> {
1549 let Some(domain_entry) = self.catalog.enum_domain(domain) else {
1550 return Err(WorkshopError::Unknown {
1551 kind: "enum domain",
1552 spelling: domain.to_string(),
1553 locale: self.locale.clone(),
1554 span: None,
1555 });
1556 };
1557 let Some(member_entry) = domain_entry.members.iter().find(|m| m.member == member) else {
1558 return Err(WorkshopError::Unknown {
1559 kind: "enum member",
1560 spelling: format!("{domain}.{member}"),
1561 locale: self.locale.clone(),
1562 span: None,
1563 });
1564 };
1565 if let Some(spelling) = member_entry.spelling(&self.locale) {
1566 return Ok(spelling.to_string());
1567 }
1568 if let Some(fallback) = &self.fallback {
1569 if let Some(spelling) = member_entry.spelling(fallback) {
1570 self.fallback_ids.push(format!("{domain}.{member}"));
1571 return Ok(spelling.to_string());
1572 }
1573 }
1574 Err(WorkshopError::MissingMapping {
1575 kind: "enum member",
1576 id: format!("{domain}.{member}"),
1577 locale: self.locale.clone(),
1578 })
1579 }
1580
1581 fn bare_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1584 let Some(node) = self.program.values.get(id) else {
1585 return Err(WorkshopError::Malformed {
1586 message: format!("dangling value {id}"),
1587 span: None,
1588 });
1589 };
1590 if let wir::Value::String(value) = &node.value {
1591 write!(out, "\"{}\"", escape_value_string(value)).unwrap();
1592 return Ok(());
1593 }
1594 self.value(id, out)
1595 }
1596
1597 fn emit_string_value(&mut self, value: &str, out: &mut String) -> Result<()> {
1600 let spelling = self.spelling(Kind::Value, "customString")?;
1601 let segments = split_string(value);
1602 emit_string_chain(&spelling, &segments, out);
1603 Ok(())
1604 }
1605
1606 fn global_name(&self, id: wir::GlobalVarId) -> Result<String> {
1607 self.program
1608 .global_variables
1609 .get(id)
1610 .map(|variable| variable.name.clone())
1611 .ok_or_else(|| WorkshopError::Unknown {
1612 kind: "global variable",
1613 spelling: format!("<{id}>"),
1614 locale: self.locale.clone(),
1615 span: None,
1616 })
1617 }
1618
1619 fn player_name(&self, id: wir::PlayerVarId) -> Result<String> {
1620 self.program
1621 .player_variables
1622 .get(id)
1623 .map(|variable| variable.name.clone())
1624 .ok_or_else(|| WorkshopError::Unknown {
1625 kind: "player variable",
1626 spelling: format!("<{id}>"),
1627 locale: self.locale.clone(),
1628 span: None,
1629 })
1630 }
1631
1632 fn line(&mut self, level: usize, text: &str) -> Result<()> {
1633 for _ in 0..level {
1634 self.out.push_str(" ");
1635 }
1636 self.out.push_str(text);
1637 self.out.push('\n');
1638 self.line_count += 1;
1639 Ok(())
1640 }
1641}
1642
1643fn is_comparison_operator(name: &str) -> bool {
1648 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
1649}
1650
1651fn escape_string(value: &str) -> String {
1652 value.replace('"', "\\\"")
1653}
1654
1655fn escape_value_string(value: &str) -> String {
1660 let mut out = String::with_capacity(value.len());
1661 for ch in value.chars() {
1662 match ch {
1663 '\\' => out.push_str("\\\\"),
1664 '"' => out.push_str("\\\""),
1665 '\n' => out.push_str("\\n"),
1666 '\r' => out.push_str("\\r"),
1667 other => out.push(other),
1668 }
1669 }
1670 out
1671}
1672
1673fn split_string(value: &str) -> Vec<String> {
1682 if value.chars().count() <= 128 {
1683 return vec![escape_value_string(value)];
1684 }
1685 let mut segments = Vec::new();
1686 let mut rest = value;
1687 while rest.chars().count() > 125 {
1688 let chunk: String = rest.chars().take(125).collect();
1689 let mut text = escape_value_string(&chunk);
1690 text.push_str("{0}");
1691 segments.push(text);
1692 rest = &rest[chunk.len()..];
1693 }
1694 if !rest.is_empty() {
1695 segments.push(escape_value_string(rest));
1696 }
1697 segments
1698}
1699
1700fn escape_settings_string(value: &str) -> String {
1706 let mut out = String::with_capacity(value.len());
1707 for ch in value.chars() {
1708 match ch {
1709 '\\' => out.push_str("\\\\"),
1710 '"' => out.push_str("\\\""),
1711 '\n' => out.push_str("\\n"),
1712 '\t' => out.push_str("\\t"),
1713 '\r' => out.push_str("\\r"),
1714 other => out.push(other),
1715 }
1716 }
1717 out
1718}
1719
1720fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
1726 let Some((first, rest)) = segments.split_first() else {
1727 return;
1728 };
1729 out.push_str(spelling);
1730 out.push('(');
1731 write!(out, "\"{first}\"").unwrap();
1732 for segment in rest {
1733 out.push_str(", ");
1734 out.push_str(spelling);
1735 out.push('(');
1736 write!(out, "\"{segment}\"").unwrap();
1737 }
1738 for _ in 0..=rest.len() {
1739 out.push(')');
1740 }
1741}
1742
1743fn fold_number(value: f64) -> String {
1747 if value.fract() == 0.0 && value.abs() < 1e15 {
1748 format!("{}", value as i64)
1749 } else {
1750 let scaled = (value * 100.0).round();
1751 let sign = if scaled < 0.0 { "-" } else { "" };
1752 let scaled = scaled.abs() as i64;
1753 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
1754 }
1755}