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::List { elements, .. }, KeyKind::ListMap) => {
464 self.line(level, &format!("{display_name} {{"))?;
465 for element in elements {
466 let english = table::map_name(&element.value).ok_or_else(|| {
467 self.malformed(format!(
468 "unknown map '{}' in settings list '{name}'",
469 element.value
470 ))
471 })?;
472 let display =
473 self.setting_name("maps", english, &format!("map.{}.name", element.value))?;
474 self.line(level + 1, &display)?;
475 }
476 self.line(level, "}")?;
477 }
478 (SettingsNode::List { elements, .. }, KeyKind::ListHero) => {
479 self.line(level, &format!("{display_name} {{"))?;
480 for element in elements {
481 let english = table::hero_name(&element.value).ok_or_else(|| {
482 self.malformed(format!(
483 "unknown hero '{}' in settings list '{name}'",
484 element.value
485 ))
486 })?;
487 let display = self.setting_name(
488 "heroes",
489 english,
490 &format!("hero.{}.name", element.value),
491 )?;
492 self.line(level + 1, &display)?;
493 }
494 self.line(level, "}")?;
495 }
496 _ => {
497 return Err(self.malformed(format!(
498 "settings key '{name}' does not match its table kind"
499 )));
500 }
501 }
502 Ok(())
503 }
504
505 fn emit_opaque_group(
506 &mut self,
507 children: &[SettingsNode],
508 name: &str,
509 level: usize,
510 ) -> Result<()> {
511 self.line(level, &format!("{name} {{"))?;
512 for child in children {
513 match child {
514 SettingsNode::Group { name, children, .. } => {
515 self.emit_opaque_group(children, name, level + 1)?;
516 }
517 _ => self.settings_member(child, level + 1, &[], None)?,
518 }
519 }
520 self.line(level, "}")?;
521 Ok(())
522 }
523
524 fn gameplay_setting_name(&mut self, hero: &str, slot: &str, id: &str) -> Result<String> {
528 let resolve = |locale: &Locale| {
529 crate::gameplay_data::builtin().ok().and_then(|catalog| {
530 catalog
531 .query()
532 .ability_name(hero, slot, None, locale.as_str())
533 .ok()
534 .map(str::to_string)
535 })
536 };
537 if let Some(name) = resolve(&self.locale) {
538 return Ok(name);
539 }
540 if let Some(fallback) = &self.fallback {
541 if let Some(name) = resolve(fallback) {
542 if !self.fallback_ids.iter().any(|value| value == "settings") {
543 self.fallback_ids.push("settings".to_string());
544 }
545 return Ok(name);
546 }
547 }
548 Err(WorkshopError::MissingMapping {
549 kind: "setting",
550 id: id.to_string(),
551 locale: self.locale.clone(),
552 })
553 }
554
555 fn setting_name(&mut self, section: &str, english: &str, id: &str) -> Result<String> {
556 let en_us = Locale::new("en-US");
557 if self.locale == en_us {
558 return Ok(english.to_string());
559 }
560 if let Some(spelling) = table::localized_name(self.locale.as_str(), section, english) {
561 return Ok(spelling.to_string());
562 }
563 if let Some(fallback) = &self.fallback {
564 if *fallback == en_us {
565 if !self.fallback_ids.iter().any(|value| value == "settings") {
566 self.fallback_ids.push("settings".to_string());
567 }
568 return Ok(english.to_string());
569 }
570 }
571 Err(WorkshopError::MissingMapping {
572 kind: "setting",
573 id: id.to_string(),
574 locale: self.locale.clone(),
575 })
576 }
577
578 fn malformed(&self, message: impl Into<String>) -> WorkshopError {
579 WorkshopError::Malformed {
580 message: message.into(),
581 span: None,
582 }
583 }
584
585 fn rule(&mut self, rule: &wir::Rule) -> Result<()> {
586 let disabled = if rule.disabled {
587 format!("{} ", self.structural("disabled")?)
588 } else {
589 String::new()
590 };
591 let rule_keyword = self.structural("rule")?;
592 self.line(
593 0,
594 &format!(
595 "{disabled}{rule_keyword} (\"{}\") {{",
596 escape_string(&rule.name)
597 ),
598 )?;
599 let event = self.structural("event")?;
600 self.line(1, &format!("{event} {{"))?;
601 match &rule.event {
602 wir::Event::Global => {
603 let spelling = self.spelling(Kind::Event, "global")?;
604 self.line(2, &format!("{spelling};"))?;
605 }
606 wir::Event::EachPlayer => {
607 let spelling = self.spelling(Kind::Event, "eachPlayer")?;
608 self.line(2, &format!("{spelling};"))?;
609 self.event_filters(wir::EventTeam::All, &wir::EventTarget::All)?;
610 }
611 wir::Event::EachPlayerWithFilters { team, target } => {
612 let spelling = self.spelling(Kind::Event, "eachPlayer")?;
613 self.line(2, &format!("{spelling};"))?;
614 self.event_filters(*team, target)?;
615 }
616 wir::Event::Player { kind, team, target } => {
617 let spelling = self.spelling(Kind::Event, kind.catalog_id())?;
618 self.line(2, &format!("{spelling};"))?;
619 self.event_filters(*team, target)?;
620 }
621 wir::Event::Subroutine(subroutine) => {
622 let spelling = self.spelling(Kind::Event, "subroutine")?;
623 self.line(2, &format!("{spelling};"))?;
624 let name = self
625 .program
626 .subroutines
627 .get(*subroutine)
628 .map(|s| s.name.clone())
629 .unwrap_or_else(|| "<dangling>".to_string());
630 self.line(2, &format!("{name};"))?;
631 }
632 }
633 self.line(1, "}")?;
634 if !rule.conditions.is_empty() {
635 let conditions = self.structural("conditions")?;
636 self.line(1, &format!("{conditions} {{"))?;
637 for condition in &rule.conditions {
638 let mut text = String::new();
639 if let Some(wir::Value::Call { name, args }) =
642 self.program.values.get(*condition).map(|node| &node.value)
643 {
644 if is_comparison_operator(name) && args.len() == 2 {
645 self.value(args[0], &mut text)?;
646 write!(text, " {name} ").unwrap();
647 self.value(args[1], &mut text)?;
648 } else {
649 self.value(*condition, &mut text)?;
650 text.push_str(" == True");
651 }
652 } else {
653 self.value(*condition, &mut text)?;
654 text.push_str(" == True");
655 }
656 self.line(2, &format!("{text};"))?;
657 }
658 self.line(1, "}")?;
659 }
660 if !rule.actions.is_empty() {
661 let actions = self.structural("actions")?;
662 self.line(1, &format!("{actions} {{"))?;
663 for (index, action) in rule.actions.iter().enumerate() {
664 let rule_final = index + 1 == rule.actions.len();
665 self.action(*action, 2, rule_final)?;
666 }
667 self.line(1, "}")?;
668 }
669 self.line(0, "}")?;
670 Ok(())
671 }
672
673 fn event_filters(&mut self, team: wir::EventTeam, target: &wir::EventTarget) -> Result<()> {
674 let team = match team {
675 wir::EventTeam::All => "ALL",
676 wir::EventTeam::Team1 => "TEAM_1",
677 wir::EventTeam::Team2 => "TEAM_2",
678 };
679 let team = self.enum_spelling("EventTeam", team)?;
680 self.line(2, &format!("{team};"))?;
681 let target = match target {
682 wir::EventTarget::All => self.enum_spelling("EventPlayer", "ALL")?,
683 wir::EventTarget::Slot(slot) => {
684 self.enum_spelling("EventPlayer", &format!("SLOT_{slot}"))?
685 }
686 wir::EventTarget::Hero(hero) => self.enum_spelling("Hero", hero)?,
687 };
688 self.line(2, &format!("{target};"))?;
689 Ok(())
690 }
691
692 fn action(&mut self, id: wir::ActionId, level: usize, rule_final: bool) -> Result<()> {
696 let Some(action) = self.program.actions.get(id) else {
697 return Err(WorkshopError::Malformed {
698 message: format!("dangling action {id}"),
699 span: None,
700 });
701 };
702 match action {
703 wir::Action::SetGlobalVariable {
704 variable, value, ..
705 } => {
706 let name = self.global_name(*variable)?;
707 let mut value_text = String::new();
708 self.value(*value, &mut value_text)?;
709 let keyword = self.spelling(Kind::Structural, "setGlobalVariable")?;
710 self.line(level, &format!("{keyword}({name}, {value_text});"))?;
711 }
712 wir::Action::ModifyGlobalVariable {
713 variable,
714 op,
715 value,
716 ..
717 } => {
718 let name = self.global_name(*variable)?;
719 let op = self.modify_op_spelling(*op)?;
720 let mut value_text = String::new();
721 self.value(*value, &mut value_text)?;
722 let keyword = self.spelling(Kind::Structural, "modifyGlobalVariable")?;
723 self.line(level, &format!("{keyword}({name}, {op}, {value_text});"))?;
724 }
725 wir::Action::SetPlayerVariable {
726 player,
727 variable,
728 value,
729 ..
730 } => {
731 let mut player_text = String::new();
732 self.value(*player, &mut player_text)?;
733 let name = self.player_name(*variable)?;
734 let mut value_text = String::new();
735 self.value(*value, &mut value_text)?;
736 let keyword = self.spelling(Kind::Structural, "setPlayerVariable")?;
737 self.line(
738 level,
739 &format!("{keyword}({player_text}, {name}, {value_text});"),
740 )?;
741 }
742 wir::Action::ModifyPlayerVariable {
743 player,
744 variable,
745 op,
746 value,
747 ..
748 } => {
749 let mut player_text = String::new();
750 self.value(*player, &mut player_text)?;
751 let name = self.player_name(*variable)?;
752 let op = self.modify_op_spelling(*op)?;
753 let mut value_text = String::new();
754 self.value(*value, &mut value_text)?;
755 let keyword = self.spelling(Kind::Structural, "modifyPlayerVariable")?;
756 self.line(
757 level,
758 &format!("{keyword}({player_text}, {name}, {op}, {value_text});"),
759 )?;
760 }
761 wir::Action::CallSubroutine { subroutine, .. } => {
762 let name = self
763 .program
764 .subroutines
765 .get(*subroutine)
766 .map(|s| s.name.clone())
767 .ok_or_else(|| WorkshopError::Unknown {
768 kind: "subroutine",
769 spelling: format!("<{subroutine}>"),
770 locale: self.locale.clone(),
771 span: None,
772 })?;
773 let keyword = self.spelling(Kind::Structural, "callSubroutine")?;
774 self.line(level, &format!("{keyword}({name});"))?;
775 }
776 wir::Action::If {
777 branches,
778 else_body,
779 ..
780 } => {
781 for (index, branch) in branches.iter().enumerate() {
782 let mut condition = String::new();
783 self.value(branch.condition, &mut condition)?;
784 let keyword =
785 self.spelling(Kind::Structural, if index == 0 { "if" } else { "elseIf" })?;
786 self.line(level, &format!("{keyword}({condition});"))?;
787 for action in &branch.body {
788 self.action(*action, level + 1, false)?;
789 }
790 }
791 if let Some(else_body) = else_body {
792 let keyword = self.spelling(Kind::Structural, "else")?;
793 self.line(level, &format!("{keyword};"))?;
794 for action in else_body {
795 self.action(*action, level + 1, false)?;
796 }
797 }
798 if !rule_final {
801 let keyword = self.spelling(Kind::Structural, "end")?;
802 self.line(level, &format!("{keyword};"))?;
803 }
804 }
805 wir::Action::While {
806 condition, body, ..
807 } => {
808 let mut text = String::new();
809 self.value(*condition, &mut text)?;
810 let keyword = self.spelling(Kind::Structural, "while")?;
811 self.line(level, &format!("{keyword}({text});"))?;
812 for action in body {
813 self.action(*action, level + 1, false)?;
814 }
815 let end = self.spelling(Kind::Structural, "end")?;
816 self.line(level, &format!("{end};"))?;
817 }
818 wir::Action::ForGlobalVariable {
819 variable,
820 start,
821 stop,
822 step,
823 body,
824 ..
825 } => {
826 let name = self.global_name(*variable)?;
827 let mut start_text = String::new();
828 let mut stop_text = String::new();
829 let mut step_text = String::new();
830 self.value(*start, &mut start_text)?;
831 self.value(*stop, &mut stop_text)?;
832 self.value(*step, &mut step_text)?;
833 let keyword = self.spelling(Kind::Structural, "forGlobalVariable")?;
834 self.line(
835 level,
836 &format!("{keyword}({name}, {start_text}, {stop_text}, {step_text});"),
837 )?;
838 for action in body {
839 self.action(*action, level + 1, false)?;
840 }
841 let end = self.spelling(Kind::Structural, "end")?;
842 self.line(level, &format!("{end};"))?;
843 }
844 wir::Action::ForPlayerVariable {
845 player,
846 variable,
847 start,
848 stop,
849 step,
850 body,
851 ..
852 } => {
853 let keyword = self.structural("forPlayerVariable")?;
854 let mut player_text = String::new();
855 let mut start_text = String::new();
856 let mut stop_text = String::new();
857 let mut step_text = String::new();
858 self.value(*player, &mut player_text)?;
859 self.value(*start, &mut start_text)?;
860 self.value(*stop, &mut stop_text)?;
861 self.value(*step, &mut step_text)?;
862 let name = self.player_name(*variable)?;
863 self.line(
864 level,
865 &format!(
866 "{}({player_text}, {name}, {start_text}, {stop_text}, {step_text});",
867 keyword
868 ),
869 )?;
870 for action in body {
871 self.action(*action, level + 1, false)?;
872 }
873 let end = self.spelling(Kind::Structural, "end")?;
874 self.line(level, &format!("{end};"))?;
875 }
876 wir::Action::Debug { value, .. } => {
877 self.emit_hud_text(*value, level, true)?;
882 }
883 wir::Action::Print { message, .. } => {
884 self.emit_hud_text(*message, level, false)?;
885 }
886 wir::Action::AssignMember {
887 target, op, value, ..
888 } => {
889 let mut target_text = String::new();
890 let mut value_text = String::new();
891 self.value(*target, &mut target_text)?;
892 self.value(*value, &mut value_text)?;
893 let operator = match op {
894 None => "=".to_string(),
895 Some(op) => {
896 let token = match op {
897 wir::ModifyOp::Add => "+",
898 wir::ModifyOp::Subtract => "-",
899 wir::ModifyOp::Multiply => "*",
900 wir::ModifyOp::Divide => "/",
901 wir::ModifyOp::Modulo => "%",
902 wir::ModifyOp::Min => "min",
903 wir::ModifyOp::Max => "max",
904 _ => {
905 return Err(WorkshopError::Unsupported {
906 message: format!(
907 "unsupported member assignment operator {op:?}"
908 ),
909 span: None,
910 });
911 }
912 };
913 format!("{token}=")
914 }
915 };
916 self.line(level, &format!("{target_text} {operator} {value_text};"))?;
917 }
918 wir::Action::Call { name, args, .. } => {
919 if matches!(name.as_str(), "chaseAtRate" | "chaseOverTime") {
927 let player_var = args.first().and_then(|id| {
928 self.program
929 .values
930 .get(*id)
931 .and_then(|node| match &node.value {
932 wir::Value::PlayerVariable { player, variable } => {
933 Some((*player, *variable))
934 }
935 _ => None,
936 })
937 });
938 let spelling = if let Some((player, variable)) = player_var {
939 let id = if name == "chaseAtRate" {
940 "chasePlayerVariableAtRate"
941 } else {
942 "chasePlayerVariableOverTime"
943 };
944 let spelling = self.spelling(Kind::Action, id)?;
945 let mut text = String::new();
949 self.value(player, &mut text)?;
950 let mut parts = vec![text, self.player_name(variable)?];
951 for arg in args.iter().skip(1) {
952 let mut part = String::new();
953 self.value(*arg, &mut part)?;
954 parts.push(part);
955 }
956 return self.line(level, &format!("{spelling}({});", parts.join(", ")));
957 } else {
958 self.spelling(Kind::Action, name)?
959 };
960 let mut args_text = String::new();
961 self.args(args, &mut args_text)?;
962 return self.line(level, &format!("{spelling}({args_text});"));
963 }
964 if name == "stopChasingPlayerVariable" {
965 let Some((player, variable)) = args.first().and_then(|id| {
966 self.program
967 .values
968 .get(*id)
969 .and_then(|node| match &node.value {
970 wir::Value::PlayerVariable { player, variable } => {
971 Some((*player, *variable))
972 }
973 _ => None,
974 })
975 }) else {
976 return Err(WorkshopError::Malformed {
977 message: "Stop Chasing Player Variable requires a player variable"
978 .into(),
979 span: None,
980 });
981 };
982 let spelling = self.spelling(Kind::Action, name)?;
983 let mut player_text = String::new();
984 self.value(player, &mut player_text)?;
985 return self.line(
986 level,
987 &format!(
988 "{spelling}({player_text}, {});",
989 self.player_name(variable)?
990 ),
991 );
992 }
993 let canonical = match name.as_str() {
996 "createBeam" => Some("createBeamEffect"),
997 _ => None,
998 };
999 let spelling = if let Some(canonical) = canonical {
1000 self.spelling(Kind::Action, canonical)?
1001 } else {
1002 self.spelling(Kind::Action, name)?
1003 };
1004 if args.is_empty() {
1005 self.line(level, &format!("{spelling};"))?;
1006 } else {
1007 let mut args_text = String::new();
1008 for (index, arg) in args.iter().enumerate() {
1009 if index > 0 {
1010 args_text.push_str(", ");
1011 }
1012 let variable_position = match name.as_str() {
1013 "setGlobalVariableAtIndex" | "modifyGlobalVariableAtIndex" => {
1014 index == 0
1015 }
1016 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex" => {
1017 index == 1
1018 }
1019 _ => false,
1020 };
1021 if variable_position {
1022 if let Some(node) = self.program.values.get(*arg) {
1023 match &node.value {
1024 wir::Value::GlobalVariable(variable) => {
1025 args_text.push_str(&self.global_name(*variable)?);
1026 continue;
1027 }
1028 wir::Value::PlayerVariable { variable, .. } => {
1029 args_text.push_str(&self.player_name(*variable)?);
1030 continue;
1031 }
1032 _ => {}
1033 }
1034 }
1035 }
1036 self.value(*arg, &mut args_text)?;
1037 }
1038 self.line(level, &format!("{spelling}({args_text});"))?;
1039 }
1040 }
1041 }
1042 Ok(())
1043 }
1044
1045 fn emit_hud_text(&mut self, value: wir::ValueId, level: usize, is_debug: bool) -> Result<()> {
1053 let mut body = String::new();
1054 if is_debug {
1055 body.push_str(&self.spelling(Kind::Value, "customString")?);
1057 body.push_str("(\"{0}\", ");
1058 self.value(value, &mut body)?;
1059 body.push(')');
1060 } else {
1061 self.value(value, &mut body)?;
1062 }
1063 let mut line = String::new();
1069 line.push_str(&self.spelling(Kind::Action, "createHudText")?);
1070 line.push('(');
1071 line.push_str(&self.spelling(Kind::Value, "allPlayers")?);
1072 line.push('(');
1073 line.push_str(&self.enum_spelling("Team", "ALL")?);
1074 line.push_str("), Null, ");
1075 line.push_str(&body);
1076 line.push_str(", Null, ");
1077 line.push_str(&self.enum_spelling("HudPosition", "LEFT")?);
1078 line.push_str(", -9999, Color(");
1079 line.push_str(&self.enum_spelling("Color", "WHITE")?);
1080 line.push_str("), Color(");
1081 line.push_str(&self.enum_spelling("Color", "WHITE")?);
1082 line.push_str("), Color(");
1083 line.push_str(&self.enum_spelling("Color", "WHITE")?);
1084 line.push_str("), ");
1085 line.push_str(&self.enum_spelling("HudReeval", "VISIBILITY_AND_STRING")?);
1086 line.push_str(", ");
1087 line.push_str(&self.enum_spelling("SpecVisibility", "VISIBLE_ALWAYS")?);
1088 line.push_str(");");
1089 self.line(level, &line)?;
1090 Ok(())
1091 }
1092
1093 fn args(&mut self, args: &[wir::ValueId], out: &mut String) -> Result<()> {
1094 for (index, arg) in args.iter().enumerate() {
1095 if index > 0 {
1096 out.push_str(", ");
1097 }
1098 self.value(*arg, out)?;
1099 }
1100 Ok(())
1101 }
1102
1103 fn value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1104 let Some(node) = self.program.values.get(id) else {
1105 return Err(WorkshopError::Malformed {
1106 message: format!("dangling value {id}"),
1107 span: None,
1108 });
1109 };
1110 match &node.value {
1111 wir::Value::Number { text, .. } => {
1112 out.push_str(text);
1116 }
1117 wir::Value::String(value) => {
1118 self.emit_string_value(value, out)?;
1125 }
1126 wir::Value::LocalizedString(id) => {
1127 out.push_str(&self.spelling(Kind::Value, "string")?);
1128 out.push('(');
1129 let spelling = self.localized_string_spelling(id)?;
1130 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1131 out.push(')');
1132 }
1133 wir::Value::Bool(true) => out.push_str("True"),
1134 wir::Value::Bool(false) => out.push_str("False"),
1135 wir::Value::Null => out.push_str("Null"),
1136 wir::Value::Array(elements) => {
1137 if elements.is_empty() {
1138 out.push_str(&self.spelling(Kind::Value, "emptyArray")?);
1140 } else {
1141 out.push_str(&self.spelling(Kind::Value, "array")?);
1142 out.push('(');
1143 self.args(elements, out)?;
1144 out.push(')');
1145 }
1146 }
1147 wir::Value::Vector { x, y, z } => {
1148 out.push_str(&self.spelling(Kind::Value, "vector")?);
1149 out.push('(');
1150 self.value(*x, out)?;
1151 out.push_str(", ");
1152 self.value(*y, out)?;
1153 out.push_str(", ");
1154 self.value(*z, out)?;
1155 out.push(')');
1156 }
1157 wir::Value::Enum { value_type, value } => {
1158 let spelling = self.enum_spelling(value_type, value)?;
1159 if matches!(value_type.as_str(), "Color" | "Map" | "Team")
1169 || value_type == "Hero"
1170 && (spelling.contains('.')
1171 || self.locale != *self.catalog.primary_locale()
1172 || (self.force_hero_constructors
1173 && self
1174 .program
1175 .global_variables
1176 .iter()
1177 .any(|variable| variable.name == spelling)))
1178 {
1179 let domain = self
1180 .catalog
1181 .enum_domain(value_type)
1182 .and_then(|entry| entry.spelling(&self.locale))
1183 .unwrap_or(value_type);
1184 write!(out, "{domain}({spelling})").unwrap();
1185 } else {
1186 out.push_str(&spelling);
1187 }
1188 }
1189 wir::Value::GlobalVariable(variable) => {
1190 let name = self.global_name(*variable)?;
1191 write!(out, "Global.{name}").unwrap();
1192 }
1193 wir::Value::PlayerVariable { player, variable } => {
1194 out.push('(');
1197 self.value(*player, out)?;
1198 out.push(')');
1199 let name = self.player_name(*variable)?;
1200 write!(out, ".{name}").unwrap();
1201 }
1202 wir::Value::Subroutine(subroutine) => {
1203 let name = self
1204 .program
1205 .subroutines
1206 .get(*subroutine)
1207 .map(|value| value.name.clone())
1208 .ok_or_else(|| WorkshopError::Malformed {
1209 message: format!("dangling subroutine value {subroutine}"),
1210 span: None,
1211 })?;
1212 out.push_str(&name);
1213 }
1214 wir::Value::EventPlayer => out.push_str(&self.spelling(Kind::Value, "eventPlayer")?),
1215 wir::Value::Call { name, args } => {
1216 if name == "memberAccess" {
1217 if args.len() < 2 || args.len() > 3 {
1218 return Err(WorkshopError::Malformed {
1219 message: "memberAccess expects two or three arguments".to_string(),
1220 span: node.span,
1221 });
1222 }
1223 let Some(wir::ValueNode {
1224 value: wir::Value::String(member),
1225 ..
1226 }) = self.program.values.get(args[1])
1227 else {
1228 return Err(WorkshopError::Malformed {
1229 message: "memberAccess member must be a string".to_string(),
1230 span: node.span,
1231 });
1232 };
1233 let bare_event_player = self
1234 .program
1235 .values
1236 .get(args[0])
1237 .is_some_and(|node| matches!(node.value, wir::Value::EventPlayer));
1238 if !bare_event_player {
1239 out.push('(');
1240 }
1241 self.value(args[0], out)?;
1242 if !bare_event_player {
1243 out.push(')');
1244 }
1245 write!(out, ".{member}").unwrap();
1246 if let Some(index) = args.get(2) {
1247 out.push('[');
1248 self.value(*index, out)?;
1249 out.push(']');
1250 }
1251 return Ok(());
1252 }
1253 if is_comparison_operator(name) {
1254 if args.len() != 2 {
1256 return Err(WorkshopError::Malformed {
1257 message: format!("comparison call '{name}' must have 2 args"),
1258 span: None,
1259 });
1260 }
1261 out.push_str(&self.spelling(Kind::Value, "compare")?);
1262 out.push('(');
1263 self.value(args[0], out)?;
1264 write!(out, ", {name}, ").unwrap();
1265 self.value(args[1], out)?;
1266 out.push(')');
1267 return Ok(());
1268 }
1269 if name == "-" && args.len() == 1 {
1272 out.push_str(&self.spelling(Kind::Value, "multiply")?);
1273 out.push_str("(-1, ");
1274 self.value(args[0], out)?;
1275 out.push(')');
1276 return Ok(());
1277 }
1278 if name == "getAllPlayers" && args.is_empty() {
1280 out.push_str(&self.spelling(Kind::Value, "allPlayers")?);
1281 out.push('(');
1282 out.push_str(&self.enum_spelling("Team", "ALL")?);
1283 out.push(')');
1284 return Ok(());
1285 }
1286 let canonical = match name.as_str() {
1291 "+" => Some("add"),
1292 "-" => Some("subtract"),
1293 "*" => Some("multiply"),
1294 "/" => Some("divide"),
1295 "len" => Some("countOf"),
1296 "abs" => Some("absoluteValue"),
1297 "sqrt" => Some("squareRoot"),
1298 "createBeam" => Some("createBeamEffect"),
1299 "random.uniform" => Some("randomReal"),
1300 "random.choice" => Some("randomValueInArray"),
1301 "format" => Some("customString"),
1302 _ => None,
1303 };
1304 let spelling = if let Some(canonical) = canonical {
1305 self.spelling(Kind::Value, canonical)?
1306 } else {
1307 self.spelling(Kind::Value, name)?
1308 };
1309 let is_custom_string = canonical == Some("customString") || name == "customString";
1312 if name == "string" {
1313 out.push_str(&spelling);
1314 out.push('(');
1315 if let Some(first) = args.first() {
1316 self.localized_string_value(*first, out)?;
1317 if args.len() > 1 {
1318 out.push_str(", ");
1319 self.args(&args[1..], out)?;
1320 }
1321 }
1322 out.push(')');
1323 } else if args.is_empty() {
1324 out.push_str(&spelling);
1326 } else if is_custom_string {
1327 match self.canonicalize_format_call(args)? {
1335 Some((text, variable_args)) => {
1336 if variable_args.is_empty() {
1337 self.emit_string_value(&text, out)?;
1338 } else {
1339 out.push_str(&spelling);
1340 out.push('(');
1341 write!(out, "\"{}\"", escape_value_string(&text)).unwrap();
1342 if !variable_args.is_empty() {
1343 out.push_str(", ");
1344 }
1345 self.args(&variable_args, out)?;
1346 out.push(')');
1347 }
1348 }
1349 None => {
1350 out.push_str(&spelling);
1354 out.push('(');
1355 self.bare_string_value(args[0], out)?;
1356 if args.len() > 1 {
1357 out.push_str(", ");
1358 }
1359 self.args(&args[1..], out)?;
1360 out.push(')');
1361 }
1362 }
1363 } else {
1364 out.push_str(&spelling);
1365 out.push('(');
1366 self.args(args, out)?;
1367 out.push(')');
1368 }
1369 }
1370 }
1371 Ok(())
1372 }
1373
1374 fn localized_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1375 let Some(node) = self.program.values.get(id) else {
1376 return Err(WorkshopError::Malformed {
1377 message: format!("dangling value {id}"),
1378 span: None,
1379 });
1380 };
1381 let wir::Value::LocalizedString(id) = &node.value else {
1382 return Err(WorkshopError::Unsupported {
1383 message: "value 'string' argument 1 must be localized string text".to_string(),
1384 span: node.span,
1385 });
1386 };
1387 let spelling = self.localized_string_spelling(id)?;
1388 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1389 Ok(())
1390 }
1391
1392 fn canonicalize_format_call(
1403 &self,
1404 args: &[wir::ValueId],
1405 ) -> Result<Option<(String, Vec<wir::ValueId>)>> {
1406 if args.len() < 2 {
1407 return Ok(None);
1408 }
1409 let Some(text) = self.program.values.get(args[0]) else {
1410 return Ok(None);
1411 };
1412 let wir::Value::String(text) = &text.value else {
1413 return Ok(None);
1414 };
1415 let format_args = &args[1..];
1416 let mut has_implicit = false;
1419 let mut has_explicit = false;
1420 let mut out_of_range = false;
1421 let mut cursor = 0usize;
1422 let mut chars = text.chars().peekable();
1423 while let Some(ch) = chars.next() {
1424 if ch == '{' {
1425 let mut inner = String::new();
1426 let mut closed = false;
1427 for next in chars.by_ref() {
1428 if next == '}' {
1429 closed = true;
1430 break;
1431 }
1432 inner.push(next);
1433 }
1434 if !closed {
1435 break; }
1437 if inner.is_empty() {
1438 if cursor >= format_args.len() {
1439 out_of_range = true;
1440 }
1441 cursor += 1;
1442 has_implicit = true;
1443 } else if inner.chars().all(|c| c.is_ascii_digit()) {
1444 match inner.parse::<usize>() {
1445 Ok(index) if index < format_args.len() => has_explicit = true,
1446 _ => out_of_range = true,
1447 }
1448 } else {
1449 out_of_range = true;
1450 }
1451 }
1452 }
1453 if out_of_range || (has_implicit && has_explicit) {
1454 return Ok(None);
1455 }
1456 let mut any_constant = false;
1457 for id in format_args {
1458 let Some(node) = self.program.values.get(*id) else {
1459 return Ok(None);
1460 };
1461 if matches!(node.value, wir::Value::Number { .. }) {
1462 any_constant = true;
1463 }
1464 }
1465 if !has_implicit && !any_constant {
1466 return Ok(None);
1467 }
1468 let mut canonical = String::with_capacity(text.len());
1472 let mut variable_args = Vec::new();
1473 let mut variable_index = 0usize;
1474 let mut cursor = 0usize;
1475 let mut chars = text.chars().peekable();
1476 while let Some(ch) = chars.next() {
1477 if ch == '{' {
1478 let mut inner = String::new();
1479 let mut closed = false;
1480 for next in chars.by_ref() {
1481 if next == '}' {
1482 closed = true;
1483 break;
1484 }
1485 inner.push(next);
1486 }
1487 if !closed {
1488 canonical.push('{');
1489 canonical.push_str(&inner);
1490 break;
1491 }
1492 let index = if inner.is_empty() {
1493 let index = cursor;
1494 cursor += 1;
1495 index
1496 } else {
1497 match inner.parse::<usize>() {
1498 Ok(index) => index,
1499 Err(_) => {
1500 canonical.push('{');
1501 canonical.push_str(&inner);
1502 canonical.push('}');
1503 continue;
1504 }
1505 }
1506 };
1507 let Some(arg) = format_args.get(index).copied() else {
1508 canonical.push('{');
1509 canonical.push_str(&inner);
1510 canonical.push('}');
1511 continue;
1512 };
1513 let node = self.program.values.get(arg);
1514 if let Some(wir::Value::Number { value, .. }) = node.map(|node| &node.value) {
1515 canonical.push_str(&fold_number(*value));
1516 } else {
1517 write!(canonical, "{{{variable_index}}}").unwrap();
1518 variable_index += 1;
1519 variable_args.push(arg);
1520 }
1521 } else {
1522 canonical.push(ch);
1523 }
1524 }
1525 Ok(Some((canonical, variable_args)))
1526 }
1527
1528 fn modify_op_spelling(&mut self, op: wir::ModifyOp) -> Result<String> {
1531 self.spelling(Kind::Operator, op.catalog_id())
1532 }
1533
1534 fn spelling(&mut self, kind: Kind, id: &str) -> Result<String> {
1539 let Some(entry) = self.catalog.entry(kind, id) else {
1540 return Err(WorkshopError::Unknown {
1541 kind: kind.as_str(),
1542 spelling: id.to_string(),
1543 locale: self.locale.clone(),
1544 span: None,
1545 });
1546 };
1547 if let Some(spelling) = entry.spelling(&self.locale) {
1548 return Ok(spelling.to_string());
1549 }
1550 if let Some(fallback) = &self.fallback {
1551 if let Some(spelling) = entry.spelling(fallback) {
1552 self.fallback_ids.push(id.to_string());
1553 return Ok(spelling.to_string());
1554 }
1555 }
1556 Err(WorkshopError::MissingMapping {
1557 kind: kind.as_str(),
1558 id: id.to_string(),
1559 locale: self.locale.clone(),
1560 })
1561 }
1562
1563 fn localized_string_spelling(&mut self, id: &str) -> Result<String> {
1564 if let Some(spelling) = self.catalog.localized_string_spelling(&self.locale, id) {
1565 return Ok(spelling.to_string());
1566 }
1567 if let Some(fallback) = &self.fallback {
1568 if let Some(spelling) = self.catalog.localized_string_spelling(fallback, id) {
1569 self.fallback_ids.push(format!("localizedString.{id}"));
1570 return Ok(spelling.to_string());
1571 }
1572 }
1573 if self.catalog.localized_strings().any(|entry| entry.id == id) {
1574 return Err(WorkshopError::MissingMapping {
1575 kind: "localized string",
1576 id: id.to_string(),
1577 locale: self.locale.clone(),
1578 });
1579 }
1580 Err(WorkshopError::Unknown {
1581 kind: "localized string",
1582 spelling: id.to_string(),
1583 locale: self.locale.clone(),
1584 span: None,
1585 })
1586 }
1587
1588 fn structural(&mut self, id: &str) -> Result<String> {
1589 self.spelling(Kind::Structural, id)
1590 }
1591
1592 fn enum_spelling(&mut self, domain: &str, member: &str) -> Result<String> {
1595 let Some(domain_entry) = self.catalog.enum_domain(domain) else {
1596 return Err(WorkshopError::Unknown {
1597 kind: "enum domain",
1598 spelling: domain.to_string(),
1599 locale: self.locale.clone(),
1600 span: None,
1601 });
1602 };
1603 let Some(member_entry) = domain_entry.members.iter().find(|m| m.member == member) else {
1604 return Err(WorkshopError::Unknown {
1605 kind: "enum member",
1606 spelling: format!("{domain}.{member}"),
1607 locale: self.locale.clone(),
1608 span: None,
1609 });
1610 };
1611 if let Some(spelling) = member_entry.spelling(&self.locale) {
1612 return Ok(spelling.to_string());
1613 }
1614 if let Some(fallback) = &self.fallback {
1615 if let Some(spelling) = member_entry.spelling(fallback) {
1616 self.fallback_ids.push(format!("{domain}.{member}"));
1617 return Ok(spelling.to_string());
1618 }
1619 }
1620 Err(WorkshopError::MissingMapping {
1621 kind: "enum member",
1622 id: format!("{domain}.{member}"),
1623 locale: self.locale.clone(),
1624 })
1625 }
1626
1627 fn bare_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1630 let Some(node) = self.program.values.get(id) else {
1631 return Err(WorkshopError::Malformed {
1632 message: format!("dangling value {id}"),
1633 span: None,
1634 });
1635 };
1636 if let wir::Value::String(value) = &node.value {
1637 write!(out, "\"{}\"", escape_value_string(value)).unwrap();
1638 return Ok(());
1639 }
1640 self.value(id, out)
1641 }
1642
1643 fn emit_string_value(&mut self, value: &str, out: &mut String) -> Result<()> {
1646 let spelling = self.spelling(Kind::Value, "customString")?;
1647 let segments = split_string(value);
1648 emit_string_chain(&spelling, &segments, out);
1649 Ok(())
1650 }
1651
1652 fn global_name(&self, id: wir::GlobalVarId) -> Result<String> {
1653 self.program
1654 .global_variables
1655 .get(id)
1656 .map(|variable| variable.name.clone())
1657 .ok_or_else(|| WorkshopError::Unknown {
1658 kind: "global variable",
1659 spelling: format!("<{id}>"),
1660 locale: self.locale.clone(),
1661 span: None,
1662 })
1663 }
1664
1665 fn player_name(&self, id: wir::PlayerVarId) -> Result<String> {
1666 self.program
1667 .player_variables
1668 .get(id)
1669 .map(|variable| variable.name.clone())
1670 .ok_or_else(|| WorkshopError::Unknown {
1671 kind: "player variable",
1672 spelling: format!("<{id}>"),
1673 locale: self.locale.clone(),
1674 span: None,
1675 })
1676 }
1677
1678 fn line(&mut self, level: usize, text: &str) -> Result<()> {
1679 for _ in 0..level {
1680 self.out.push_str(" ");
1681 }
1682 self.out.push_str(text);
1683 self.out.push('\n');
1684 self.line_count += 1;
1685 Ok(())
1686 }
1687}
1688
1689fn is_comparison_operator(name: &str) -> bool {
1694 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
1695}
1696
1697fn escape_string(value: &str) -> String {
1698 value.replace('"', "\\\"")
1699}
1700
1701fn escape_value_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 '\r' => out.push_str("\\r"),
1713 other => out.push(other),
1714 }
1715 }
1716 out
1717}
1718
1719fn split_string(value: &str) -> Vec<String> {
1728 if value.chars().count() <= 128 {
1729 return vec![escape_value_string(value)];
1730 }
1731 let mut segments = Vec::new();
1732 let mut rest = value;
1733 while rest.chars().count() > 125 {
1734 let chunk: String = rest.chars().take(125).collect();
1735 let mut text = escape_value_string(&chunk);
1736 text.push_str("{0}");
1737 segments.push(text);
1738 rest = &rest[chunk.len()..];
1739 }
1740 if !rest.is_empty() {
1741 segments.push(escape_value_string(rest));
1742 }
1743 segments
1744}
1745
1746fn escape_settings_string(value: &str) -> String {
1752 let mut out = String::with_capacity(value.len());
1753 for ch in value.chars() {
1754 match ch {
1755 '\\' => out.push_str("\\\\"),
1756 '"' => out.push_str("\\\""),
1757 '\n' => out.push_str("\\n"),
1758 '\t' => out.push_str("\\t"),
1759 '\r' => out.push_str("\\r"),
1760 other => out.push(other),
1761 }
1762 }
1763 out
1764}
1765
1766fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
1772 let Some((first, rest)) = segments.split_first() else {
1773 return;
1774 };
1775 out.push_str(spelling);
1776 out.push('(');
1777 write!(out, "\"{first}\"").unwrap();
1778 for segment in rest {
1779 out.push_str(", ");
1780 out.push_str(spelling);
1781 out.push('(');
1782 write!(out, "\"{segment}\"").unwrap();
1783 }
1784 for _ in 0..=rest.len() {
1785 out.push(')');
1786 }
1787}
1788
1789fn fold_number(value: f64) -> String {
1793 if value.fract() == 0.0 && value.abs() < 1e15 {
1794 format!("{}", value as i64)
1795 } else {
1796 let scaled = (value * 100.0).round();
1797 let sign = if scaled < 0.0 { "-" } else { "" };
1798 let scaled = scaled.abs() as i64;
1799 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
1800 }
1801}