A game about forced loneliness, made by TACStudios
1using System;
2using Unity.VisualScripting.Dependencies.NCalc;
3using UnityEngine;
4using NCalc = Unity.VisualScripting.Dependencies.NCalc.Expression;
5
6namespace Unity.VisualScripting
7{
8 /// <summary>
9 /// Evaluates a mathematical or logical formula with multiple arguments.
10 /// </summary>
11 public sealed class Formula : MultiInputUnit<object>
12 {
13 [SerializeAs(nameof(Formula))]
14 private string _formula;
15
16 private NCalc ncalc;
17
18 /// <summary>
19 /// A mathematical or logical expression tree.
20 /// </summary>
21 [DoNotSerialize]
22 [Inspectable, UnitHeaderInspectable]
23 [InspectorTextArea]
24 public string formula
25 {
26 get => _formula;
27 set
28 {
29 _formula = value;
30
31 InitializeNCalc();
32 }
33 }
34
35 /// <summary>
36 /// Whether input arguments should only be fetched once and then reused.
37 /// </summary>
38 [Serialize]
39 [Inspectable(order = int.MaxValue)]
40 [InspectorExpandTooltip]
41 public bool cacheArguments { get; set; }
42
43 /// <summary>
44 /// The result of the calculation or evaluation.
45 /// </summary>
46 [DoNotSerialize]
47 [PortLabelHidden]
48 public ValueOutput result { get; private set; }
49
50 protected override int minInputCount => 0;
51
52 protected override void Definition()
53 {
54 base.Definition();
55
56 result = ValueOutput(nameof(result), Evaluate);
57
58 InputsAllowNull();
59
60 foreach (var input in multiInputs)
61 {
62 Requirement(input, result);
63 }
64
65 InitializeNCalc();
66 }
67
68 private void InitializeNCalc()
69 {
70 if (string.IsNullOrEmpty(formula))
71 {
72 ncalc = null;
73 return;
74 }
75
76 ncalc = new NCalc(formula);
77 ncalc.Options = EvaluateOptions.IgnoreCase;
78 ncalc.EvaluateParameter += EvaluateTreeParameter;
79 ncalc.EvaluateFunction += EvaluateTreeFunction;
80 }
81
82 private object Evaluate(Flow flow)
83 {
84 if (ncalc == null)
85 {
86 throw new InvalidOperationException("No formula provided.");
87 }
88
89 ncalc.UpdateUnityTimeParameters();
90
91 return ncalc.Evaluate(flow);
92 }
93
94 private void EvaluateTreeFunction(Flow flow, string name, FunctionArgs args)
95 {
96 if (name == "v2" || name == "V2")
97 {
98 if (args.Parameters.Length != 2)
99 {
100 throw new ArgumentException($"v2() takes at exactly 2 arguments. {args.Parameters.Length} provided.");
101 }
102
103 args.Result = new Vector2
104 (
105 ConversionUtility.Convert<float>(args.Parameters[0].Evaluate(flow)),
106 ConversionUtility.Convert<float>(args.Parameters[1].Evaluate(flow))
107 );
108 }
109 else if (name == "v3" || name == "V3")
110 {
111 if (args.Parameters.Length != 3)
112 {
113 throw new ArgumentException($"v3() takes at exactly 3 arguments. {args.Parameters.Length} provided.");
114 }
115
116 args.Result = new Vector3
117 (
118 ConversionUtility.Convert<float>(args.Parameters[0].Evaluate(flow)),
119 ConversionUtility.Convert<float>(args.Parameters[1].Evaluate(flow)),
120 ConversionUtility.Convert<float>(args.Parameters[2].Evaluate(flow))
121 );
122 }
123 else if (name == "v4" || name == "V4")
124 {
125 if (args.Parameters.Length != 4)
126 {
127 throw new ArgumentException($"v4() takes at exactly 4 arguments. {args.Parameters.Length} provided.");
128 }
129
130 args.Result = new Vector4
131 (
132 ConversionUtility.Convert<float>(args.Parameters[0].Evaluate(flow)),
133 ConversionUtility.Convert<float>(args.Parameters[1].Evaluate(flow)),
134 ConversionUtility.Convert<float>(args.Parameters[2].Evaluate(flow)),
135 ConversionUtility.Convert<float>(args.Parameters[3].Evaluate(flow))
136 );
137 }
138 }
139
140 public object GetParameterValue(Flow flow, string name)
141 {
142 if (name.Length == 1)
143 {
144 var character = name[0];
145
146 if (char.IsLetter(character))
147 {
148 character = char.ToLower(character);
149
150 var index = GetArgumentIndex(character);
151
152 if (index < multiInputs.Count)
153 {
154 var input = multiInputs[index];
155
156 if (cacheArguments && !flow.IsLocal(input))
157 {
158 flow.SetValue(input, flow.GetValue<object>(input));
159 }
160
161 return flow.GetValue<object>(input);
162 }
163 }
164 }
165 else
166 {
167 if (Variables.Graph(flow.stack).IsDefined(name))
168 {
169 return Variables.Graph(flow.stack).Get(name);
170 }
171
172 var self = flow.stack.self;
173
174 if (self != null)
175 {
176 if (Variables.Object(self).IsDefined(name))
177 {
178 return Variables.Object(self).Get(name);
179 }
180 }
181
182 var scene = flow.stack.scene;
183
184 if (scene != null)
185 {
186 if (Variables.Scene(scene).IsDefined(name))
187 {
188 return Variables.Scene(scene).Get(name);
189 }
190 }
191
192 if (Variables.Application.IsDefined(name))
193 {
194 return Variables.Application.Get(name);
195 }
196
197 if (Variables.Saved.IsDefined(name))
198 {
199 return Variables.Saved.Get(name);
200 }
201 }
202
203 throw new InvalidOperationException($"Unknown expression tree parameter: '{name}'.\nSupported parameter names are alphabetical indices and variable names.");
204 }
205
206 private void EvaluateTreeParameter(Flow flow, string name, ParameterArgs args)
207 {
208 // [param.fieldOrProperty]
209 // [param.parmeterLessMethod()]
210 if (name.Contains("."))
211 {
212 var parts = name.Split('.');
213
214 if (parts.Length == 2)
215 {
216 var parameterName = parts[0];
217
218 var memberName = parts[1].TrimEnd("()");
219
220 var variableValue = GetParameterValue(flow, parameterName);
221
222 var manipulator = new Member(variableValue.GetType(), memberName, Type.EmptyTypes);
223
224 var target = variableValue;
225
226 if (manipulator.isInvocable)
227 {
228 args.Result = manipulator.Invoke(target);
229 }
230 else if (manipulator.isGettable)
231 {
232 args.Result = manipulator.Get(target);
233 }
234 else
235 {
236 throw new InvalidOperationException($"Cannot get or invoke expression tree parameter: [{parameterName}.{memberName}]");
237 }
238 }
239 else
240 {
241 throw new InvalidOperationException($"Cannot parse expression tree parameter: [{name}]");
242 }
243 }
244 else
245 {
246 args.Result = GetParameterValue(flow, name);
247 }
248 }
249
250 public static string GetArgumentName(int index)
251 {
252 if (index > ('z' - 'a'))
253 {
254 throw new NotImplementedException("Argument indices above 26 are not yet supported.");
255 }
256
257 return ((char)('a' + index)).ToString();
258 }
259
260 public static int GetArgumentIndex(char name)
261 {
262 if (name < 'a' || name > 'z')
263 {
264 throw new NotImplementedException("Unalphabetical argument names are not yet supported.");
265 }
266
267 return name - 'a';
268 }
269 }
270}