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::AssignMember {
877 target, op, value, ..
878 } => {
879 let mut target_text = String::new();
880 let mut value_text = String::new();
881 self.value(*target, &mut target_text)?;
882 self.value(*value, &mut value_text)?;
883 let operator = match op {
884 None => "=".to_string(),
885 Some(op) => {
886 let token = match op {
887 wir::ModifyOp::Add => "+",
888 wir::ModifyOp::Subtract => "-",
889 wir::ModifyOp::Multiply => "*",
890 wir::ModifyOp::Divide => "/",
891 wir::ModifyOp::Modulo => "%",
892 wir::ModifyOp::Min => "min",
893 wir::ModifyOp::Max => "max",
894 _ => {
895 return Err(WorkshopError::Unsupported {
896 message: format!(
897 "unsupported member assignment operator {op:?}"
898 ),
899 span: None,
900 });
901 }
902 };
903 format!("{token}=")
904 }
905 };
906 self.line(level, &format!("{target_text} {operator} {value_text};"))?;
907 }
908 wir::Action::Call { name, args, .. } => {
909 if matches!(name.as_str(), "chaseAtRate" | "chaseOverTime") {
917 let player_var = args.first().and_then(|id| {
918 self.program
919 .values
920 .get(*id)
921 .and_then(|node| match &node.value {
922 wir::Value::PlayerVariable { player, variable } => {
923 Some((*player, *variable))
924 }
925 _ => None,
926 })
927 });
928 let spelling = if let Some((player, variable)) = player_var {
929 let id = if name == "chaseAtRate" {
930 "chasePlayerVariableAtRate"
931 } else {
932 "chasePlayerVariableOverTime"
933 };
934 let spelling = self.spelling(Kind::Action, id)?;
935 let mut text = String::new();
939 self.value(player, &mut text)?;
940 let mut parts = vec![text, self.player_name(variable)?];
941 for arg in args.iter().skip(1) {
942 let mut part = String::new();
943 self.value(*arg, &mut part)?;
944 parts.push(part);
945 }
946 return self.line(level, &format!("{spelling}({});", parts.join(", ")));
947 } else {
948 self.spelling(Kind::Action, name)?
949 };
950 let mut args_text = String::new();
951 self.args(args, &mut args_text)?;
952 return self.line(level, &format!("{spelling}({args_text});"));
953 }
954 if name == "stopChasingPlayerVariable" {
955 let Some((player, variable)) = args.first().and_then(|id| {
956 self.program
957 .values
958 .get(*id)
959 .and_then(|node| match &node.value {
960 wir::Value::PlayerVariable { player, variable } => {
961 Some((*player, *variable))
962 }
963 _ => None,
964 })
965 }) else {
966 return Err(WorkshopError::Malformed {
967 message: "Stop Chasing Player Variable requires a player variable"
968 .into(),
969 span: None,
970 });
971 };
972 let spelling = self.spelling(Kind::Action, name)?;
973 let mut player_text = String::new();
974 self.value(player, &mut player_text)?;
975 return self.line(
976 level,
977 &format!(
978 "{spelling}({player_text}, {});",
979 self.player_name(variable)?
980 ),
981 );
982 }
983 let canonical = match name.as_str() {
986 "createBeam" => Some("createBeamEffect"),
987 _ => None,
988 };
989 let spelling = if let Some(canonical) = canonical {
990 self.spelling(Kind::Action, canonical)?
991 } else {
992 self.spelling(Kind::Action, name)?
993 };
994 if args.is_empty() {
995 self.line(level, &format!("{spelling};"))?;
996 } else {
997 let mut args_text = String::new();
998 for (index, arg) in args.iter().enumerate() {
999 if index > 0 {
1000 args_text.push_str(", ");
1001 }
1002 let variable_position = match name.as_str() {
1003 "setGlobalVariableAtIndex" | "modifyGlobalVariableAtIndex" => {
1004 index == 0
1005 }
1006 "setPlayerVariableAtIndex" | "modifyPlayerVariableAtIndex" => {
1007 index == 1
1008 }
1009 _ => false,
1010 };
1011 if variable_position {
1012 if let Some(node) = self.program.values.get(*arg) {
1013 match &node.value {
1014 wir::Value::GlobalVariable(variable) => {
1015 args_text.push_str(&self.global_name(*variable)?);
1016 continue;
1017 }
1018 wir::Value::PlayerVariable { variable, .. } => {
1019 args_text.push_str(&self.player_name(*variable)?);
1020 continue;
1021 }
1022 _ => {}
1023 }
1024 }
1025 }
1026 self.value(*arg, &mut args_text)?;
1027 }
1028 self.line(level, &format!("{spelling}({args_text});"))?;
1029 }
1030 }
1031 }
1032 Ok(())
1033 }
1034
1035 fn args(&mut self, args: &[wir::ValueId], out: &mut String) -> Result<()> {
1036 for (index, arg) in args.iter().enumerate() {
1037 if index > 0 {
1038 out.push_str(", ");
1039 }
1040 self.value(*arg, out)?;
1041 }
1042 Ok(())
1043 }
1044
1045 fn value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1046 let Some(node) = self.program.values.get(id) else {
1047 return Err(WorkshopError::Malformed {
1048 message: format!("dangling value {id}"),
1049 span: None,
1050 });
1051 };
1052 match &node.value {
1053 wir::Value::Number { text, .. } => {
1054 out.push_str(text);
1058 }
1059 wir::Value::String(value) => {
1060 self.emit_string_value(value, out)?;
1067 }
1068 wir::Value::LocalizedString(id) => {
1069 out.push_str(&self.spelling(Kind::Value, "string")?);
1070 out.push('(');
1071 let spelling = self.localized_string_spelling(id)?;
1072 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1073 out.push(')');
1074 }
1075 wir::Value::Bool(true) => out.push_str("True"),
1076 wir::Value::Bool(false) => out.push_str("False"),
1077 wir::Value::Null => out.push_str("Null"),
1078 wir::Value::Array(elements) => {
1079 if elements.is_empty() {
1080 out.push_str(&self.spelling(Kind::Value, "emptyArray")?);
1082 } else {
1083 out.push_str(&self.spelling(Kind::Value, "array")?);
1084 out.push('(');
1085 self.args(elements, out)?;
1086 out.push(')');
1087 }
1088 }
1089 wir::Value::Vector { x, y, z } => {
1090 out.push_str(&self.spelling(Kind::Value, "vector")?);
1091 out.push('(');
1092 self.value(*x, out)?;
1093 out.push_str(", ");
1094 self.value(*y, out)?;
1095 out.push_str(", ");
1096 self.value(*z, out)?;
1097 out.push(')');
1098 }
1099 wir::Value::Enum { value_type, value } => {
1100 let spelling = self.enum_spelling(value_type, value)?;
1101 if matches!(value_type.as_str(), "Color" | "Map" | "Team")
1111 || value_type == "Hero"
1112 && (spelling.contains('.')
1113 || self.locale != *self.catalog.primary_locale()
1114 || (self.force_hero_constructors
1115 && self
1116 .program
1117 .global_variables
1118 .iter()
1119 .any(|variable| variable.name == spelling)))
1120 {
1121 let domain = self
1122 .catalog
1123 .enum_domain(value_type)
1124 .and_then(|entry| entry.spelling(&self.locale))
1125 .unwrap_or(value_type);
1126 write!(out, "{domain}({spelling})").unwrap();
1127 } else {
1128 out.push_str(&spelling);
1129 }
1130 }
1131 wir::Value::GlobalVariable(variable) => {
1132 let name = self.global_name(*variable)?;
1133 write!(out, "Global.{name}").unwrap();
1134 }
1135 wir::Value::PlayerVariable { player, variable } => {
1136 out.push('(');
1139 self.value(*player, out)?;
1140 out.push(')');
1141 let name = self.player_name(*variable)?;
1142 write!(out, ".{name}").unwrap();
1143 }
1144 wir::Value::Subroutine(subroutine) => {
1145 let name = self
1146 .program
1147 .subroutines
1148 .get(*subroutine)
1149 .map(|value| value.name.clone())
1150 .ok_or_else(|| WorkshopError::Malformed {
1151 message: format!("dangling subroutine value {subroutine}"),
1152 span: None,
1153 })?;
1154 out.push_str(&name);
1155 }
1156 wir::Value::EventPlayer => out.push_str(&self.spelling(Kind::Value, "eventPlayer")?),
1157 wir::Value::Call { name, args } => {
1158 if name == "memberAccess" {
1159 if args.len() < 2 || args.len() > 3 {
1160 return Err(WorkshopError::Malformed {
1161 message: "memberAccess expects two or three arguments".to_string(),
1162 span: node.span,
1163 });
1164 }
1165 let Some(wir::ValueNode {
1166 value: wir::Value::String(member),
1167 ..
1168 }) = self.program.values.get(args[1])
1169 else {
1170 return Err(WorkshopError::Malformed {
1171 message: "memberAccess member must be a string".to_string(),
1172 span: node.span,
1173 });
1174 };
1175 let bare_event_player = self
1176 .program
1177 .values
1178 .get(args[0])
1179 .is_some_and(|node| matches!(node.value, wir::Value::EventPlayer));
1180 if !bare_event_player {
1181 out.push('(');
1182 }
1183 self.value(args[0], out)?;
1184 if !bare_event_player {
1185 out.push(')');
1186 }
1187 write!(out, ".{member}").unwrap();
1188 if let Some(index) = args.get(2) {
1189 out.push('[');
1190 self.value(*index, out)?;
1191 out.push(']');
1192 }
1193 return Ok(());
1194 }
1195 if is_comparison_operator(name) {
1196 if args.len() != 2 {
1198 return Err(WorkshopError::Malformed {
1199 message: format!("comparison call '{name}' must have 2 args"),
1200 span: None,
1201 });
1202 }
1203 out.push_str(&self.spelling(Kind::Value, "compare")?);
1204 out.push('(');
1205 self.value(args[0], out)?;
1206 write!(out, ", {name}, ").unwrap();
1207 self.value(args[1], out)?;
1208 out.push(')');
1209 return Ok(());
1210 }
1211 if name == "-" && args.len() == 1 {
1214 out.push_str(&self.spelling(Kind::Value, "multiply")?);
1215 out.push_str("(-1, ");
1216 self.value(args[0], out)?;
1217 out.push(')');
1218 return Ok(());
1219 }
1220 if name == "getAllPlayers" && args.is_empty() {
1222 out.push_str(&self.spelling(Kind::Value, "allPlayers")?);
1223 out.push('(');
1224 out.push_str(&self.enum_spelling("Team", "ALL")?);
1225 out.push(')');
1226 return Ok(());
1227 }
1228 let canonical = match name.as_str() {
1233 "+" => Some("add"),
1234 "-" => Some("subtract"),
1235 "*" => Some("multiply"),
1236 "/" => Some("divide"),
1237 "len" => Some("countOf"),
1238 "abs" => Some("absoluteValue"),
1239 "sqrt" => Some("squareRoot"),
1240 "createBeam" => Some("createBeamEffect"),
1241 "random.uniform" => Some("randomReal"),
1242 "random.choice" => Some("randomValueInArray"),
1243 "format" => Some("customString"),
1244 _ => None,
1245 };
1246 let spelling = if let Some(canonical) = canonical {
1247 self.spelling(Kind::Value, canonical)?
1248 } else {
1249 self.spelling(Kind::Value, name)?
1250 };
1251 let is_custom_string = canonical == Some("customString") || name == "customString";
1254 if name == "string" {
1255 out.push_str(&spelling);
1256 out.push('(');
1257 if let Some(first) = args.first() {
1258 self.localized_string_value(*first, out)?;
1259 if args.len() > 1 {
1260 out.push_str(", ");
1261 self.args(&args[1..], out)?;
1262 }
1263 }
1264 out.push(')');
1265 } else if args.is_empty() {
1266 out.push_str(&spelling);
1268 } else if is_custom_string {
1269 match self.canonicalize_format_call(args)? {
1277 Some((text, variable_args)) => {
1278 if variable_args.is_empty() {
1279 self.emit_string_value(&text, out)?;
1280 } else {
1281 out.push_str(&spelling);
1282 out.push('(');
1283 write!(out, "\"{}\"", escape_value_string(&text)).unwrap();
1284 if !variable_args.is_empty() {
1285 out.push_str(", ");
1286 }
1287 self.args(&variable_args, out)?;
1288 out.push(')');
1289 }
1290 }
1291 None => {
1292 out.push_str(&spelling);
1296 out.push('(');
1297 self.bare_string_value(args[0], out)?;
1298 if args.len() > 1 {
1299 out.push_str(", ");
1300 }
1301 self.args(&args[1..], out)?;
1302 out.push(')');
1303 }
1304 }
1305 } else {
1306 out.push_str(&spelling);
1307 out.push('(');
1308 self.args(args, out)?;
1309 out.push(')');
1310 }
1311 }
1312 }
1313 Ok(())
1314 }
1315
1316 fn localized_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1317 let Some(node) = self.program.values.get(id) else {
1318 return Err(WorkshopError::Malformed {
1319 message: format!("dangling value {id}"),
1320 span: None,
1321 });
1322 };
1323 let wir::Value::LocalizedString(id) = &node.value else {
1324 return Err(WorkshopError::Unsupported {
1325 message: "value 'string' argument 1 must be localized string text".to_string(),
1326 span: node.span,
1327 });
1328 };
1329 let spelling = self.localized_string_spelling(id)?;
1330 write!(out, "\"{}\"", escape_value_string(&spelling)).unwrap();
1331 Ok(())
1332 }
1333
1334 fn canonicalize_format_call(
1345 &self,
1346 args: &[wir::ValueId],
1347 ) -> Result<Option<(String, Vec<wir::ValueId>)>> {
1348 if args.len() < 2 {
1349 return Ok(None);
1350 }
1351 let Some(text) = self.program.values.get(args[0]) else {
1352 return Ok(None);
1353 };
1354 let wir::Value::String(text) = &text.value else {
1355 return Ok(None);
1356 };
1357 let format_args = &args[1..];
1358 let mut has_implicit = false;
1361 let mut has_explicit = false;
1362 let mut out_of_range = false;
1363 let mut cursor = 0usize;
1364 let mut chars = text.chars().peekable();
1365 while let Some(ch) = chars.next() {
1366 if ch == '{' {
1367 let mut inner = String::new();
1368 let mut closed = false;
1369 for next in chars.by_ref() {
1370 if next == '}' {
1371 closed = true;
1372 break;
1373 }
1374 inner.push(next);
1375 }
1376 if !closed {
1377 break; }
1379 if inner.is_empty() {
1380 if cursor >= format_args.len() {
1381 out_of_range = true;
1382 }
1383 cursor += 1;
1384 has_implicit = true;
1385 } else if inner.chars().all(|c| c.is_ascii_digit()) {
1386 match inner.parse::<usize>() {
1387 Ok(index) if index < format_args.len() => has_explicit = true,
1388 _ => out_of_range = true,
1389 }
1390 } else {
1391 out_of_range = true;
1392 }
1393 }
1394 }
1395 if out_of_range || (has_implicit && has_explicit) {
1396 return Ok(None);
1397 }
1398 let mut any_constant = false;
1399 for id in format_args {
1400 let Some(node) = self.program.values.get(*id) else {
1401 return Ok(None);
1402 };
1403 if matches!(node.value, wir::Value::Number { .. }) {
1404 any_constant = true;
1405 }
1406 }
1407 if !has_implicit && !any_constant {
1408 return Ok(None);
1409 }
1410 let mut canonical = String::with_capacity(text.len());
1414 let mut variable_args = Vec::new();
1415 let mut variable_index = 0usize;
1416 let mut cursor = 0usize;
1417 let mut chars = text.chars().peekable();
1418 while let Some(ch) = chars.next() {
1419 if ch == '{' {
1420 let mut inner = String::new();
1421 let mut closed = false;
1422 for next in chars.by_ref() {
1423 if next == '}' {
1424 closed = true;
1425 break;
1426 }
1427 inner.push(next);
1428 }
1429 if !closed {
1430 canonical.push('{');
1431 canonical.push_str(&inner);
1432 break;
1433 }
1434 let index = if inner.is_empty() {
1435 let index = cursor;
1436 cursor += 1;
1437 index
1438 } else {
1439 match inner.parse::<usize>() {
1440 Ok(index) => index,
1441 Err(_) => {
1442 canonical.push('{');
1443 canonical.push_str(&inner);
1444 canonical.push('}');
1445 continue;
1446 }
1447 }
1448 };
1449 let Some(arg) = format_args.get(index).copied() else {
1450 canonical.push('{');
1451 canonical.push_str(&inner);
1452 canonical.push('}');
1453 continue;
1454 };
1455 let node = self.program.values.get(arg);
1456 if let Some(wir::Value::Number { value, .. }) = node.map(|node| &node.value) {
1457 canonical.push_str(&fold_number(*value));
1458 } else {
1459 write!(canonical, "{{{variable_index}}}").unwrap();
1460 variable_index += 1;
1461 variable_args.push(arg);
1462 }
1463 } else {
1464 canonical.push(ch);
1465 }
1466 }
1467 Ok(Some((canonical, variable_args)))
1468 }
1469
1470 fn modify_op_spelling(&mut self, op: wir::ModifyOp) -> Result<String> {
1473 self.spelling(Kind::Operator, op.catalog_id())
1474 }
1475
1476 fn spelling(&mut self, kind: Kind, id: &str) -> Result<String> {
1481 let Some(entry) = self.catalog.entry(kind, id) else {
1482 return Err(WorkshopError::Unknown {
1483 kind: kind.as_str(),
1484 spelling: id.to_string(),
1485 locale: self.locale.clone(),
1486 span: None,
1487 });
1488 };
1489 if let Some(spelling) = entry.spelling(&self.locale) {
1490 return Ok(spelling.to_string());
1491 }
1492 if let Some(fallback) = &self.fallback {
1493 if let Some(spelling) = entry.spelling(fallback) {
1494 self.fallback_ids.push(id.to_string());
1495 return Ok(spelling.to_string());
1496 }
1497 }
1498 Err(WorkshopError::MissingMapping {
1499 kind: kind.as_str(),
1500 id: id.to_string(),
1501 locale: self.locale.clone(),
1502 })
1503 }
1504
1505 fn localized_string_spelling(&mut self, id: &str) -> Result<String> {
1506 if let Some(spelling) = self.catalog.localized_string_spelling(&self.locale, id) {
1507 return Ok(spelling.to_string());
1508 }
1509 if let Some(fallback) = &self.fallback {
1510 if let Some(spelling) = self.catalog.localized_string_spelling(fallback, id) {
1511 self.fallback_ids.push(format!("localizedString.{id}"));
1512 return Ok(spelling.to_string());
1513 }
1514 }
1515 if self.catalog.localized_strings().any(|entry| entry.id == id) {
1516 return Err(WorkshopError::MissingMapping {
1517 kind: "localized string",
1518 id: id.to_string(),
1519 locale: self.locale.clone(),
1520 });
1521 }
1522 Err(WorkshopError::Unknown {
1523 kind: "localized string",
1524 spelling: id.to_string(),
1525 locale: self.locale.clone(),
1526 span: None,
1527 })
1528 }
1529
1530 fn structural(&mut self, id: &str) -> Result<String> {
1531 self.spelling(Kind::Structural, id)
1532 }
1533
1534 fn enum_spelling(&mut self, domain: &str, member: &str) -> Result<String> {
1537 let Some(domain_entry) = self.catalog.enum_domain(domain) else {
1538 return Err(WorkshopError::Unknown {
1539 kind: "enum domain",
1540 spelling: domain.to_string(),
1541 locale: self.locale.clone(),
1542 span: None,
1543 });
1544 };
1545 let Some(member_entry) = domain_entry.members.iter().find(|m| m.member == member) else {
1546 return Err(WorkshopError::Unknown {
1547 kind: "enum member",
1548 spelling: format!("{domain}.{member}"),
1549 locale: self.locale.clone(),
1550 span: None,
1551 });
1552 };
1553 if let Some(spelling) = member_entry.spelling(&self.locale) {
1554 return Ok(spelling.to_string());
1555 }
1556 if let Some(fallback) = &self.fallback {
1557 if let Some(spelling) = member_entry.spelling(fallback) {
1558 self.fallback_ids.push(format!("{domain}.{member}"));
1559 return Ok(spelling.to_string());
1560 }
1561 }
1562 Err(WorkshopError::MissingMapping {
1563 kind: "enum member",
1564 id: format!("{domain}.{member}"),
1565 locale: self.locale.clone(),
1566 })
1567 }
1568
1569 fn bare_string_value(&mut self, id: wir::ValueId, out: &mut String) -> Result<()> {
1572 let Some(node) = self.program.values.get(id) else {
1573 return Err(WorkshopError::Malformed {
1574 message: format!("dangling value {id}"),
1575 span: None,
1576 });
1577 };
1578 if let wir::Value::String(value) = &node.value {
1579 write!(out, "\"{}\"", escape_value_string(value)).unwrap();
1580 return Ok(());
1581 }
1582 self.value(id, out)
1583 }
1584
1585 fn emit_string_value(&mut self, value: &str, out: &mut String) -> Result<()> {
1588 let spelling = self.spelling(Kind::Value, "customString")?;
1589 let segments = split_string(value);
1590 emit_string_chain(&spelling, &segments, out);
1591 Ok(())
1592 }
1593
1594 fn global_name(&self, id: wir::GlobalVarId) -> Result<String> {
1595 self.program
1596 .global_variables
1597 .get(id)
1598 .map(|variable| variable.name.clone())
1599 .ok_or_else(|| WorkshopError::Unknown {
1600 kind: "global variable",
1601 spelling: format!("<{id}>"),
1602 locale: self.locale.clone(),
1603 span: None,
1604 })
1605 }
1606
1607 fn player_name(&self, id: wir::PlayerVarId) -> Result<String> {
1608 self.program
1609 .player_variables
1610 .get(id)
1611 .map(|variable| variable.name.clone())
1612 .ok_or_else(|| WorkshopError::Unknown {
1613 kind: "player variable",
1614 spelling: format!("<{id}>"),
1615 locale: self.locale.clone(),
1616 span: None,
1617 })
1618 }
1619
1620 fn line(&mut self, level: usize, text: &str) -> Result<()> {
1621 for _ in 0..level {
1622 self.out.push_str(" ");
1623 }
1624 self.out.push_str(text);
1625 self.out.push('\n');
1626 self.line_count += 1;
1627 Ok(())
1628 }
1629}
1630
1631fn is_comparison_operator(name: &str) -> bool {
1636 matches!(name, "==" | "!=" | "<" | "<=" | ">" | ">=")
1637}
1638
1639fn escape_string(value: &str) -> String {
1640 value.replace('"', "\\\"")
1641}
1642
1643fn escape_value_string(value: &str) -> String {
1648 let mut out = String::with_capacity(value.len());
1649 for ch in value.chars() {
1650 match ch {
1651 '\\' => out.push_str("\\\\"),
1652 '"' => out.push_str("\\\""),
1653 '\n' => out.push_str("\\n"),
1654 '\r' => out.push_str("\\r"),
1655 other => out.push(other),
1656 }
1657 }
1658 out
1659}
1660
1661fn split_string(value: &str) -> Vec<String> {
1670 if value.chars().count() <= 128 {
1671 return vec![escape_value_string(value)];
1672 }
1673 let mut segments = Vec::new();
1674 let mut rest = value;
1675 while rest.chars().count() > 125 {
1676 let chunk: String = rest.chars().take(125).collect();
1677 let mut text = escape_value_string(&chunk);
1678 text.push_str("{0}");
1679 segments.push(text);
1680 rest = &rest[chunk.len()..];
1681 }
1682 if !rest.is_empty() {
1683 segments.push(escape_value_string(rest));
1684 }
1685 segments
1686}
1687
1688fn escape_settings_string(value: &str) -> String {
1694 let mut out = String::with_capacity(value.len());
1695 for ch in value.chars() {
1696 match ch {
1697 '\\' => out.push_str("\\\\"),
1698 '"' => out.push_str("\\\""),
1699 '\n' => out.push_str("\\n"),
1700 '\t' => out.push_str("\\t"),
1701 '\r' => out.push_str("\\r"),
1702 other => out.push(other),
1703 }
1704 }
1705 out
1706}
1707
1708fn emit_string_chain(spelling: &str, segments: &[String], out: &mut String) {
1714 let Some((first, rest)) = segments.split_first() else {
1715 return;
1716 };
1717 out.push_str(spelling);
1718 out.push('(');
1719 write!(out, "\"{first}\"").unwrap();
1720 for segment in rest {
1721 out.push_str(", ");
1722 out.push_str(spelling);
1723 out.push('(');
1724 write!(out, "\"{segment}\"").unwrap();
1725 }
1726 for _ in 0..=rest.len() {
1727 out.push(')');
1728 }
1729}
1730
1731fn fold_number(value: f64) -> String {
1735 if value.fract() == 0.0 && value.abs() < 1e15 {
1736 format!("{}", value as i64)
1737 } else {
1738 let scaled = (value * 100.0).round();
1739 let sign = if scaled < 0.0 { "-" } else { "" };
1740 let scaled = scaled.abs() as i64;
1741 format!("{sign}{}.{:02}", scaled / 100, scaled % 100)
1742 }
1743}