-
Notifications
You must be signed in to change notification settings - Fork 0
/
AngularExposedExport.cs
251 lines (229 loc) · 10.4 KB
/
AngularExposedExport.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
#nullable enable
#if UNITY_EDITOR
using System;
using System.CodeDom.Compiler;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEditor;
namespace UnityAngularBridge.SwaggerAttribute
{
/// <summary>
/// Export AngularExposed Methods to TypeScript as some type of SwaggerClient to MyDocuments folder.
/// This to mostly call SendMessage() methods of Unity with different parameter values,
/// See: https://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html
/// .
/// We want a predefined GameObject, since generating this at runtime is not possible.
/// It could be possible to generate before start of application, but with multiple scenes it will override this file and give incorrect values,
/// since it can only read the active scene unfortunately.
/// That is why the decision was made to predefine GameObject names.
/// If necessary in the future, we could make it so that Angular can pass a GameObject name by itself and just has a default value from this generator otherwise.
/// .
/// TODO: export file to frontend by placing this in a NPM package.
/// </summary>
[InitializeOnLoad]
public class AngularExposedExport
{
private static readonly string _angularTsClientFileName = "UnityClient.ts";
/// <summary>
/// The tabstring to use, since its for TypeScript it will be 2 spaces.
/// </summary>
private static readonly string _tabString = " ";
static AngularExposedExport()
{
GenerateUnityClient();
}
#region GenerateUnityClient
/// <summary>
/// Generates TypeScript Client for Angular.
/// </summary>
private static void GenerateUnityClient()
{
using IndentedTextWriter writer = new(new StreamWriter(Path.Combine(GetMyDocumentsPath(), _angularTsClientFileName)) { AutoFlush = true }, _tabString);
AddAutoGeneratedFileCommentLines(writer);
AddInjectibleClassWithImportLines(writer);
GetAndAddMethodsLines(writer);
AddClassClosingLines(writer);
AddIUnityInstanceInterfaceLines(writer);
}
/// <summary>
/// Create autgenerated Text including eslint-disable text.
/// //----------------------
/// // <auto-generated>
/// // Generated using AngularExposedExport.cs by Jim Halewijn in SmartTwinEditor project.
/// // </auto-generated>
/// //----------------------
/// //
/// /* eslint-disable */
/// //.
/// </summary>
private static void AddAutoGeneratedFileCommentLines(IndentedTextWriter writer)
{
writer.WriteLine("//----------------------");
writer.WriteLine("// <auto-generated>");
writer.WriteLine("// Generated using AngularExposedExport.cs by Jim Halewijn in SmartTwinEditor project.");
writer.WriteLine("// </auto-generated>");
writer.WriteLine("//----------------------");
writer.WriteLine(string.Empty);
writer.WriteLine("/* eslint-disable */");
writer.WriteLine(string.Empty);
}
/// <summary>
/// import { Injectable } from '@angular/core';
/// .
/// @Injectable()
/// export class UnityClient {.
/// </summary>
private static void AddInjectibleClassWithImportLines(IndentedTextWriter writer)
{
writer.WriteLine("import { Injectable } from \"@angular/core\";");
writer.WriteLine(string.Empty);
writer.WriteLine("@Injectable()");
writer.WriteLine("export class UnityClient {");
writer.Indent++;
}
private static void GetAndAddMethodsLines(IndentedTextWriter writer)
{
// Get Assembly
Assembly assembly = Assembly.GetExecutingAssembly();
// Get Classes Types
IEnumerable<Type>? publicClasses = assembly.GetExportedTypes().Where(p => p.IsClass);
foreach (Type? type in publicClasses)
{
// Get Class Methods with AngularExposedAttribute
IEnumerable<MethodInfo>? methodInfos = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static)
.Where(m => m.GetCustomAttributes(typeof(AngularExposedAttribute), false).Length > 0);
foreach (MethodInfo? methodInfo in methodInfos)
{
// Get monobehaviour name
object[] methodAttributes = methodInfo.GetCustomAttributes(typeof(AngularExposedAttribute), false);
string gameObjectName = string.Empty;
foreach (AngularExposedAttribute attribute in methodAttributes.Cast<AngularExposedAttribute>())
{
gameObjectName = attribute.GameObjectName;
}
// Get methodName
string methodName = methodInfo.Name;
// Get first parameter type and name if there is one
string parameterType = string.Empty;
string parameterName = string.Empty;
ParameterInfo[] parameters = methodInfo.GetParameters();
if (parameters.Length > 1)
{
throw new InvalidOperationException($"Method {methodName} is only allowed to have 1 argument");
}
else if (parameters.Length == 1)
{
parameterType = ParameterTypeToTypescriptType(parameters[0].ParameterType);
parameterName = parameters[0].Name;
}
AddMethodLines(writer, gameObjectName, methodName, parameterType, parameterName);
}
}
}
/// <summary>
/// public [GO Name]_[objectMethodName]([ParameterType] [ParameterName]): void {
/// this.unityInstance?.SendMessage([GO Name], [objectMethodName], data);
/// }.
/// .
/// </summary>
/// <param name="gameObjectName">GameObject name.</param>
/// <param name="methodName">Method name.</param>
/// <param name="parameterType">Parameter Type name.</param>
/// <param name="parameterName">Parameter name.</param>
private static void AddMethodLines(IndentedTextWriter writer, string gameObjectName, string methodName, string? parameterType, string? parameterName)
{
bool hasParameter = !string.IsNullOrEmpty(parameterName);
if (hasParameter)
{
writer.WriteLine($"public {FirstCharToLowerCase(gameObjectName)}_{methodName}(unityInstance: IUnityInstance, {parameterName}: {parameterType}" + "): void {");
writer.Indent++;
writer.WriteLine($"unityInstance?.SendMessage(\"{gameObjectName}\", \"{methodName}\", {parameterName});");
writer.Indent--;
writer.WriteLine("}");
writer.WriteLine(string.Empty);
}
else
{
writer.WriteLine($"public {FirstCharToLowerCase(gameObjectName)}_{methodName}(unityInstance: IUnityInstance" + "): void {");
writer.Indent++;
writer.WriteLine($"unityInstance?.SendMessage(\"{gameObjectName}\", \"{methodName}\");");
writer.Indent--;
writer.WriteLine("}");
writer.WriteLine(string.Empty);
}
}
/// <summary>
/// Line to close the class:
/// }.
/// </summary>
private static void AddClassClosingLines(IndentedTextWriter writer)
{
writer.Indent--;
writer.WriteLine("}");
writer.WriteLine(string.Empty);
}
/// <summary>
/// Add IUnityInstance interface lines.
/// export interface IUnityInstance {
/// SendMessage(gameObjectName: string, methodName: string, data?: unknown): void;
/// SetFullscreen(value: number): void;
/// Quit(): Promise<unknown>;
/// }
/// </summary>
/// <param name="writer">Writer.</param>
private static void AddIUnityInstanceInterfaceLines(IndentedTextWriter writer)
{
writer.WriteLine("export interface IUnityInstance {");
writer.Indent++;
writer.WriteLine("SendMessage(gameObjectName: string, methodName: string, data?: unknown): void;");
writer.WriteLine("SetFullscreen(value: number): void;");
writer.WriteLine("Quit(): Promise<unknown>;");
writer.Indent--;
writer.WriteLine("}");
}
#endregion
#region csharp to ts types conversion
/// <summary>
/// Unity only supports string and number for TypeScript.
/// </summary>
/// <param name="parameterType">Objectype of parameter.</param>
/// <returns>TypeScript type as string.</returns>
/// <exception cref="InvalidOperationException">Unsupported type.</exception>
private static string ParameterTypeToTypescriptType(object parameterType)
{
string parameterTypeString = parameterType.ToString();
return parameterTypeString switch
{
"System.String" => "string",
"System.Int32" => "number",
"System.Single" => "number",
_ => throw new InvalidOperationException($"ParameterType {parameterType} is not supported." +
$"Only string and number variants are allowed."),
};
}
#endregion
#region pathUtilities
/// <summary>
/// Get MyDocuments path.
/// </summary>
/// <returns>MyDocuments path.</returns>
private static string GetMyDocumentsPath()
{
return Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
}
#endregion
#region stringUtilities
private static string? FirstCharToLowerCase(string? str)
{
if (!string.IsNullOrEmpty(str) && char.IsUpper(str[0]))
{
return str.Length == 1 ? char.ToLower(str[0]).ToString() : char.ToLower(str[0]) + str[1..];
}
return str;
}
#endregion
}
}
#endif