A game about forced loneliness, made by TACStudios
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Linq;
5using System.Xml.Linq;
6using Unity.VisualScripting;
7using UnityEditor;
8using UnityEditor.Build;
9using UnityEditor.Build.Reporting;
10using UnityEditor.Callbacks;
11using UnityEditor.SceneManagement;
12using UnityEngine;
13using UnityEngine.SceneManagement;
14
15internal class LinkerCreator : IPreprocessBuildWithReport
16{
17 private static string linkerPath => Path.Combine(BoltCore.Paths.persistentGenerated, "link.xml");
18
19 public int callbackOrder { get; }
20
21 private static ManagedStrippingLevel GetManagedStrippingLevel(BuildTargetGroup buildTarget)
22 {
23#if UNITY_2023_1_OR_NEWER
24 var namedBuildTarget = UnityEditor.Build.NamedBuildTarget.FromBuildTargetGroup(buildTarget);
25 return PlayerSettings.GetManagedStrippingLevel(namedBuildTarget);
26#else
27 return PlayerSettings.GetManagedStrippingLevel(buildTarget);
28#endif
29 }
30
31 public void OnPreprocessBuild(BuildReport report)
32 {
33 if (VSUsageUtility.isVisualScriptingUsed)
34 {
35 try
36 {
37 if (GetManagedStrippingLevel(EditorUserBuildSettings.selectedBuildTargetGroup) !=
38 ManagedStrippingLevel.Disabled)
39 {
40 GenerateLinker();
41 }
42 }
43 catch (Exception ex)
44 {
45 Debug.LogException(ex);
46
47 DeleteLinker();
48 }
49 }
50 }
51
52 [PostProcessBuild]
53 private static void OnPostprocessBuild(BuildTarget buildTarget, string path)
54 {
55 if (VSUsageUtility.isVisualScriptingUsed)
56 {
57 DeleteLinker();
58 }
59 }
60
61 private static void DeleteLinker()
62 {
63 PathUtility.DeleteProjectFileIfExists(linkerPath, true);
64 }
65
66 // Automatically generates the link.xml file to prevent stripping.
67 // Currently only used for plugin assemblies, because blanket preserving
68 // all setting assemblies sometimes causes the IL2CPP process to fail.
69 // For settings assemblies, the AOT stubs are good enough to fool
70 // the static code analysis without needing this full coverage.
71 // https://docs.unity3d.com/Manual/iphone-playerSizeOptimization.html
72 // However, for FullSerializer, we need to preserve our custom assemblies.
73 // This is mostly because IL2CPP will attempt to transform non-public
74 // property setters used in deserialization into read-only accessors
75 // that return false on PropertyInfo.CanWrite, but only in stripped builds.
76 // Therefore, in stripped builds, FS will skip properties that should be
77 // deserialized without any error (and that took hours of debugging to figure out).
78 public static void GenerateLinker()
79 {
80 var linker = new XDocument();
81
82 var linkerNode = new XElement("linker");
83
84 if (!PluginContainer.initialized)
85 PluginContainer.Initialize();
86
87 foreach (var pluginAssembly in PluginContainer.plugins
88 .SelectMany(plugin => plugin.GetType()
89 .GetAttributes<PluginRuntimeAssemblyAttribute>()
90 .Select(a => a.assemblyName))
91 .Distinct())
92 {
93 var assemblyNode = new XElement("assembly");
94 var fullnameAttribute = new XAttribute("fullname", pluginAssembly);
95 var preserveAttribute = new XAttribute("preserve", "all");
96 assemblyNode.Add(fullnameAttribute);
97 assemblyNode.Add(preserveAttribute);
98 linkerNode.Add(assemblyNode);
99 }
100
101 linker.Add(linkerNode);
102
103 AddCustomNodesToLinker(linkerNode, PluginContainer.plugins);
104
105 PathUtility.CreateDirectoryIfNeeded(BoltCore.Paths.transientGenerated);
106
107 PathUtility.DeleteProjectFileIfExists(linkerPath, true);
108
109 // Using ToString instead of Save to omit the <?xml> declaration,
110 // which doesn't appear in the Unity documentation page for the linker.
111 File.WriteAllText(linkerPath, linker.ToString());
112 }
113
114 private static void AddCustomNodesToLinker(XElement linkerNode, IEnumerable<Plugin> plugins)
115 {
116 var types = FindAllCustomTypes();
117
118 var customTypes = types.Where(t => !plugins.Any(p => t.Assembly == p.runtimeAssembly));
119
120 foreach (var type in customTypes)
121 {
122 var userAssembly = type.Assembly.GetName().Name;
123
124 var assemblyNode = new XElement("assembly");
125 var fullnameAttribute = new XAttribute("fullname", userAssembly);
126 var preserveAttribute = new XAttribute("preserve", "all");
127
128 assemblyNode.Add(fullnameAttribute);
129
130 var customType = new XElement("type");
131 var fullnameType = new XAttribute("fullname", type.FullName);
132
133 customType.Add(fullnameType);
134 customType.Add(preserveAttribute);
135
136 assemblyNode.Add(customType);
137
138 linkerNode.Add(assemblyNode);
139 }
140 }
141
142 private static void ProcessSubGraphs(HashSet<Type> types, SubgraphUnit subgraph)
143 {
144 foreach (var unit in subgraph.nest.graph.units)
145 {
146 AddTypeToHashSet(types, unit);
147 }
148 }
149
150 private static void AddTypeToHashSet(HashSet<Type> types, IUnit unit)
151 {
152 if (unit.GetType() == typeof(SubgraphUnit))
153 {
154 ProcessSubGraphs(types, (SubgraphUnit)unit);
155 }
156 else
157 {
158 types.Add(unit.GetType());
159 }
160 }
161
162 private static HashSet<Type> FindGraphsOnAssets()
163 {
164 var scriptGraphAssets = AssetUtility.GetAllAssetsOfType<ScriptGraphAsset>();
165
166 var types = new HashSet<Type>();
167
168 var index = 0;
169 var total = scriptGraphAssets.Count();
170
171 foreach (var scriptGraphAsset in scriptGraphAssets)
172 {
173 if (EditorUtility.DisplayCancelableProgressBar($"Processing on assets {index}/{total}",
174 $"Asset {scriptGraphAsset.name}", (float)index / (float)total))
175 {
176 break;
177 }
178
179 index++;
180
181 if (scriptGraphAsset.graph != null)
182 {
183 foreach (var unit in scriptGraphAsset.graph.units)
184 {
185 AddTypeToHashSet(types, unit);
186 }
187 }
188 }
189
190 EditorUtility.ClearProgressBar();
191
192 return types;
193 }
194
195 private static HashSet<Type> FindGraphsOnScenes(bool includeGraphAssets)
196 {
197 var activeScenePath = SceneManager.GetActiveScene().path;
198 var scenePaths = EditorBuildSettings.scenes.Select(s => s.path).ToArray();
199
200 var index = 0;
201 var total = scenePaths.Count();
202 var types = new HashSet<Type>();
203
204 foreach (var scenePath in scenePaths)
205 {
206 index++;
207
208 if (EditorUtility.DisplayCancelableProgressBar($"Processing scenes {index} / {total}", $"Scene {scenePath}",
209 (float)index / (float)total))
210 {
211 break;
212 }
213
214 if (!string.IsNullOrEmpty(scenePath))
215 {
216 EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
217
218 var scriptMachines = UnityObjectUtility.FindObjectsOfTypeIncludingInactive<ScriptMachine>();
219
220 foreach (var scriptMachine in scriptMachines)
221 {
222 if (scriptMachine.nest != null &&
223 (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) ||
224 scriptMachine.nest.source == GraphSource.Embed)
225 {
226 foreach (var unit in scriptMachine.graph.units)
227 {
228 AddTypeToHashSet(types, unit);
229 }
230 }
231 }
232 }
233 }
234
235 if (!string.IsNullOrEmpty(activeScenePath))
236 {
237 EditorSceneManager.OpenScene(activeScenePath);
238 }
239
240 GC.Collect();
241
242 EditorUtility.ClearProgressBar();
243
244 return types;
245 }
246
247 private static HashSet<Type> FindGraphsOnPrefabs(bool includeGraphAssets)
248 {
249 var types = new HashSet<Type>();
250
251 var files = System.IO.Directory.GetFiles(Application.dataPath, "*.prefab",
252 System.IO.SearchOption.AllDirectories);
253
254 var index = 0;
255 var total = files.Count();
256
257 var currentScene = EditorSceneManager.GetActiveScene();
258 var scenePath = currentScene.path;
259
260 EditorSceneManager.NewScene(NewSceneSetup.EmptyScene);
261
262 foreach (var file in files)
263 {
264 index++;
265
266 if (EditorUtility.DisplayCancelableProgressBar($"Processing prefabs {index} / {total}", $"Prefab {file}",
267 (float)index / (float)total))
268 {
269 break;
270 }
271
272 var prefabPath = file.Replace(Application.dataPath, "Assets");
273
274 var prefab = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) as GameObject;
275
276 if (prefab != null)
277 {
278 FindGraphInPrefab(types, prefab, includeGraphAssets);
279 prefab = null;
280
281 EditorUtility.UnloadUnusedAssetsImmediate(true);
282 }
283 }
284
285 EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);
286
287 EditorUtility.UnloadUnusedAssetsImmediate(true);
288
289 GC.Collect();
290
291 EditorUtility.ClearProgressBar();
292
293 return types;
294 }
295
296 private static void FindGraphInPrefab(HashSet<Type> types, GameObject gameObject, bool includeGraphAssets)
297 {
298 var scriptMachines = gameObject.GetComponents<ScriptMachine>();
299
300 foreach (var scriptMachine in scriptMachines)
301 {
302 if (scriptMachine.nest != null &&
303 (scriptMachine.nest.source == GraphSource.Macro && includeGraphAssets) ||
304 scriptMachine.nest.source == GraphSource.Embed)
305 {
306 foreach (var unit in scriptMachine.graph.units)
307 {
308 AddTypeToHashSet(types, unit);
309 }
310 }
311 }
312
313 foreach (Transform child in gameObject.transform)
314 {
315 FindGraphInPrefab(types, child.gameObject, includeGraphAssets);
316 }
317 }
318
319 private static HashSet<Type> FindAllCustomTypes()
320 {
321 var types = new HashSet<Type>();
322
323 var settings = (List<bool>)BoltCore.Configuration.GetMetadata("LinkerPropertyProviderSettings").value;
324
325 if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets])
326 {
327 types.AddRange(FindGraphsOnAssets());
328 }
329
330 var includeGraphAssets = !settings[(int)BoltCoreConfiguration.LinkerScanTarget.GraphAssets];
331
332 if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedSceneGraphs])
333 {
334 types.AddRange(FindGraphsOnScenes(includeGraphAssets));
335 }
336
337 if (settings[(int)BoltCoreConfiguration.LinkerScanTarget.EmbeddedPrefabGraphs])
338 {
339 types.AddRange(FindGraphsOnPrefabs(includeGraphAssets));
340 }
341
342 return types;
343 }
344}