A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.IO;
4using System.Linq;
5using System.Text;
6using UnityEngine.InputSystem.Utilities;
7using UnityEditor;
8
9////TODO: option to allow referencing the original asset rather than embedding it
10
11////TODO: emit indexer directly at toplevel so you can more easily look up actions dynamically
12
13////TODO: put the generated code behind #if that depends on input system
14
15////TODO: suffix map properties with Map or Actions (e.g. "PlayerMap" instead of "Player")
16
17////TODO: unify the generated events so that performed, canceled, and started all go into a single event
18
19////TODO: look up actions and maps by ID rather than by name
20
21////TODO: only generate @something if @ is really needed
22
23////TODO: allow having an unnamed or default-named action set which spills actions directly into the toplevel wrapper
24
25////TODO: add cleanup for ActionEvents
26
27////TODO: protect generated wrapper against modifications made to asset
28
29////TODO: make capitalization consistent in the generated code
30
31////TODO: instead of loading from JSON, generate the structure in code
32
33////REVIEW: allow putting *all* of the data from the inputactions asset into the generated class?
34
35namespace UnityEngine.InputSystem.Editor
36{
37 /// <summary>
38 /// Utility to generate code that makes it easier to work with action sets.
39 /// </summary>
40 public static class InputActionCodeGenerator
41 {
42 private const int kSpacesPerIndentLevel = 4;
43
44 public struct Options
45 {
46 public string className { get; set; }
47 public string namespaceName { get; set; }
48 public string sourceAssetPath { get; set; }
49 }
50
51 public static string GenerateWrapperCode(InputActionAsset asset, Options options = default)
52 {
53 if (asset == null)
54 throw new ArgumentNullException(nameof(asset));
55
56 if (string.IsNullOrEmpty(options.sourceAssetPath))
57 options.sourceAssetPath = AssetDatabase.GetAssetPath(asset);
58 if (string.IsNullOrEmpty(options.className) && !string.IsNullOrEmpty(asset.name))
59 options.className =
60 CSharpCodeHelpers.MakeTypeName(asset.name);
61
62 if (string.IsNullOrEmpty(options.className))
63 {
64 if (string.IsNullOrEmpty(options.sourceAssetPath))
65 throw new ArgumentException("options.sourceAssetPath");
66 options.className =
67 CSharpCodeHelpers.MakeTypeName(Path.GetFileNameWithoutExtension(options.sourceAssetPath));
68 }
69
70 var writer = new Writer
71 {
72 buffer = new StringBuilder()
73 };
74
75 // Header.
76 writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputActionCodeGenerator",
77 InputSystem.version.ToString(),
78 options.sourceAssetPath));
79
80 // Usings.
81 writer.WriteLine("using System;");
82 writer.WriteLine("using System.Collections;");
83 writer.WriteLine("using System.Collections.Generic;");
84 writer.WriteLine("using UnityEngine.InputSystem;");
85 writer.WriteLine("using UnityEngine.InputSystem.Utilities;");
86 writer.WriteLine("");
87
88 // Begin namespace.
89 var haveNamespace = !string.IsNullOrEmpty(options.namespaceName);
90 if (haveNamespace)
91 {
92 writer.WriteLine($"namespace {options.namespaceName}");
93 writer.BeginBlock();
94 }
95
96 // Begin class.
97 writer.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable");
98 writer.BeginBlock();
99
100 writer.WriteLine($"public InputActionAsset asset {{ get; }}");
101
102 // Default constructor.
103 writer.WriteLine($"public @{options.className}()");
104 writer.BeginBlock();
105 writer.WriteLine($"asset = InputActionAsset.FromJson(@\"{asset.ToJson().Replace("\"", "\"\"")}\");");
106
107 var maps = asset.actionMaps;
108 var schemes = asset.controlSchemes;
109 foreach (var map in maps)
110 {
111 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
112 writer.WriteLine($"// {map.name}");
113 writer.WriteLine($"m_{mapName} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);");
114
115 foreach (var action in map.actions)
116 {
117 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
118 writer.WriteLine($"m_{mapName}_{actionName} = m_{mapName}.FindAction(\"{action.name}\", throwIfNotFound: true);");
119 }
120 }
121 writer.EndBlock();
122 writer.WriteLine();
123
124 writer.WriteLine($"~@{options.className}()");
125 writer.BeginBlock();
126 foreach (var map in maps)
127 {
128 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
129 writer.WriteLine($"UnityEngine.Debug.Assert(!m_{mapName}.enabled, \"This will cause a leak and performance issues, {options.className}.{mapName}.Disable() has not been called.\");");
130 }
131 writer.EndBlock();
132 writer.WriteLine();
133
134 writer.WriteLine("public void Dispose()");
135 writer.BeginBlock();
136 writer.WriteLine("UnityEngine.Object.Destroy(asset);");
137 writer.EndBlock();
138 writer.WriteLine();
139
140 writer.WriteLine("public InputBinding? bindingMask");
141 writer.BeginBlock();
142 writer.WriteLine("get => asset.bindingMask;");
143 writer.WriteLine("set => asset.bindingMask = value;");
144 writer.EndBlock();
145 writer.WriteLine();
146
147 writer.WriteLine("public ReadOnlyArray<InputDevice>? devices");
148 writer.BeginBlock();
149 writer.WriteLine("get => asset.devices;");
150 writer.WriteLine("set => asset.devices = value;");
151 writer.EndBlock();
152 writer.WriteLine();
153
154 writer.WriteLine("public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;");
155 writer.WriteLine();
156
157 writer.WriteLine("public bool Contains(InputAction action)");
158 writer.BeginBlock();
159 writer.WriteLine("return asset.Contains(action);");
160 writer.EndBlock();
161 writer.WriteLine();
162
163 writer.WriteLine("public IEnumerator<InputAction> GetEnumerator()");
164 writer.BeginBlock();
165 writer.WriteLine("return asset.GetEnumerator();");
166 writer.EndBlock();
167 writer.WriteLine();
168
169 writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()");
170 writer.BeginBlock();
171 writer.WriteLine("return GetEnumerator();");
172 writer.EndBlock();
173 writer.WriteLine();
174
175 writer.WriteLine("public void Enable()");
176 writer.BeginBlock();
177 writer.WriteLine("asset.Enable();");
178 writer.EndBlock();
179 writer.WriteLine();
180
181 writer.WriteLine("public void Disable()");
182 writer.BeginBlock();
183 writer.WriteLine("asset.Disable();");
184 writer.EndBlock();
185 writer.WriteLine();
186
187 writer.WriteLine("public IEnumerable<InputBinding> bindings => asset.bindings;");
188 writer.WriteLine();
189
190 writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)");
191 writer.BeginBlock();
192 writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);");
193 writer.EndBlock();
194 writer.WriteLine();
195
196 writer.WriteLine("public int FindBinding(InputBinding bindingMask, out InputAction action)");
197 writer.BeginBlock();
198 writer.WriteLine("return asset.FindBinding(bindingMask, out action);");
199 writer.EndBlock();
200
201 // Action map accessors.
202 foreach (var map in maps)
203 {
204 writer.WriteLine();
205 writer.WriteLine($"// {map.name}");
206
207 var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
208 var mapTypeName = CSharpCodeHelpers.MakeTypeName(mapName, "Actions");
209
210 // Caching field for action map.
211 writer.WriteLine($"private readonly InputActionMap m_{mapName};");
212 writer.WriteLine(string.Format("private List<I{0}> m_{0}CallbackInterfaces = new List<I{0}>();", mapTypeName));
213
214 // Caching fields for all actions.
215 foreach (var action in map.actions)
216 {
217 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
218 writer.WriteLine($"private readonly InputAction m_{mapName}_{actionName};");
219 }
220
221 // Struct wrapping access to action set.
222 writer.WriteLine($"public struct {mapTypeName}");
223 writer.BeginBlock();
224
225 // Constructor.
226 writer.WriteLine($"private @{options.className} m_Wrapper;");
227 writer.WriteLine($"public {mapTypeName}(@{options.className} wrapper) {{ m_Wrapper = wrapper; }}");
228
229 // Getter for each action.
230 foreach (var action in map.actions)
231 {
232 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
233 writer.WriteLine(
234 $"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};");
235 }
236
237 // Action map getter.
238 writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}");
239
240 // Enable/disable methods.
241 writer.WriteLine("public void Enable() { Get().Enable(); }");
242 writer.WriteLine("public void Disable() { Get().Disable(); }");
243 writer.WriteLine("public bool enabled => Get().enabled;");
244
245 // Implicit conversion operator.
246 writer.WriteLine(
247 $"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}");
248
249 // AddCallbacks method.
250 writer.WriteLine($"public void AddCallbacks(I{mapTypeName} instance)");
251 writer.BeginBlock();
252
253 // Initialize new interface.
254 writer.WriteLine($"if (instance == null || m_Wrapper.m_{mapTypeName}CallbackInterfaces.Contains(instance)) return;");
255 writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Add(instance);");
256
257 foreach (var action in map.actions)
258 {
259 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
260 var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);
261
262 writer.WriteLine($"@{actionName}.started += instance.On{actionTypeName};");
263 writer.WriteLine($"@{actionName}.performed += instance.On{actionTypeName};");
264 writer.WriteLine($"@{actionName}.canceled += instance.On{actionTypeName};");
265 }
266
267 writer.EndBlock();
268 writer.WriteLine();
269
270 // UnregisterCallbacks method.
271 writer.WriteLine($"private void UnregisterCallbacks(I{mapTypeName} instance)");
272 writer.BeginBlock();
273 foreach (var action in map.actions)
274 {
275 var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
276 var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);
277
278 writer.WriteLine($"@{actionName}.started -= instance.On{actionTypeName};");
279 writer.WriteLine($"@{actionName}.performed -= instance.On{actionTypeName};");
280 writer.WriteLine($"@{actionName}.canceled -= instance.On{actionTypeName};");
281 }
282 writer.EndBlock();
283 writer.WriteLine();
284
285 // RemoveCallbacks method.
286 writer.WriteLine($"public void RemoveCallbacks(I{mapTypeName} instance)");
287 writer.BeginBlock();
288 writer.WriteLine($"if (m_Wrapper.m_{mapTypeName}CallbackInterfaces.Remove(instance))");
289 writer.WriteLine($" UnregisterCallbacks(instance);");
290 writer.EndBlock();
291 writer.WriteLine();
292
293 // SetCallbacks method.
294 writer.WriteLine($"public void SetCallbacks(I{mapTypeName} instance)");
295 writer.BeginBlock();
296
297 ////REVIEW: this would benefit from having a single callback on InputActions rather than three different endpoints
298
299 writer.WriteLine($"foreach (var item in m_Wrapper.m_{mapTypeName}CallbackInterfaces)");
300 writer.WriteLine($" UnregisterCallbacks(item);");
301 writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Clear();");
302
303 // Initialize new interface.
304 writer.WriteLine("AddCallbacks(instance);");
305 writer.EndBlock();
306 writer.EndBlock();
307
308 // Getter for instance of struct.
309 writer.WriteLine($"public {mapTypeName} @{mapName} => new {mapTypeName}(this);");
310 }
311
312 // Control scheme accessors.
313 foreach (var scheme in schemes)
314 {
315 var identifier = CSharpCodeHelpers.MakeIdentifier(scheme.name);
316
317 writer.WriteLine($"private int m_{identifier}SchemeIndex = -1;");
318 writer.WriteLine($"public InputControlScheme {identifier}Scheme");
319 writer.BeginBlock();
320 writer.WriteLine("get");
321 writer.BeginBlock();
322 writer.WriteLine($"if (m_{identifier}SchemeIndex == -1) m_{identifier}SchemeIndex = asset.FindControlSchemeIndex(\"{scheme.name}\");");
323 writer.WriteLine($"return asset.controlSchemes[m_{identifier}SchemeIndex];");
324 writer.EndBlock();
325 writer.EndBlock();
326 }
327
328 // Generate interfaces.
329 foreach (var map in maps)
330 {
331 var typeName = CSharpCodeHelpers.MakeTypeName(map.name);
332 writer.WriteLine($"public interface I{typeName}Actions");
333 writer.BeginBlock();
334
335 foreach (var action in map.actions)
336 {
337 var methodName = CSharpCodeHelpers.MakeTypeName(action.name);
338 writer.WriteLine($"void On{methodName}(InputAction.CallbackContext context);");
339 }
340
341 writer.EndBlock();
342 }
343
344 // End class.
345 writer.EndBlock();
346
347 // End namespace.
348 if (haveNamespace)
349 writer.EndBlock();
350
351 return writer.buffer.ToString();
352 }
353
354 ////TODO: move this to a shared place
355 internal struct Writer
356 {
357 public StringBuilder buffer;
358 public int indentLevel;
359
360 public void BeginBlock()
361 {
362 WriteIndent();
363 buffer.Append("{\n");
364 ++indentLevel;
365 }
366
367 public void EndBlock()
368 {
369 --indentLevel;
370 WriteIndent();
371 buffer.Append("}\n");
372 }
373
374 public void WriteLine()
375 {
376 buffer.Append('\n');
377 }
378
379 public void WriteLine(string text)
380 {
381 if (!text.All(char.IsWhiteSpace))
382 {
383 WriteIndent();
384 buffer.Append(text);
385 }
386 buffer.Append('\n');
387 }
388
389 public void Write(string text)
390 {
391 buffer.Append(text);
392 }
393
394 public void WriteIndent()
395 {
396 for (var i = 0; i < indentLevel; ++i)
397 {
398 for (var n = 0; n < kSpacesPerIndentLevel; ++n)
399 buffer.Append(' ');
400 }
401 }
402 }
403
404 // Updates the given file with wrapper code generated for the given action sets.
405 // If the generated code is unchanged, does not touch the file.
406 // Returns true if the file was touched, false otherwise.
407 public static bool GenerateWrapperCode(string filePath, InputActionAsset asset, Options options)
408 {
409 if (!Path.HasExtension(filePath))
410 filePath += ".cs";
411
412 // Generate code.
413 var code = GenerateWrapperCode(asset, options);
414
415 // Check if the code changed. Don't write if it hasn't.
416 if (File.Exists(filePath))
417 {
418 var existingCode = File.ReadAllText(filePath);
419 if (existingCode == code || existingCode.WithAllWhitespaceStripped() == code.WithAllWhitespaceStripped())
420 return false;
421 }
422
423 // Write.
424 EditorHelpers.CheckOut(filePath);
425 File.WriteAllText(filePath, code);
426 return true;
427 }
428 }
429}
430#endif // UNITY_EDITOR