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 {
101 match team {
102 EventTeam::All => "ALL",
103 EventTeam::Team1 => "TEAM_1",
104 EventTeam::Team2 => "TEAM_2",
105 }
106}
107
108fn target_id(target: &EventTarget) -> String {
109 match target {
110 EventTarget::All => "ALL".to_string(),
111 EventTarget::Slot(slot) => format!("SLOT_{slot}"),
112 EventTarget::Hero(hero) => hero.clone(),
113 }
114}
115
116fn condition_node(condition: &Condition) -> Result<JsonValue> {
117 let mut node = Map::new();
118 node.insert("value".to_string(), value_node(&condition.value)?);
119 if condition.disabled {
120 node.insert("disabled".to_string(), true.into());
121 }
122 Ok(JsonValue::Object(node))
123}
124
125fn action_node(action: &Action, disabled: bool) -> Result<JsonValue> {
126 let (call, args) = match action {
127 Action::SetGlobalVariable { variable, value } => (
128 "setGlobalVariable",
129 vec![variable_node(variable), value_node(value)?],
130 ),
131 Action::ModifyGlobalVariable {
132 variable,
133 op,
134 value,
135 } => (
136 "modifyGlobalVariable",
137 vec![
138 variable_node(variable),
139 modify_op_node(*op),
140 value_node(value)?,
141 ],
142 ),
143 Action::SetPlayerVariable {
144 player,
145 variable,
146 value,
147 } => (
148 "setPlayerVariable",
149 vec![
150 value_node(player)?,
151 variable_node(variable),
152 value_node(value)?,
153 ],
154 ),
155 Action::ModifyPlayerVariable {
156 player,
157 variable,
158 op,
159 value,
160 } => (
161 "modifyPlayerVariable",
162 vec![
163 value_node(player)?,
164 variable_node(variable),
165 modify_op_node(*op),
166 value_node(value)?,
167 ],
168 ),
169 Action::AssignMember { target, op, value } => {
170 let args = match op {
171 None => vec![value_node(target)?, value_node(value)?],
172 Some(op) => vec![value_node(target)?, modify_op_node(*op), value_node(value)?],
173 };
174 (
175 if op.is_some() {
176 MODIFY_MEMBER_CALL
177 } else {
178 ASSIGN_MEMBER_CALL
179 },
180 args,
181 )
182 }
183 Action::CallSubroutine { subroutine } => {
184 ("callSubroutine", vec![subroutine_node(subroutine)])
185 }
186 Action::If { condition } => ("if", vec![value_node(condition)?]),
187 Action::ElseIf { condition } => ("elseIf", vec![value_node(condition)?]),
188 Action::Else => ("else", Vec::new()),
189 Action::While { condition } => ("while", vec![value_node(condition)?]),
190 Action::ForGlobalVariable {
191 variable,
192 start,
193 stop,
194 step,
195 } => (
196 "forGlobalVariable",
197 vec![
198 variable_node(variable),
199 value_node(start)?,
200 value_node(stop)?,
201 value_node(step)?,
202 ],
203 ),
204 Action::ForPlayerVariable {
205 player,
206 variable,
207 start,
208 stop,
209 step,
210 } => (
211 "forPlayerVariable",
212 vec![
213 value_node(player)?,
214 variable_node(variable),
215 value_node(start)?,
216 value_node(stop)?,
217 value_node(step)?,
218 ],
219 ),
220 Action::End => ("end", Vec::new()),
221 Action::Disabled { action } => return action_node(action, true),
222 Action::Call { name, args } => (
223 chase_call_id(name, args),
224 args.iter().map(value_node).collect::<Result<_>>()?,
225 ),
226 };
227 reject_sentinel(call)?;
228 let mut node = Map::new();
229 node.insert("call".to_string(), call.into());
230 node.insert("args".to_string(), JsonValue::Array(args));
231 if disabled {
232 node.insert("disabled".to_string(), true.into());
233 }
234 Ok(JsonValue::Object(node))
235}
236
237fn value_node(value: &Value) -> Result<JsonValue> {
238 Ok(match value {
239 Value::Number(number) => serde_json::Number::from_f64(*number)
240 .ok_or_else(|| {
241 WorkshopError::unsupported(
242 format!("rule-content-v1 cannot express non-finite number {number}"),
243 None,
244 )
245 })?
246 .into(),
247 Value::Bool(value) => (*value).into(),
248 Value::Null => JsonValue::Null,
249 Value::String(text) => json!({ "string": text }),
250 Value::LocalizedString(id) => json!({ "localizedString": id }),
251 Value::Enum { value_type, value } => {
252 json!({ "enum": value_type, "member": value })
253 }
254 Value::GlobalVariable(variable) => {
255 call_node(GLOBAL_VARIABLE_CALL, vec![variable_node(variable)])
256 }
257 Value::PlayerVariable { player, variable } => call_node(
258 PLAYER_VARIABLE_CALL,
259 vec![value_node(player)?, variable_node(variable)],
260 ),
261 Value::Subroutine(name) => subroutine_node(name),
262 Value::EventPlayer => call_node("eventPlayer", Vec::new()),
263 Value::Array(elements) => call_node(
264 "array",
265 elements.iter().map(value_node).collect::<Result<_>>()?,
266 ),
267 Value::Vector { x, y, z } => call_node(
268 "vector",
269 vec![value_node(x)?, value_node(y)?, value_node(z)?],
270 ),
271 Value::Call { name, args } if name == AMBIGUOUS_ENUM_CALL => ambiguous_enum_node(args)?,
272 Value::Call { name, args } => {
273 call_node(name, args.iter().map(value_node).collect::<Result<_>>()?)
274 }
275 })
276}
277
278fn ambiguous_enum_node(args: &[Value]) -> Result<JsonValue> {
279 let [Value::String(spelling), Value::Array(candidates)] = args else {
280 return Err(WorkshopError::malformed(
281 "ambiguous enum value expects spelling and candidates",
282 None,
283 ));
284 };
285 if candidates.is_empty() {
286 return Err(WorkshopError::malformed(
287 "ambiguous enum value has no candidates",
288 None,
289 ));
290 }
291 let candidates = candidates
292 .iter()
293 .map(|candidate| match candidate {
294 Value::Enum { value_type, value } => Ok(json!({ "enum": value_type, "member": value })),
295 _ => Err(WorkshopError::malformed(
296 "ambiguous enum candidate must be an enum value",
297 None,
298 )),
299 })
300 .collect::<Result<Vec<JsonValue>>>()?;
301 Ok(json!({ "ambiguous": spelling, "candidates": candidates }))
302}
303
304fn chase_call_id<'a>(name: &'a str, args: &[Value]) -> &'a str {
305 match (name, args.first()) {
306 ("chaseAtRate", Some(Value::PlayerVariable { .. })) => "chasePlayerVariableAtRate",
307 ("chaseOverTime", Some(Value::PlayerVariable { .. })) => "chasePlayerVariableOverTime",
308 _ => name,
309 }
310}
311
312fn call_node(call: &str, args: Vec<JsonValue>) -> JsonValue {
313 let mut node = Map::new();
314 node.insert("call".to_string(), call.into());
315 node.insert("args".to_string(), JsonValue::Array(args));
316 JsonValue::Object(node)
317}
318
319fn variable_node(name: &str) -> JsonValue {
320 json!({ "variable": name })
321}
322
323fn subroutine_node(name: &str) -> JsonValue {
324 json!({ "subroutine": name })
325}
326
327fn modify_op_node(op: ModifyOp) -> JsonValue {
328 call_node(op.catalog_id(), Vec::new())
329}
330
331fn reject_sentinel(call: &str) -> Result<()> {
332 if call == AMBIGUOUS_ENUM_CALL {
333 return Err(WorkshopError::unsupported(
334 format!("rule-content-v1 cannot express '{call}'"),
335 None,
336 ));
337 }
338 Ok(())
339}