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