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
9 [
Attribute(defvalue:
"0",
UIWidgets.CheckBox,
"Clear existing children of prefab (remove prefab members)")]
10 protected bool m_bClearChildren;
13 protected bool m_bVerboseOutput;
16 protected int m_iProcessedPrefabs;
17 protected int m_iCreatedEntities;
18 protected int m_iTotalSockets;
19 protected ref array<string> m_aErrors;
32 protected bool Close()
41 if (Workbench.ScriptDialog(
43 "Process selected prefab (.et) assets from the Resource Browser.\n\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" +
49 "• Clear existing children: remove all current prefab children before spawning\n" +
50 "• Verbose output: print detailed progress to the console",
60 m_iProcessedPrefabs = 0;
61 m_iCreatedEntities = 0;
65 array<ResourceName> selectedEtPrefabs = {};
66 if (!ProcessSelectedPrefabs(selectedEtPrefabs))
72 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
75 m_aErrors.Insert(
"Failed to get WorldEditor module");
82 if (!ProcessSinglePrefab(prefabRes, worldEditor, m_bClearChildren))
85 m_iProcessedPrefabs++;
98 protected void GetSelectedResources(out array<ResourceName> selectedResources)
100 selectedResources = {};
103 ResourceManager resourceManager = Workbench.GetModule(ResourceManager);
104 if (!resourceManager)
108 resourceManager.GetResourceBrowserSelection(selectedResources.Insert,
true);
115 protected bool ProcessSelectedPrefabs(out array<ResourceName> outPrefabs)
120 array<ResourceName> selectedResources = {};
121 GetSelectedResources(selectedResources);
123 if (selectedResources.IsEmpty())
125 m_aErrors.Insert(
"No resources selected in Resource Browser. Select one or more .et prefabs.");
133 if (resourcePath.EndsWith(
".et"))
139 if (outPrefabs.IsEmpty())
141 m_aErrors.Insert(
"No .et prefab files in selection.");
145 if (m_bVerboseOutput)
146 PrintFormat(
"SocketSpawner: %1 prefab(s) queued from selection.", outPrefabs.Count());
157 protected bool ProcessSinglePrefab(
ResourceName prefabResource, WorldEditor worldEditor,
bool clearExistingChildren)
159 if (prefabResource.IsEmpty())
161 m_aErrors.Insert(
"Invalid prefab resource");
167 if ( !resource.IsValid())
169 m_aErrors.Insert(
string.Format(
"Failed to load prefab: %1", prefabResource));
176 m_aErrors.Insert(
"Failed to get WorldEditor API");
181 api.BeginEntityAction();
182 IEntitySource entitySource = api.CreateEntity(prefabResource,
"", api.GetCurrentEntityLayerId(), null,
vector.Zero,
vector.Zero);
185 array<IEntitySource> childEntitySources;
186 if (clearExistingChildren && entitySource && entitySource.GetNumChildren() > 0)
189 childEntitySources = {};
190 for (
int i, count = entitySource.GetNumChildren(); i < count; i++)
192 childEntitySources.Insert(actualPrefab.GetChild(i));
195 if (!childEntitySources.IsEmpty())
196 api.RemovePrefabMembers(childEntitySources);
198 api.EndEntityAction();
202 m_aErrors.Insert(
string.Format(
"Failed to get entity source for: %1", prefabResource));
208 if (!RefreshBoneMatrixMap(entitySource, boneMatrixMap, api))
210 if (m_bVerboseOutput)
211 PrintFormat(
"No bones found in prefab: %1", prefabResource);
213 api.BeginEntityAction();
214 api.DeleteEntities({entitySource});
215 api.EndEntityAction();
220 int createdInThisPrefab = 0;
221 int validSocketCount = 0;
223 for (
int boneIndex = 0, boneCount = boneMatrixMap.Count(); boneIndex < boneCount; boneIndex++)
225 string boneName = boneMatrixMap.GetKey(boneIndex);
228 if (boneName ==
"Scene_Root")
231 if (!boneName.StartsWith(
"socket_"))
236 if (!ExtractGuidFromBoneName(boneName, guid16, suffix))
239 if (m_bVerboseOutput)
240 PrintFormat(
"SocketSpawner: Bone '%1' ignored - GUID16 not found.", boneName, level:
LogLevel.WARNING);
246 array<vector> matrices = boneMatrixMap.GetElement(boneIndex);
248 if (matrices && matrices.Count() >= 4)
251 if (CreateEntityAtBone(boneName, guid16,
position, matrices, worldEditor, entitySource))
253 createdInThisPrefab++;
254 m_iCreatedEntities++;
258 m_iTotalSockets += validSocketCount;
260 if (m_bVerboseOutput && createdInThisPrefab > 0)
261 PrintFormat(
"SocketSpawner: Created %1 entities in prefab %2", createdInThisPrefab, prefabResource);
264 api.BeginEntityAction();
265 api.DeleteEntities({entitySource});
266 api.EndEntityAction();
278 protected bool RefreshBoneMatrixMap(notnull
IEntitySource entitySource, notnull out
map<
string, ref array<vector>> boneMatrixMap,
WorldEditorAPI api)
280 boneMatrixMap.Clear();
282 api.BeginEntityAction();
284 IEntity entity = api.SourceToEntity(entitySource);
287 if (m_bVerboseOutput)
288 PrintFormat(
"Could not resolve entity for: %1", entitySource.GetResourceName());
289 api.EndEntityAction();
296 array<string> boneNames = {};
297 animation.GetBoneNames(boneNames);
300 foreach (
string boneName : boneNames)
302 TNodeId boneIndex = animation.GetBoneIndex(boneName);
303 if (!animation.GetBoneMatrix(boneIndex, mat))
305 if (m_bVerboseOutput)
306 Print(
"Cannot get bone " + boneName +
"'s matrix",
LogLevel.WARNING);
310 array<vector> matrixArray = { mat[0], mat[1], mat[2], mat[3] };
311 boneMatrixMap.Insert(boneName, matrixArray);
315 api.EndEntityAction();
317 return boneMatrixMap.Count() > 0;
326 protected bool ExtractGuidFromBoneName(
string boneName, out
string guid16, out
string suffix)
335 string lowerBoneName = boneName;
336 lowerBoneName.ToLower();
339 if (!lowerBoneName.StartsWith(
"socket_"))
346 if (lowerBoneName.Length() < start + 16)
350 string candidate = lowerBoneName.Substring(start, 16);
353 static const string HEX_CHARS =
"0123456789abcdef";
354 for (
int i = 0; i < 16; i++)
356 if (HEX_CHARS.IndexOf(candidate.Get(i)) == -1)
365 int suffixStart = start + 16;
366 if (suffixStart < boneName.Length() && boneName.Get(suffixStart) ==
"_")
367 suffix = boneName.Substring(suffixStart + 1, boneName.Length() - (suffixStart + 1));
383 protected bool CreateEntityAtBone(
string boneName,
string guid16,
vector position, array<vector> boneMatrix, WorldEditor worldEditor,
IEntitySource parentEntitySource)
388 m_aErrors.Insert(
"Failed to get WorldEditor API");
396 api.BeginEntityAction();
398 vector zeroAngles =
"0 0 0";
399 IEntitySource newEntitySource = api.CreateEntity(childRes,
"", api.GetCurrentEntityLayerId(), null,
position, zeroAngles);
400 if (!newEntitySource)
402 api.EndEntityAction();
403 m_aErrors.Insert(
string.Format(
"Failed to create entity for socket %1 using resource %2", boneName, childRes));
415 api.SetVariableValue(newEntitySource, null,
"coords",
position.ToString(
false));
417 api.SetVariableValue(newEntitySource, null,
"angles",
string.Format(
"%1 %2 %3",
angles[1],
angles[0],
angles[2]));
420 if (parentEntitySource && parentEntitySource.GetAncestor())
422 array<IEntitySource> toMove = { newEntitySource };
423 api.MoveEntitiesToPrefab(parentEntitySource, parentEntitySource.GetAncestor(), toMove);
426 api.EndEntityAction();
428 if (m_bVerboseOutput)
429 PrintFormat(
"SocketSpawner: Created child at '%1' using %2 at %3", boneName, childRes,
position);
436 protected void ShowResults()
438 string summary =
string.Format(
"Processed %1 prefabs, created %2 entities from %3 socket(s)",
439 m_iProcessedPrefabs, m_iCreatedEntities, m_iTotalSockets);
441 if (!m_aErrors.IsEmpty())
443 summary +=
"\nErrors:\n";
444 foreach (
string e : m_aErrors)
445 summary +=
"- " + e +
"\n";
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
462 override protected void GetSelectedResources(out array<ResourceName> selectedResources)
464 selectedResources = {};
467 WorldEditor worldEditor = Workbench.GetModule(WorldEditor);
472 worldEditor.GetResourceBrowserSelection(selectedResources.Insert,
true);
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
proto native void Close()
class WorkbenchDialog_AbortRetryIgnore ButtonAttribute("OK", true)
proto external Animation GetAnimation()
Object holding reference to resource. In destructor release the resource.
static void PrintDialog(string message, string caption="", LogLevel level=LogLevel.WARNING)
proto void Print(void var, LogLevel level=LogLevel.NORMAL)
Prints content of variable to console/log.
LogLevel
Enum with severity of the logging message.
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.