1use serde_json::{Map, Value as JsonValue, json};
20
21use crate::core::error::{Result, WorkshopError};
22use crate::program::{Action, Condition, Event, EventTarget, EventTeam, ModifyOp, Rule, Value};
23use crate::wir::AMBIGUOUS_ENUM_CALL;
24
25pub const RULE_CONTENT_V1: &str = "workshop-rs/rule-content-v1";
27
28pub const RULE_CONTENT_V1_SCHEMA: &str = include_str!("../../schemas/rule-content-v1.schema.json");
30
31pub const GLOBAL_VARIABLE_CALL: &str = "globalVariable";
33pub const PLAYER_VARIABLE_CALL: &str = "playerVariable";
35pub const ASSIGN_MEMBER_CALL: &str = "assignMember";
37pub const MODIFY_MEMBER_CALL: &str = "modifyMember";
39pub const MEMBER_ACCESS_CALL: &str = "memberAccess";
43pub const RAW_WORKSHOP_ACTION_CALL: &str = "rawWorkshopAction";
46
47impl Rule {
48 pub fn content(&self) -> Result<JsonValue> {
57 rule_content(self)
58 }
59}
60
61pub fn rule_content(rule: &Rule) -> Result<JsonValue> {
63 let mut document = Map::new();
64 document.insert("format".to_string(), RULE_CONTENT_V1.into());
65 document.insert("event".to_string(), event_node(&rule.event));
66 document.insert("disabled".to_string(), rule.disabled.into());
67 document.insert(
68 "conditions".to_string(),
69 rule.conditions
70 .iter()
71 .map(condition_node)
72 .collect::<Result<_>>()?,
73 );
74 document.insert(
75 "actions".to_string(),
76 rule.actions
77 .iter()
78 .map(|action| action_node(action, false))
79 .collect::<Result<_>>()?,
80 );
81 Ok(JsonValue::Object(document))
82}
83
84fn event_node(event: &Event) -> JsonValue {
85 let mut node = Map::new();
86 node.insert("id".to_string(), event.catalog_id().into());
87 match event {
88 Event::EachPlayerWithFilters { team, target } | Event::Player { team, target, .. } => {
89 node.insert("team".to_string(), team_id(*team).into());
90 node.insert("player".to_string(), target_id(target).into());
91 }
92 Event::Subroutine(name) => {
93 node.insert("subroutine".to_string(), name.clone().into());
94 }
95 Event::Global | Event::EachPlayer => {}
96 }
97 JsonValue::Object(node)
98}
99
100fn team_id(team: EventTeam) -> &'static str {
102 match team {
103 EventTeam::All => "ALL",
104 EventTeam::Team1 => "TEAM_1",
105 EventTeam::Team2 => "TEAM_2",
106 }
107}
108
109fn target_id(target: &EventTarget) -> String {
112 match target {
113 EventTarget::All => "ALL".to_string(),
114 EventTarget::Slot(slot) => format!("SLOT_{slot}"),
115 EventTarget::Hero(hero) => hero.clone(),
116 }
117}
118
119fn condition_node(condition: &Condition) -> Result<JsonValue> {
120 let mut node = Map::new();
121 node.insert("value".to_string(), value_node(&condition.value)?);
122 if condition.disabled {
123 node.insert("disabled".to_string(), true.into());
124 }
125 Ok(JsonValue::Object(node))
126}
127
128fn action_node(action: &Action, disabled: bool) -> Result<JsonValue> {
129 let (call, args) = match action {
130 Action::SetGlobalVariable { variable, value } => (
131 "setGlobalVariable",
132 vec![variable_node(variable), value_node(value)?],
133 ),
134 Action::ModifyGlobalVariable {
135 variable,
136 op,
137 value,
138 } => (
139 "modifyGlobalVariable",
140 vec![
141 variable_node(variable),
142 modify_op_node(*op),
143 value_node(value)?,
144 ],
145 ),
146 Action::SetPlayerVariable {
147 player,
148 variable,
149 value,
150 } => (
151 "setPlayerVariable",
152 vec![
153 value_node(player)?,
154 variable_node(variable),
155 value_node(value)?,
156 ],
157 ),
158 Action::ModifyPlayerVariable {
159 player,
160 variable,
161 op,
162 value,
163 } => (
164 "modifyPlayerVariable",
165 vec![
166 value_node(player)?,
167 variable_node(variable),
168 modify_op_node(*op),
169 value_node(value)?,
170 ],
171 ),
172 Action::AssignMember { target, op, value } => {
173 let args = match op {
174 None => vec![value_node(target)?, value_node(value)?],
175 Some(op) => vec![value_node(target)?, modify_op_node(*op), value_node(value)?],
176 };
177 (
178 if op.is_some() {
179 MODIFY_MEMBER_CALL
180 } else {
181 ASSIGN_MEMBER_CALL
182 },
183 args,
184 )
185 }
186 Action::CallSubroutine { subroutine } => {
187 ("callSubroutine", vec![subroutine_node(subroutine)])
188 }
189 Action::If { condition } => ("if", vec![value_node(condition)?]),
190 Action::ElseIf { condition } => ("elseIf", vec![value_node(condition)?]),
191 Action::Else => ("else", Vec::new()),
192 Action::While { condition } => ("while", vec![value_node(condition)?]),
193 Action::ForGlobalVariable {
194 variable,
195 start,
196 stop,
197 step,
198 } => (
199 "forGlobalVariable",
200 vec![
201 variable_node(variable),
202 value_node(start)?,
203 value_node(stop)?,
204 value_node(step)?,
205 ],
206 ),
207 Action::ForPlayerVariable {
208 player,
209 variable,
210 start,
211 stop,
212 step,
213 } => (
214 "forPlayerVariable",
215 vec![
216 value_node(player)?,
217 variable_node(variable),
218 value_node(start)?,
219 value_node(stop)?,
220 value_node(step)?,
221 ],
222 ),
223 Action::End => ("end", Vec::new()),
224 Action::Disabled { action } => return action_node(action, true),
225 Action::Call { name, args } => (
226 chase_call_id(name, args),
227 args.iter().map(value_node).collect::<Result<_>>()?,
228 ),
229 };
230 reject_sentinel(call)?;
231 let mut node = Map::new();
232 node.insert("call".to_string(), call.into());
233 node.insert("args".to_string(), JsonValue::Array(args));
234 if disabled {
235 node.insert("disabled".to_string(), true.into());
236 }
237 Ok(JsonValue::Object(node))
238}
239
240fn value_node(value: &Value) -> Result<JsonValue> {
241 Ok(match value {
242 Value::Number(number) => serde_json::Number::from_f64(*number)
243 .ok_or_else(|| {
244 WorkshopError::unsupported(
245 format!("rule-content-v1 cannot express non-finite number {number}"),
246 None,
247 )
248 })?
249 .into(),
250 Value::Bool(value) => (*value).into(),
251 Value::Null => JsonValue::Null,
252 Value::String(text) => json!({ "string": text }),
253 Value::LocalizedString(id) => json!({ "localizedString": id }),
254 Value::Enum { value_type, value } => {
255 json!({ "enum": value_type, "member": value })
256 }
257 Value::GlobalVariable(variable) => {
258 call_node(GLOBAL_VARIABLE_CALL, vec![variable_node(variable)])
259 }
260 Value::PlayerVariable { player, variable } => call_node(
261 PLAYER_VARIABLE_CALL,
262 vec![value_node(player)?, variable_node(variable)],
263 ),
264 Value::Subroutine(name) => subroutine_node(name),
265 Value::EventPlayer => call_node("eventPlayer", Vec::new()),
266 Value::Array(elements) => call_node(
267 "array",
268 elements.iter().map(value_node).collect::<Result<_>>()?,
269 ),
270 Value::Vector { x, y, z } => call_node(
271 "vector",
272 vec![value_node(x)?, value_node(y)?, value_node(z)?],
273 ),
274 Value::Call { name, args } if name == AMBIGUOUS_ENUM_CALL => ambiguous_enum_node(args)?,
275 Value::Call { name, args } => {
276 call_node(name, args.iter().map(value_node).collect::<Result<_>>()?)
277 }
278 })
279}
280
281fn ambiguous_enum_node(args: &[Value]) -> Result<JsonValue> {
286 let [Value::String(spelling), Value::Array(candidates)] = args else {
287 return Err(WorkshopError::malformed(
288 "ambiguous enum value expects spelling and candidates",
289 None,
290 ));
291 };
292 if candidates.is_empty() {
293 return Err(WorkshopError::malformed(
294 "ambiguous enum value has no candidates",
295 None,
296 ));
297 }
298 let candidates = candidates
299 .iter()
300 .map(|candidate| match candidate {
301 Value::Enum { value_type, value } => Ok(json!({ "enum": value_type, "member": value })),
302 _ => Err(WorkshopError::malformed(
303 "ambiguous enum candidate must be an enum value",
304 None,
305 )),
306 })
307 .collect::<Result<Vec<JsonValue>>>()?;
308 Ok(json!({ "ambiguous": spelling, "candidates": candidates }))
309}
310
311fn chase_call_id<'a>(name: &'a str, args: &[Value]) -> &'a str {
317 match (name, args.first()) {
318 ("chaseAtRate", Some(Value::PlayerVariable { .. })) => "chasePlayerVariableAtRate",
319 ("chaseOverTime", Some(Value::PlayerVariable { .. })) => "chasePlayerVariableOverTime",
320 _ => name,
321 }
322}
323
324fn call_node(call: &str, args: Vec<JsonValue>) -> JsonValue {
325 let mut node = Map::new();
326 node.insert("call".to_string(), call.into());
327 node.insert("args".to_string(), JsonValue::Array(args));
328 JsonValue::Object(node)
329}
330
331fn variable_node(name: &str) -> JsonValue {
332 json!({ "variable": name })
333}
334
335fn subroutine_node(name: &str) -> JsonValue {
336 json!({ "subroutine": name })
337}
338
339fn modify_op_node(op: ModifyOp) -> JsonValue {
342 call_node(op.catalog_id(), Vec::new())
343}
344
345fn reject_sentinel(call: &str) -> Result<()> {
350 if call == AMBIGUOUS_ENUM_CALL {
351 return Err(WorkshopError::unsupported(
352 format!("rule-content-v1 cannot express '{call}'"),
353 None,
354 ));
355 }
356 Ok(())
357}