A game about forced loneliness, made by TACStudios
1#if UNITY_EDITOR
2using System;
3using System.Collections.Generic;
4using System.IO;
5using System.Linq;
6using System.Reflection;
7using System.Text;
8using Unity.Collections.LowLevel.Unsafe;
9using UnityEngine.InputSystem.Controls;
10using UnityEngine.InputSystem.Layouts;
11using UnityEngine.InputSystem.Processors;
12using UnityEngine.InputSystem.Utilities;
13
14namespace UnityEngine.InputSystem.Editor
15{
16 internal static class InputLayoutCodeGenerator
17 {
18 public static string GenerateCodeFileForDeviceLayout(string layoutName, string fileName, string prefix = "Fast")
19 {
20 string defines = null;
21 string @namespace = null;
22 var visibility = "public";
23
24 // If the file already exists, read out the changes we preserve.
25 if (File.Exists(fileName))
26 {
27 var lines = File.ReadLines(fileName).Take(50).ToList();
28
29 // Read out #defines.
30 for (var i = 0; i < (lines.Count - 1); ++i)
31 {
32 var line = lines[i].Trim();
33 if (line.StartsWith("#if "))
34 defines = line.Substring("#if ".Length);
35 else if (line.StartsWith("namespace "))
36 @namespace = line.Substring("namespace ".Length);
37 }
38
39 if (lines.Any(x => x.Contains("internal partial class " + prefix)))
40 visibility = "internal";
41 }
42
43 return GenerateCodeForDeviceLayout(layoutName,
44 defines: defines, visibility: visibility, @namespace: @namespace, namePrefix: prefix);
45 }
46
47 /// <summary>
48 /// Generate C# code that for the given device layout called <paramref name="layoutName"/> instantly creates
49 /// an <see cref="InputDevice"/> equivalent to what the input system would create by manually interpreting
50 /// the given <see cref="InputControlLayout"/>.
51 /// </summary>
52 /// <param name="layoutName">Name of the device layout to generate code for.</param>
53 /// <param name="defines">Null/empty or a valid expression for an #if conditional compilation statement.</param>
54 /// <param name="namePrefix">Prefix to prepend to the type name of <paramref name="layoutName"/>.</param>
55 /// <param name="visibility">C# access modifier to use with the generated class.</param>
56 /// <param name="namespace">Namespace to put the generated class in. If <c>null</c>, namespace of type behind <paramref name="layoutName"/> will be used.</param>
57 /// <returns>C# source code for a precompiled version of the device layout.</returns>
58 /// <remarks>
59 /// The code generated by this method will be many times faster than the reflection-based <see cref="InputDevice"/>
60 /// creation normally performed by the input system. It will also create less GC heap garbage.
61 ///
62 /// The downside to the generated code is that the makeup of the device is hardcoded and can no longer
63 /// be changed by altering the <see cref="InputControlLayout"/> setup of the system.
64 ///
65 /// Note that it is possible to use this method with layouts generated on-the-fly by layout builders such as
66 /// the one employed for <see cref="HID"/>. However, this must be done at compile/build time and can thus not
67 /// be done for devices dynamically discovered at runtime. When this is acceptable, it is a way to dramatically
68 /// speed up the creation of these devices.
69 /// </remarks>
70 /// <seealso cref="InputSystem.RegisterPrecompiledLayout{T}"/>
71 public static unsafe string GenerateCodeForDeviceLayout(string layoutName, string defines = null, string namePrefix = "Fast", string visibility = "public", string @namespace = null)
72 {
73 if (string.IsNullOrEmpty(layoutName))
74 throw new ArgumentNullException(nameof(layoutName));
75
76 // Produce a device from the layout.
77 var device = InputDevice.Build<InputDevice>(layoutName, noPrecompiledLayouts: true);
78
79 // Get info about base type.
80 var baseType = device.GetType();
81 var baseTypeName = baseType.Name;
82 var baseTypeNamespace = baseType.Namespace;
83
84 // Begin generating code.
85 var writer = new InputActionCodeGenerator.Writer
86 {
87 buffer = new StringBuilder()
88 };
89
90 writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputLayoutCodeGenerator",
91 InputSystem.version.ToString(),
92 $"\"{layoutName}\" layout"));
93
94 // Defines.
95 if (defines != null)
96 {
97 writer.WriteLine($"#if {defines}");
98 writer.WriteLine();
99 }
100
101 if (@namespace == null)
102 @namespace = baseTypeNamespace;
103
104 writer.WriteLine("using UnityEngine.InputSystem;");
105 writer.WriteLine("using UnityEngine.InputSystem.LowLevel;");
106 writer.WriteLine("using UnityEngine.InputSystem.Utilities;");
107 writer.WriteLine("");
108 writer.WriteLine("// Suppress warnings from local variables for control references");
109 writer.WriteLine("// that we don't end up using.");
110 writer.WriteLine("#pragma warning disable CS0219");
111 writer.WriteLine("");
112 if (!string.IsNullOrEmpty(@namespace))
113 {
114 writer.WriteLine("namespace " + @namespace);
115 writer.BeginBlock();
116 }
117
118 if (string.IsNullOrEmpty(baseTypeNamespace))
119 writer.WriteLine($"{visibility} partial class {namePrefix}{baseTypeName} : {baseTypeName}");
120 else
121 writer.WriteLine($"{visibility} partial class {namePrefix}{baseTypeName} : {baseTypeNamespace}.{baseTypeName}");
122
123 writer.BeginBlock();
124
125 // "Metadata". ATM this is simply a flat, semicolon-separated list of names for layouts and processors that
126 // we depend on. If any of them are touched, the precompiled layout should be considered invalidated.
127 var internedLayoutName = new InternedString(layoutName);
128 var allControls = device.allControls;
129 var usedControlLayouts = allControls.Select(x => x.m_Layout).Distinct().ToList();
130 var layoutDependencies = string.Join(";",
131 usedControlLayouts.SelectMany(l => InputControlLayout.s_Layouts.GetBaseLayouts(l))
132 .Union(InputControlLayout.s_Layouts.GetBaseLayouts(internedLayoutName)));
133 var processorDependencies = string.Join(";",
134 allControls.SelectMany(c => c.GetProcessors()).Select(p => InputProcessor.s_Processors.FindNameForType(p.GetType()))
135 .Where(n => !n.IsEmpty()).Distinct());
136 var metadata = string.Join(";", processorDependencies, layoutDependencies);
137 writer.WriteLine($"public const string metadata = \"{metadata}\";");
138
139 // Constructor.
140 writer.WriteLine($"public {namePrefix}{baseTypeName}()");
141 writer.BeginBlock();
142
143 var usagesForEachControl = device.m_UsagesForEachControl;
144 var usageToControl = device.m_UsageToControl;
145 var aliasesForEachControl = device.m_AliasesForEachControl;
146 var controlCount = allControls.Count;
147 var usageCount = usagesForEachControl?.Length ?? 0;
148 var aliasCount = aliasesForEachControl?.Length ?? 0;
149
150 // Set up device control info.
151 writer.WriteLine($"var builder = this.Setup({controlCount}, {usageCount}, {aliasCount})");
152 writer.WriteLine($" .WithName(\"{device.name}\")");
153 writer.WriteLine($" .WithDisplayName(\"{device.displayName}\")");
154 writer.WriteLine($" .WithChildren({device.m_ChildStartIndex}, {device.m_ChildCount})");
155 writer.WriteLine($" .WithLayout(new InternedString(\"{device.layout}\"))");
156 writer.WriteLine($" .WithStateBlock(new InputStateBlock {{ format = new FourCC({(int)device.stateBlock.format}), sizeInBits = {device.stateBlock.sizeInBits} }});");
157
158 if (device.noisy)
159 writer.WriteLine("builder.IsNoisy(true);");
160
161 // Add controls to device.
162 writer.WriteLine();
163 foreach (var layout in usedControlLayouts)
164 writer.WriteLine($"var k{layout}Layout = new InternedString(\"{layout}\");");
165
166 for (var i = 0; i < controlCount; ++i)
167 {
168 var control = allControls[i];
169 var controlVariableName = MakeControlVariableName(control);
170
171 writer.WriteLine("");
172 writer.WriteLine($"// {control.path}");
173 var parentName = "this";
174 if (control.parent != device)
175 parentName = MakeControlVariableName(control.parent);
176 writer.WriteLine($"var {controlVariableName} = {NameOfControlMethod(controlVariableName)}(k{control.layout}Layout, {parentName});");
177 }
178
179 // Initialize usages array.
180 if (usageCount > 0)
181 {
182 writer.WriteLine();
183 writer.WriteLine("// Usages.");
184 for (var i = 0; i < usageCount; ++i)
185 writer.WriteLine(
186 $"builder.WithControlUsage({i}, new InternedString(\"{usagesForEachControl[i]}\"), {MakeControlVariableName(usageToControl[i])});");
187 }
188
189 // Initialize aliases array.
190 if (aliasCount > 0)
191 {
192 writer.WriteLine();
193 writer.WriteLine("// Aliases.");
194 for (var i = 0; i < aliasCount; ++i)
195 writer.WriteLine($"builder.WithControlAlias({i}, new InternedString(\"{aliasesForEachControl[i]}\"));");
196 }
197
198 // Emit initializers for control getters and control arrays. This is usually what's getting set up
199 // in FinishSetup(). We hardcode the look results here.
200 var controlGetterProperties = new Dictionary<Type, List<PropertyInfo>>();
201 var controlArrayProperties = new Dictionary<Type, List<PropertyInfo>>();
202
203 writer.WriteLine();
204 writer.WriteLine("// Control getters/arrays.");
205 writer.EmitControlArrayInitializers(device, "this", controlArrayProperties);
206 writer.EmitControlGetterInitializers(device, "this", controlGetterProperties);
207
208 for (var i = 0; i < controlCount; ++i)
209 {
210 var control = allControls[i];
211 var controlVariableName = MakeControlVariableName(control);
212
213 writer.EmitControlArrayInitializers(control, controlVariableName, controlArrayProperties);
214 writer.EmitControlGetterInitializers(control, controlVariableName, controlGetterProperties);
215 }
216
217 // State offset to control index map.
218 if (device.m_StateOffsetToControlMap != null)
219 {
220 writer.WriteLine();
221 writer.WriteLine("// State offset to control index map.");
222 writer.WriteLine("builder.WithStateOffsetToControlIndexMap(new uint[]");
223 writer.WriteLine("{");
224 ++writer.indentLevel;
225 var map = device.m_StateOffsetToControlMap;
226 var entryCount = map.Length;
227 for (var index = 0; index < entryCount;)
228 {
229 if (index != 0)
230 writer.WriteLine();
231 // 10 entries a line.
232 writer.WriteIndent();
233 for (var i = 0; i < 10 && index < entryCount; ++index, ++i)
234 writer.Write((index != 0 ? ", " : "") + map[index] + "u");
235 }
236 writer.WriteLine();
237 --writer.indentLevel;
238 writer.WriteLine("});");
239 }
240
241 writer.WriteLine();
242
243 if (device.m_ControlTreeNodes != null)
244 {
245 if (device.m_ControlTreeIndices == null)
246 throw new InvalidOperationException(
247 $"Control tree indicies was null. Ensure the '{device.displayName}' device was created without errors.");
248
249 writer.WriteLine("builder.WithControlTree(new byte[]");
250 writer.WriteLine("{");
251 ++writer.indentLevel;
252 writer.WriteLine("// Control tree nodes as bytes");
253 var nodePtr = (byte*)UnsafeUtility.AddressOf(ref device.m_ControlTreeNodes[0]);
254 var byteCount = device.m_ControlTreeNodes.Length * UnsafeUtility.SizeOf<InputDevice.ControlBitRangeNode>();
255
256 for (var i = 0; i < byteCount;)
257 {
258 if (i != 0)
259 writer.WriteLine();
260
261 writer.WriteIndent();
262 for (var j = 0; j < 30 && i < byteCount; j++, i++)
263 {
264 writer.Write((i != 0 ? ", " : "") + *(nodePtr + i));
265 }
266 }
267 writer.WriteLine();
268 --writer.indentLevel;
269
270 writer.WriteLine("}, new ushort[]");
271
272 ++writer.indentLevel;
273 writer.WriteLine("{");
274 ++writer.indentLevel;
275 writer.WriteLine("// Control tree node indicies");
276 writer.WriteLine();
277
278 for (var i = 0; i < device.m_ControlTreeIndices.Length;)
279 {
280 if (i != 0)
281 writer.WriteLine();
282
283 writer.WriteIndent();
284 for (var j = 0; j < 30 && i < device.m_ControlTreeIndices.Length; j++, i++)
285 {
286 writer.Write((i != 0 ? ", " : "") + device.m_ControlTreeIndices[i]);
287 }
288 }
289
290 writer.WriteLine();
291 --writer.indentLevel;
292 writer.WriteLine("});");
293 --writer.indentLevel;
294 }
295
296 writer.WriteLine();
297
298 writer.WriteLine("builder.Finish();");
299 writer.EndBlock();
300
301 for (var i = 0; i < controlCount; ++i)
302 {
303 var control = allControls[i];
304 var controlType = control.GetType();
305 var controlVariableName = MakeControlVariableName(control);
306 var controlFieldInits = control.GetInitializersForPublicPrimitiveTypeFields();
307 writer.WriteLine();
308 EmitControlMethod(writer, controlVariableName, controlType, controlFieldInits, i, control);
309 }
310
311 writer.EndBlock();
312
313 if (!string.IsNullOrEmpty(@namespace))
314 writer.EndBlock();
315
316 if (defines != null)
317 writer.WriteLine($"#endif // {defines}");
318
319 return writer.buffer.ToString();
320 }
321
322 private static string NameOfControlMethod(string controlVariableName)
323 {
324 return $"Initialize_{controlVariableName}";
325 }
326
327 // We emit this as a separate method instead of directly inline to avoid generating a single massive constructor method
328 // as these can lead to large build times with il2cpp and C++ compilers (https://fogbugz.unity3d.com/f/cases/1282090/).
329 private static void EmitControlMethod(InputActionCodeGenerator.Writer writer, string controlVariableName, Type controlType,
330 string controlFieldInits, int i, InputControl control)
331 {
332 var controlTypeName = controlType.FullName.Replace('+', '.');
333 writer.WriteLine($"private {controlTypeName} {NameOfControlMethod(controlVariableName)}(InternedString k{control.layout}Layout, InputControl parent)");
334 writer.BeginBlock();
335 writer.WriteLine($"var {controlVariableName} = new {controlTypeName}{controlFieldInits};");
336 writer.WriteLine($"{controlVariableName}.Setup()");
337 writer.WriteLine($" .At(this, {i})");
338 writer.WriteLine(" .WithParent(parent)");
339 if (control.children.Count > 0)
340 writer.WriteLine($" .WithChildren({control.m_ChildStartIndex}, {control.m_ChildCount})");
341 writer.WriteLine($" .WithName(\"{control.name}\")");
342 writer.WriteLine($" .WithDisplayName(\"{control.m_DisplayNameFromLayout.Replace("\\", "\\\\")}\")");
343 if (!string.IsNullOrEmpty(control.m_ShortDisplayNameFromLayout))
344 writer.WriteLine(
345 $" .WithShortDisplayName(\"{control.m_ShortDisplayNameFromLayout.Replace("\\", "\\\\")}\")");
346 writer.WriteLine($" .WithLayout(k{control.layout}Layout)");
347 if (control.usages.Count > 0)
348 writer.WriteLine($" .WithUsages({control.m_UsageStartIndex}, {control.m_UsageCount})");
349 if (control.aliases.Count > 0)
350 writer.WriteLine($" .WithAliases({control.m_AliasStartIndex}, {control.m_AliasCount})");
351 if (control.noisy)
352 writer.WriteLine(" .IsNoisy(true)");
353 if (control.synthetic)
354 writer.WriteLine(" .IsSynthetic(true)");
355 if (control.dontReset)
356 writer.WriteLine(" .DontReset(true)");
357 if (control is ButtonControl)
358 writer.WriteLine(" .IsButton(true)");
359 writer.WriteLine(" .WithStateBlock(new InputStateBlock");
360 writer.WriteLine(" {");
361 writer.WriteLine($" format = new FourCC({(int) control.stateBlock.format}),");
362 writer.WriteLine($" byteOffset = {control.stateBlock.byteOffset},");
363 writer.WriteLine($" bitOffset = {control.stateBlock.bitOffset},");
364 writer.WriteLine($" sizeInBits = {control.stateBlock.sizeInBits}");
365 writer.WriteLine(" })");
366 if (control.hasDefaultState)
367 writer.WriteLine($" .WithDefaultState({control.m_DefaultState})");
368 if (control.m_MinValue != default || control.m_MaxValue != default)
369 writer.WriteLine($" .WithMinAndMax({control.m_MinValue}, {control.m_MaxValue})");
370 foreach (var processor in control.GetProcessors())
371 {
372 var isEditorWindowSpaceProcessor = processor is EditorWindowSpaceProcessor;
373 if (isEditorWindowSpaceProcessor)
374 writer.WriteLine(" #if UNITY_EDITOR");
375
376 var processorType = processor.GetType().FullName.Replace("+", ".");
377 var valueType = InputProcessor.GetValueTypeFromType(processor.GetType());
378 var fieldInits = processor.GetInitializersForPublicPrimitiveTypeFields();
379
380 writer.WriteLine(
381 $" .WithProcessor<InputProcessor<{valueType}>, {valueType}>(new {processorType}{fieldInits})");
382
383 if (isEditorWindowSpaceProcessor)
384 writer.WriteLine(" #endif");
385 }
386
387 writer.WriteLine(" .Finish();");
388
389 if (control is KeyControl key)
390 writer.WriteLine($"{controlVariableName}.keyCode = UnityEngine.InputSystem.Key.{key.keyCode};");
391 else if (control is DpadControl.DpadAxisControl dpadAxis)
392 writer.WriteLine($"{controlVariableName}.component = {dpadAxis.component};");
393
394 writer.WriteLine($"return {controlVariableName};");
395 writer.EndBlock();
396 }
397
398 private static string MakeControlVariableName(InputControl control)
399 {
400 return "ctrl" + CSharpCodeHelpers.MakeIdentifier(control.path);
401 }
402
403 private static void EmitControlGetterInitializers(this InputActionCodeGenerator.Writer writer, InputControl control,
404 string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlGetterPropertyTable)
405 {
406 var type = control.GetType();
407 if (!controlGetterPropertyTable.TryGetValue(type, out var controlGetterProperties))
408 {
409 controlGetterProperties = GetControlGetterProperties(type);
410 controlGetterPropertyTable[type] = controlGetterProperties;
411 }
412
413 foreach (var property in controlGetterProperties)
414 {
415 var value = (InputControl)property.GetValue(control);
416 if (value == null)
417 continue;
418 writer.WriteLine($"{controlVariableName}.{property.Name} = {MakeControlVariableName(value)};");
419 }
420 }
421
422 private static void EmitControlArrayInitializers(this InputActionCodeGenerator.Writer writer, InputControl control,
423 string controlVariableName, Dictionary<Type, List<PropertyInfo>> controlArrayPropertyTable)
424 {
425 var type = control.GetType();
426 if (!controlArrayPropertyTable.TryGetValue(type, out var controlArrayProperties))
427 {
428 controlArrayProperties = GetControlArrayProperties(type);
429 controlArrayPropertyTable[type] = controlArrayProperties;
430 }
431
432 foreach (var property in controlArrayProperties)
433 {
434 var array = (Array)property.GetValue(control);
435 if (array == null)
436 continue;
437 var arrayLength = array.Length;
438 var arrayElementType = array.GetType().GetElementType();
439 writer.WriteLine($"{controlVariableName}.{property.Name} = new {arrayElementType.FullName.Replace('+','.')}[{arrayLength}];");
440
441 for (var i = 0; i < arrayLength; ++i)
442 {
443 var value = (InputControl)array.GetValue(i);
444 if (value == null)
445 continue;
446 writer.WriteLine($"{controlVariableName}.{property.Name}[{i}] = {MakeControlVariableName(value)};");
447 }
448 }
449 }
450
451 private static List<PropertyInfo> GetControlGetterProperties(Type type)
452 {
453 return type.GetProperties(BindingFlags.Instance | BindingFlags.Public)
454 .Where(x => typeof(InputControl).IsAssignableFrom(x.PropertyType) && x.CanRead && x.CanWrite &&
455 x.GetIndexParameters().LengthSafe() == 0 && x.Name != "device" && x.Name != "parent").ToList();
456 }
457
458 private static List<PropertyInfo> GetControlArrayProperties(Type type)
459 {
460 return type.GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public)
461 .Where(x => x.PropertyType.IsArray && typeof(InputControl).IsAssignableFrom(x.PropertyType.GetElementType()) && x.CanRead && x.CanWrite &&
462 x.GetIndexParameters().LengthSafe() == 0).ToList();
463 }
464 }
465}
466#endif // UNITY_EDITOR