Arma Reforger Explorer 1.7.0.54
Arma Reforger Code Explorer by Zeroy - Thanks to MisterOutofTime
Loading...
Searching...
No Matches
SCR_SocketSpawnerPlugin.c
Go to the documentation of this file.
1#ifdef WORKBENCH
5[WorkbenchPluginAttribute(name: "Socket Spawner", category: "Tools", description: "Spawn prefab children at GUID-based socket bones for selected .et prefabs", awesomeFontCode: 0xf1b3, wbModules: {"ResourceManager"})]
6class SCR_SocketSpawnerPlugin : WorkbenchPlugin
7{
8 // Options
9 [Attribute(defvalue: "0", UIWidgets.CheckBox, "Clear existing children of prefab (remove prefab members)")]
10 protected bool m_bClearChildren;
11
12 [Attribute(defvalue: "1", UIWidgets.CheckBox, "Show detailed output")]
13 protected bool m_bVerboseOutput;
14
15 // Result tracking (integrated from SCR_SocketSpawnerResult)
16 protected int m_iProcessedPrefabs;
17 protected int m_iCreatedEntities;
18 protected int m_iTotalSockets;
19 protected ref array<string> m_aErrors;
20
21 //------------------------------------------------------------------------------------------------
23 [ButtonAttribute("Execute")]
24 protected void Execute()
25 {
26 Run();
27 }
28
29 //------------------------------------------------------------------------------------------------
31 [ButtonAttribute("Close")]
32 protected bool Close()
33 {
34 return false;
35 }
36
37 //------------------------------------------------------------------------------------------------
39 override void Configure()
40 {
41 if (Workbench.ScriptDialog(
42 "Socket Spawner",
43 "Process selected prefab (.et) assets from the Resource Browser.\n\n" +
44 "Rules:\n" +
45 "• Scans Animation bones with names starting 'socket_'\n" +
46 "• Expected pattern: socket_<GUID16>_<suffix> e.g., socket_B57077CC7F07D01D_01\n" +
47 "• Spawns a child entity using ResourceName \"{<GUID16>}\" at that bone's transform\n\n" +
48 "Options:\n" +
49 "• Clear existing children: remove all current prefab children before spawning\n" +
50 "• Verbose output: print detailed progress to the console",
51 this) == 0)
52 return;
53 }
54
55 //------------------------------------------------------------------------------------------------
57 override void Run()
58 {
59 // Initialize result tracking
60 m_iProcessedPrefabs = 0;
61 m_iCreatedEntities = 0;
62 m_iTotalSockets = 0;
63 m_aErrors = {};
64
65 array<ResourceName> selectedEtPrefabs = {};
66 if (!ProcessSelectedPrefabs(selectedEtPrefabs))
67 {
68 ShowResults();
69 return;
70 }
71
72 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
73 if (!worldEditor)
74 {
75 m_aErrors.Insert("Failed to get WorldEditor module");
76 ShowResults();
77 return;
78 }
79
80 foreach (ResourceName prefabRes : selectedEtPrefabs)
81 {
82 if (!ProcessSinglePrefab(prefabRes, worldEditor, m_bClearChildren))
83 continue;
84
85 m_iProcessedPrefabs++;
86 }
87
88 // Save changes to prefab files
89 worldEditor.Save();
90
91 ShowResults();
92 }
93
94 //------------------------------------------------------------------------------------------------
98 protected void GetSelectedResources(out array<ResourceName> selectedResources)
99 {
100 selectedResources = {};
101
102 // Get ResourceManager module
103 ResourceManager resourceManager = Workbench.GetModule(ResourceManager);
104 if (!resourceManager)
105 return;
106
107 // Get list of currently selected resources
108 resourceManager.GetResourceBrowserSelection(selectedResources.Insert, true);
109 }
110
111 //------------------------------------------------------------------------------------------------
115 protected bool ProcessSelectedPrefabs(out array<ResourceName> outPrefabs)
116 {
117 outPrefabs = {};
118
119 // Get list of currently selected resources
120 array<ResourceName> selectedResources = {};
121 GetSelectedResources(selectedResources);
122
123 if (selectedResources.IsEmpty())
124 {
125 m_aErrors.Insert("No resources selected in Resource Browser. Select one or more .et prefabs.");
126 return false;
127 }
128
129 // Filter for .et files (prefabs)
130 foreach (ResourceName resourceName : selectedResources)
131 {
132 string resourcePath = resourceName.GetPath();
133 if (resourcePath.EndsWith(".et"))
134 {
135 outPrefabs.Insert(resourceName);
136 }
137 }
138
139 if (outPrefabs.IsEmpty())
140 {
141 m_aErrors.Insert("No .et prefab files in selection.");
142 return false;
143 }
144
145 if (m_bVerboseOutput)
146 PrintFormat("SocketSpawner: %1 prefab(s) queued from selection.", outPrefabs.Count());
147
148 return true;
149 }
150
151 //------------------------------------------------------------------------------------------------
157 protected bool ProcessSinglePrefab(ResourceName prefabResource, WorldEditor worldEditor, bool clearExistingChildren)
158 {
159 if (prefabResource.IsEmpty())
160 {
161 m_aErrors.Insert("Invalid prefab resource");
162 return false;
163 }
164
165 // Load prefab resource
166 Resource resource = Resource.Load(prefabResource);
167 if ( !resource.IsValid())
168 {
169 m_aErrors.Insert(string.Format("Failed to load prefab: %1", prefabResource));
170 return false;
171 }
172
173 WorldEditorAPI api = worldEditor.GetApi();
174 if (!api)
175 {
176 m_aErrors.Insert("Failed to get WorldEditor API");
177 return false;
178 }
179
180 // Create a working entity source from the prefab
181 api.BeginEntityAction();
182 IEntitySource entitySource = api.CreateEntity(prefabResource, "", api.GetCurrentEntityLayerId(), null, vector.Zero, vector.Zero);
183
184 // Optionally clear existing children (remove prefab members from ancestor)
185 array<IEntitySource> childEntitySources;
186 if (clearExistingChildren && entitySource && entitySource.GetNumChildren() > 0)
187 {
188 IEntitySource actualPrefab = IEntitySource.Cast(entitySource.GetAncestor());
189 childEntitySources = {};
190 for (int i, count = entitySource.GetNumChildren(); i < count; i++)
191 {
192 childEntitySources.Insert(actualPrefab.GetChild(i));
193 }
194
195 if (!childEntitySources.IsEmpty()) // only call if non-empty
196 api.RemovePrefabMembers(childEntitySources);
197 }
198 api.EndEntityAction();
199
200 if (!entitySource)
201 {
202 m_aErrors.Insert(string.Format("Failed to get entity source for: %1", prefabResource));
203 return false;
204 }
205
206 // Build bone map
208 if (!RefreshBoneMatrixMap(entitySource, boneMatrixMap, api))
209 {
210 if (m_bVerboseOutput)
211 PrintFormat("No bones found in prefab: %1", prefabResource);
212 // Cleanup entity source
213 api.BeginEntityAction();
214 api.DeleteEntities({entitySource});
215 api.EndEntityAction();
216 return true; // not an error
217 }
218
219 // Iterate bones; look for "socket_" prefix and GUID pattern
220 int createdInThisPrefab = 0;
221 int validSocketCount = 0;
222
223 for (int boneIndex = 0, boneCount = boneMatrixMap.Count(); boneIndex < boneCount; boneIndex++)
224 {
225 string boneName = boneMatrixMap.GetKey(boneIndex);
226
227 // Skip common root
228 if (boneName == "Scene_Root")
229 continue;
230
231 if (!boneName.StartsWith("socket_"))
232 continue;
233
234 string guid16;
235 string suffix;
236 if (!ExtractGuidFromBoneName(boneName, guid16, suffix))
237 {
238 // Warn when verbose only
239 if (m_bVerboseOutput)
240 PrintFormat("SocketSpawner: Bone '%1' ignored - GUID16 not found.", boneName, level: LogLevel.WARNING);
241 continue;
242 }
243
244 validSocketCount++;
245
246 array<vector> matrices = boneMatrixMap.GetElement(boneIndex);
247 vector position = "0 0 0";
248 if (matrices && matrices.Count() >= 4)
249 position = matrices[3];
250
251 if (CreateEntityAtBone(boneName, guid16, position, matrices, worldEditor, entitySource))
252 {
253 createdInThisPrefab++;
254 m_iCreatedEntities++;
255 }
256 }
257
258 m_iTotalSockets += validSocketCount;
259
260 if (m_bVerboseOutput && createdInThisPrefab > 0)
261 PrintFormat("SocketSpawner: Created %1 entities in prefab %2", createdInThisPrefab, prefabResource);
262
263 // Cleanup working entity
264 api.BeginEntityAction();
265 api.DeleteEntities({entitySource});
266 api.EndEntityAction();
267
268 return true;
269 }
270
271 //------------------------------------------------------------------------------------------------
278 protected bool RefreshBoneMatrixMap(notnull IEntitySource entitySource, notnull out map<string, ref array<vector>> boneMatrixMap, WorldEditorAPI api)
279 {
280 boneMatrixMap.Clear();
281
282 api.BeginEntityAction();
283
284 IEntity entity = api.SourceToEntity(entitySource);
285 if (!entity)
286 {
287 if (m_bVerboseOutput)
288 PrintFormat("Could not resolve entity for: %1", entitySource.GetResourceName());
289 api.EndEntityAction();
290 return false;
291 }
292
293 Animation animation = entity.GetAnimation();
294 if (animation)
295 {
296 array<string> boneNames = {};
297 animation.GetBoneNames(boneNames);
298 vector mat[4];
299
300 foreach (string boneName : boneNames)
301 {
302 TNodeId boneIndex = animation.GetBoneIndex(boneName);
303 if (!animation.GetBoneMatrix(boneIndex, mat))
304 {
305 if (m_bVerboseOutput)
306 Print("Cannot get bone " + boneName + "'s matrix", LogLevel.WARNING);
307 continue;
308 }
309
310 array<vector> matrixArray = { mat[0], mat[1], mat[2], mat[3] };
311 boneMatrixMap.Insert(boneName, matrixArray);
312 }
313 }
314
315 api.EndEntityAction();
316
317 return boneMatrixMap.Count() > 0;
318 }
319
320 //------------------------------------------------------------------------------------------------
326 protected bool ExtractGuidFromBoneName(string boneName, out string guid16, out string suffix)
327 {
328 guid16 = "";
329 suffix = "";
330
331 if (!boneName)
332 return false;
333
334 // Convert to lowercase for case-insensitive matching
335 string lowerBoneName = boneName;
336 lowerBoneName.ToLower();
337
338 // Quick prefix check (case-insensitive)
339 if (!lowerBoneName.StartsWith("socket_"))
340 return false;
341
342 // Position after "socket_" (7 characters)
343 const int start = 7;
344
345 // Ensure at least 16 characters are available for GUID
346 if (lowerBoneName.Length() < start + 16)
347 return false;
348
349 // Extract exactly 16 characters
350 string candidate = lowerBoneName.Substring(start, 16);
351
352 // Validate all characters are hex using lookup
353 static const string HEX_CHARS = "0123456789abcdef";
354 for (int i = 0; i < 16; i++)
355 {
356 if (HEX_CHARS.IndexOf(candidate.Get(i)) == -1)
357 return false;
358 }
359
360 // Convert to uppercase
361 candidate.ToUpper();
362 guid16 = candidate;
363
364 // Extract suffix after GUID (if underscore exists at position start + 16)
365 int suffixStart = start + 16;
366 if (suffixStart < boneName.Length() && boneName.Get(suffixStart) == "_")
367 suffix = boneName.Substring(suffixStart + 1, boneName.Length() - (suffixStart + 1));
368 else
369 suffix = "";
370
371 return true;
372 }
373
374 //------------------------------------------------------------------------------------------------
383 protected bool CreateEntityAtBone(string boneName, string guid16, vector position, array<vector> boneMatrix, WorldEditor worldEditor, IEntitySource parentEntitySource)
384 {
385 WorldEditorAPI api = worldEditor.GetApi();
386 if (!api)
387 {
388 m_aErrors.Insert("Failed to get WorldEditor API");
389 return false;
390 }
391
392 // Compose resource name like "{B57077CC7F07D01D}"
393 ResourceName childRes = string.Format("{%1}", guid16);
394
395 // Create entity at position; orientation from bone matrix
396 api.BeginEntityAction();
397
398 vector zeroAngles = "0 0 0";
399 IEntitySource newEntitySource = api.CreateEntity(childRes, "", api.GetCurrentEntityLayerId(), null, position, zeroAngles);
400 if (!newEntitySource)
401 {
402 api.EndEntityAction();
403 m_aErrors.Insert(string.Format("Failed to create entity for socket %1 using resource %2", boneName, childRes));
404 return false;
405 }
406
407 // Extract rotation from bone matrix (right, up, dir) then to angles
408 vector mat3[3] = {
409 boneMatrix[0],
410 boneMatrix[1],
411 boneMatrix[2]
412 };
413 vector angles = Math3D.MatrixToAngles(mat3);
414
415 api.SetVariableValue(newEntitySource, null, "coords", position.ToString(false));
416 // Note: same Euler order as reference patterns
417 api.SetVariableValue(newEntitySource, null, "angles", string.Format("%1 %2 %3", angles[1], angles[0], angles[2]));
418
419 // Attach under prefab ancestor as a child
420 if (parentEntitySource && parentEntitySource.GetAncestor())
421 {
422 array<IEntitySource> toMove = { newEntitySource };
423 api.MoveEntitiesToPrefab(parentEntitySource, parentEntitySource.GetAncestor(), toMove);
424 }
425
426 api.EndEntityAction();
427
428 if (m_bVerboseOutput)
429 PrintFormat("SocketSpawner: Created child at '%1' using %2 at %3", boneName, childRes, position);
430
431 return true;
432 }
433
434 //------------------------------------------------------------------------------------------------
436 protected void ShowResults()
437 {
438 string summary = string.Format("Processed %1 prefabs, created %2 entities from %3 socket(s)",
439 m_iProcessedPrefabs, m_iCreatedEntities, m_iTotalSockets);
440
441 if (!m_aErrors.IsEmpty())
442 {
443 summary += "\nErrors:\n";
444 foreach (string e : m_aErrors)
445 summary += "- " + e + "\n";
446 }
447
448 SCR_WorkbenchHelper.PrintDialog(summary, "Socket Spawner Results", LogLevel.NORMAL);
449 }
450}
451
452//------------------------------------------------------------------------------------------------
455//------------------------------------------------------------------------------------------------
456[WorkbenchPluginAttribute(name: "Socket Spawner", category: "Tools", description: "Spawn prefab children at GUID-based socket bones for selected .et prefabs", awesomeFontCode: 0xf1b3, wbModules: {"WorldEditor"})]
457class SCR_SocketSpawnerPluginWE : SCR_SocketSpawnerPlugin
458{
459 //------------------------------------------------------------------------------------------------
462 override protected void GetSelectedResources(out array<ResourceName> selectedResources)
463 {
464 selectedResources = {};
465
466 // Get WorldEditor module
467 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
468 if (!worldEditor)
469 return;
470
471 // Get list of currently selected resources using WorldEditor API
472 worldEditor.GetResourceBrowserSelection(selectedResources.Insert, true);
473 }
474}
475#endif
GenerateFlowMaps WorkbenchPlugin WorkbenchPluginAttribute("Regenerate river flow-maps", "Generate and save/overwrite river flow-maps", "", "", {"WorldEditor"}, "", 0xf773)
Definition FlowmapTool.c:59
ref array< string > angles
bool Execute(notnull SCR_AIGroupUtilityComponent groupUtility, vector targetPosition, SCR_AIActivitySmokeCoverFeatureProperties smokeCoverProperties, notnull array< AIAgent > avoidAgents, notnull array< AIAgent > excludeAgents, int maxPositionCount=1, SCR_AIActivityBase contextActivity=null)
ResourceName resourceName
Definition SCR_AIGroup.c:66
vector position
override void Run()
proto native void Close()
override void Configure()
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
proto external Animation GetAnimation()
Object holding reference to resource. In destructor release the resource.
Definition Resource.c:25
static void PrintDialog(string message, string caption="", LogLevel level=LogLevel.WARNING)
Definition Types.c:486
proto void Print(void var, LogLevel level=LogLevel.NORMAL)
Prints content of variable to console/log.
LogLevel
Enum with severity of the logging message.
Definition LogLevel.c:14
proto void PrintFormat(string fmt, void param1=NULL, void param2=NULL, void param3=NULL, void param4=NULL, void param5=NULL, void param6=NULL, void param7=NULL, void param8=NULL, void param9=NULL, LogLevel level=LogLevel.NORMAL)
SCR_FieldOfViewSettings Attribute
int TNodeId
Node global id is a hash of name of the node.
Definition EnEntity.c:18