1- using CUE4Parse . UE4 . Assets ;
1+ using CUE4Parse . UE4 . Assets . Exports ;
2+ using CUE4Parse . UE4 . Assets . Exports . Component ;
23using CUE4Parse . UE4 . Assets . Exports . Engine ;
34using CUE4Parse . UE4 . Objects . Core . Misc ;
45using CUE4Parse . UE4 . Objects . Engine ;
@@ -10,100 +11,305 @@ namespace Snooper.Rendering.Actors;
1011
1112public class BlueprintActor : UnrealActor
1213{
13- public BlueprintActor ( UBlueprintGeneratedClass blueprint ) : base ( blueprint )
14+ private const int MaxChildActorDepth = 8 ;
15+
16+ public BlueprintActor ( UBlueprintGeneratedClass blueprint ) : this ( blueprint , [ ] , 0 )
1417 {
15- var bps = new List < UBlueprintGeneratedClass > ( ) ;
16- var current = blueprint ;
17- while ( current != null )
18+
19+ }
20+
21+ private BlueprintActor ( UBlueprintGeneratedClass blueprint , HashSet < string > ancestry , int depth ) : base ( blueprint )
22+ {
23+ _ancestry = ancestry ;
24+ _depth = depth ;
25+ _classKey = blueprint . GetPathName ( ) ;
26+
27+ var chain = CollectClassChain ( blueprint ) ;
28+ var deferred = BuildNativeComponents ( chain ) ;
29+ BuildOverrideTable ( chain ) ;
30+ ExecuteConstructionScripts ( chain ) ;
31+
32+ // non-scene native components (movement, audio, ...) carry no transform, so they must never become the root
33+ foreach ( var component in deferred )
1834 {
19- bps . Add ( current ) ;
20- if ( current . Owner is { } pkg )
21- _blueprintPackages . Add ( pkg ) ;
35+ Components . Add ( component ) ;
36+ }
37+
38+ _nativeByName . Clear ( ) ;
39+ _byTemplatePtr . Clear ( ) ;
40+ _scsByVariable . Clear ( ) ;
41+ _scsByName . Clear ( ) ;
42+ _visitedNodes . Clear ( ) ;
43+ _overridesByGuid . Clear ( ) ;
44+ _overridesByKey . Clear ( ) ;
45+ }
46+
47+ public override string Icon => Settings . ClipboardListIcon ;
2248
23- current = current . Super ? . Load < UBlueprintGeneratedClass > ( ) ;
49+ private readonly HashSet < string > _ancestry ;
50+ private readonly int _depth ;
51+ private readonly string _classKey ;
52+
53+ private readonly Dictionary < string , SpatialComponent > _nativeByName = new ( StringComparer . Ordinal ) ;
54+ private readonly Dictionary < FPackageIndex , SpatialComponent > _byTemplatePtr = [ ] ;
55+ private readonly Dictionary < ( string OwnerClass , string Variable ) , SpatialComponent > _scsByVariable = [ ] ;
56+ private readonly Dictionary < string , SpatialComponent > _scsByName = new ( StringComparer . Ordinal ) ;
57+ private readonly HashSet < FPackageIndex > _visitedNodes = [ ] ;
58+ private readonly Dictionary < FGuid , FPackageIndex > _overridesByGuid = [ ] ;
59+ private readonly Dictionary < ( string OwnerClass , string Variable ) , FPackageIndex > _overridesByKey = [ ] ;
60+
61+ private List < UBlueprintGeneratedClass > CollectClassChain ( UBlueprintGeneratedClass blueprint )
62+ {
63+ var chain = new List < UBlueprintGeneratedClass > ( ) ;
64+ var seen = new HashSet < string > ( StringComparer . Ordinal ) ;
65+
66+ var current = blueprint ;
67+ while ( current != null && seen . Add ( current . GetPathName ( ) ) )
68+ {
69+ chain . Add ( current ) ;
70+ current = current . SuperStruct is { IsNull : false } super && super . TryLoad < UBlueprintGeneratedClass > ( out var parent ) ? parent : null ;
2471 }
25- bps . Reverse ( ) ;
2672
27- foreach ( var bp in bps )
73+ return chain ;
74+ }
75+
76+ private List < SpatialComponent > BuildNativeComponents ( List < UBlueprintGeneratedClass > chain )
77+ {
78+ // collect default subobjects of every class default object in the chain, the most-derived export of a given name wins
79+ var subobjects = new Dictionary < string , FPackageIndex > ( StringComparer . Ordinal ) ;
80+ UObject ? mostDerivedCdo = null ;
81+ foreach ( var bp in chain )
2882 {
29- var handler = bp . InheritableComponentHandler ? . Load < UInheritableComponentHandler > ( ) ;
30- if ( handler == null ) continue ;
83+ var cdoPtr = bp . ClassDefaultObject ;
84+ if ( cdoPtr is not { IsNull : false } || cdoPtr . ResolvedObject is not { ExportIndex : >= 0 } cdo ) continue ;
3185
32- foreach ( var record in handler . Records )
86+ mostDerivedCdo ??= cdoPtr . Load ( ) ;
87+
88+ var package = cdo . Package ;
89+ for ( var i = 0 ; i < package . ExportMapLength ; i ++ )
3390 {
34- if ( record . ComponentTemplate == null || record . ComponentTemplate . IsNull ) continue ;
35- _overrides [ record . ComponentKey . AssociatedGuid ] = record . ComponentTemplate ;
91+ var ptr = new FPackageIndex ( package , i + 1 ) ;
92+ var resolved = package . ResolvePackageIndex ( ptr ) ;
93+ if ( resolved ? . Outer is not { } outer || outer . ExportIndex != cdo . ExportIndex || ! ReferenceEquals ( outer . Package , package ) ) continue ;
94+
95+ subobjects . TryAdd ( resolved . Name . Text , ptr ) ;
3696 }
3797 }
3898
39- var candidates = new HashSet < FPackageIndex ? > ( ) ;
40- foreach ( var bp in bps )
99+ var created = new List < ( ComponentPair Pair , bool IsScene ) > ( ) ;
100+ foreach ( var ( name , ptr ) in subobjects )
41101 {
42- var script = bp . SimpleConstructionScript ? . Load < USimpleConstructionScript > ( ) ;
43- if ( script == null ) continue ;
102+ if ( ! ptr . TryLoad < UActorComponent > ( out var actorComponent ) ) continue ;
103+
104+ var pair = CreateComponentPair ( ptr ) ;
105+ pair . Component . Name = name ;
106+ created . Add ( ( pair , actorComponent is USceneComponent ) ) ;
44107
45- candidates . Add ( script . DefaultSceneRootNode ) ;
46- candidates . UnionWith ( script . RootNodes ) ;
108+ _nativeByName [ name ] = pair . Component ;
109+ _byTemplatePtr [ ptr ] = pair . Component ;
47110 }
48111
49- foreach ( var candidate in candidates )
112+ // the attach parent pointer may target an ancestor package's export of the same subobject, so resolve it by name
113+ foreach ( var ( pair , _) in created )
50114 {
51- if ( candidate ? . TryLoad < USCS_Node > ( out var node ) == true )
115+ if ( pair . ParentPtr is { IsNull : false } parentPtr && _nativeByName . TryGetValue ( parentPtr . Name , out var parent ) && parent != pair . Component )
52116 {
53- ProcessNode ( node ) ;
117+ pair . Component . Relation = parent ;
54118 }
55119 }
56120
57- _overrides . Clear ( ) ;
58- _blueprintPackages . Clear ( ) ;
121+ SpatialComponent ? root = null ;
122+ if ( mostDerivedCdo ? . GetOrDefault < FPackageIndex ? > ( "RootComponent" ) is { IsNull : false } rootPtr )
123+ {
124+ _nativeByName . TryGetValue ( rootPtr . Name , out root ) ;
125+ }
126+ root ??= created . FirstOrDefault ( c => c . IsScene && c . Pair . Component . Relation is null ) . Pair . Component ;
127+
128+ // the first spatial component added becomes the actor's root
129+ var deferred = new List < SpatialComponent > ( ) ;
130+ if ( root != null ) Components . Add ( root ) ;
131+ foreach ( var ( pair , isScene ) in created )
132+ {
133+ if ( pair . Component == root ) continue ;
134+ if ( isScene ) Components . Add ( pair . Component ) ;
135+ else deferred . Add ( pair . Component ) ;
136+ }
137+ return deferred ;
59138 }
60139
61- private readonly Dictionary < FGuid , FPackageIndex > _overrides = [ ] ;
62- private readonly List < IPackage > _blueprintPackages = [ ] ;
140+ private void BuildOverrideTable ( List < UBlueprintGeneratedClass > chain )
141+ {
142+ // base first so that the most-derived override wins
143+ for ( var i = chain . Count - 1 ; i >= 0 ; i -- )
144+ {
145+ var handler = chain [ i ] . InheritableComponentHandler ? . Load < UInheritableComponentHandler > ( ) ;
146+ if ( handler == null ) continue ;
147+
148+ foreach ( var record in handler . Records )
149+ {
150+ if ( record . ComponentTemplate is not { IsNull : false } template ) continue ;
151+
152+ var key = record . ComponentKey ;
153+ if ( key . AssociatedGuid . IsValid ( ) )
154+ {
155+ _overridesByGuid [ key . AssociatedGuid ] = template ;
156+ }
157+
158+ var ownerName = key . OwnerClass ? . Name ;
159+ if ( ownerName != null && key . SCSVariableName is { IsNone : false } variable )
160+ {
161+ _overridesByKey [ ( ownerName , variable . Text ) ] = template ;
162+ }
163+ }
164+ }
165+ }
63166
64- private void ProcessNode ( USCS_Node node , SpatialComponent ? parent = null )
167+ private void ExecuteConstructionScripts ( List < UBlueprintGeneratedClass > chain )
65168 {
66- _overrides . TryGetValue ( node . VariableGuid , out var templateOverride ) ;
169+ for ( var i = chain . Count - 1 ; i >= 0 ; i -- )
170+ {
171+ var owner = chain [ i ] ;
172+ var script = owner . SimpleConstructionScript ? . Load < USimpleConstructionScript > ( ) ;
173+ if ( script == null ) continue ;
67174
68- var template = templateOverride ?? node . ComponentTemplate ;
69- if ( template == null || template . IsNull )
175+ var processedAny = false ;
176+ foreach ( var nodePtr in script . RootNodes )
177+ {
178+ if ( nodePtr is not { IsNull : false } ) continue ;
179+
180+ processedAny = true ;
181+ ProcessNode ( nodePtr , owner , null ) ;
182+ }
183+
184+ // the default scene root only exists when nothing else can be the root
185+ if ( ! processedAny && RootComponent is null && script . DefaultSceneRootNode is { IsNull : false } defaultRoot )
186+ {
187+ ProcessNode ( defaultRoot , owner , null ) ;
188+ }
189+ }
190+ }
191+
192+ private void ProcessNode ( FPackageIndex nodePtr , UBlueprintGeneratedClass owner , SpatialComponent ? parentFromRecursion )
193+ {
194+ if ( ! _visitedNodes . Add ( nodePtr ) || ! nodePtr . TryLoad < USCS_Node > ( out var node ) ) return ;
195+
196+ var variableName = node . InternalVariableName . Text ;
197+ var template = ResolveTemplate ( node , owner . Name , variableName ) ;
198+ if ( template is not { IsNull : false } )
70199 {
71- Log . Warning ( "Node {NodeName} has no component template, skipping." , node . InternalVariableName . Text ) ;
200+ Log . Warning ( "Node {NodeName} has no component template, skipping" , variableName ) ;
201+ foreach ( var childPtr in node . ChildNodes )
202+ {
203+ if ( childPtr is { IsNull : false } ) ProcessNode ( childPtr , owner , parentFromRecursion ) ;
204+ }
72205 return ;
73206 }
74207
75- var component = CreateComponentPair ( template ) . Component ;
76- component . AttachSocketName = node . GetOrDefault < FName ? > ( "AttachToName" ) ? . Text ;
77- if ( node . GetOrDefault < FName ? > ( "InternalVariableName" ) is { } internalVariableName )
208+ if ( ! _byTemplatePtr . TryGetValue ( template , out var component ) )
78209 {
79- component . Name = internalVariableName . Text ;
210+ var pair = CreateComponentPair ( template ) ;
211+ component = pair . Component ;
212+ component . Name = variableName ;
213+
214+ if ( node . GetOrDefault < FName ? > ( "AttachToName" ) is { IsNone : false } socket )
215+ {
216+ component . AttachSocketName = socket . Text ;
217+ }
218+
219+ var parent = ResolveNodeParent ( node , parentFromRecursion ) ;
220+ var isScene = template . ResolvedObject ? . Object ? . Value is USceneComponent ;
221+ if ( RootComponent is null && isScene && parent is null )
222+ {
223+ // becomes the root
224+ }
225+ else
226+ {
227+ var expected = parent ?? RootComponent ;
228+ component . Relation = expected ;
229+ if ( expected != null && component . Relation != expected )
230+ {
231+ Log . Warning ( "Node {NodeName} could not be attached to {Parent}" , variableName , expected . Name ) ;
232+ }
233+ }
234+
235+ Components . Add ( component ) ;
236+
237+ _byTemplatePtr [ template ] = component ;
238+ _scsByVariable [ ( owner . Name , variableName ) ] = component ;
239+ _scsByName . TryAdd ( variableName , component ) ;
240+
241+ if ( template . ResolvedObject ? . Object ? . Value is UChildActorComponent childActorComponent )
242+ {
243+ TryCreateChildActor ( childActorComponent , component ) ;
244+ }
80245 }
81- if ( node . GetOrDefault < FName ? > ( "ParentComponentOrVariableName" ) is { } parentComponentOrVariableName )
246+
247+ foreach ( var childPtr in node . ChildNodes )
82248 {
83- parent = node . GetOrDefault ( "bIsParentComponentNative" , false ) ? LoadNativeComponent ( parentComponentOrVariableName . Text ) : Components . OfType < SpatialComponent > ( ) . FirstOrDefault ( c => c . Name == parentComponentOrVariableName . Text ) ;
249+ if ( childPtr is { IsNull : false } ) ProcessNode ( childPtr , owner , component ) ;
84250 }
85- component . Relation = parent ;
86- Components . Add ( component ) ;
251+ }
252+
253+ private FPackageIndex ? ResolveTemplate ( USCS_Node node , string ownerName , string variableName )
254+ {
255+ if ( node . VariableGuid . IsValid ( ) && _overridesByGuid . TryGetValue ( node . VariableGuid , out var byGuid ) )
256+ return byGuid ;
257+ if ( _overridesByKey . TryGetValue ( ( ownerName , variableName ) , out var byKey ) )
258+ return byKey ;
259+ return node . ComponentTemplate ;
260+ }
261+
262+ private SpatialComponent ? ResolveNodeParent ( USCS_Node node , SpatialComponent ? parentFromRecursion )
263+ {
264+ if ( node . GetOrDefault < FName ? > ( "ParentComponentOrVariableName" ) is not { IsNone : false } parentName )
265+ return parentFromRecursion ;
87266
88- foreach ( var child in node . GetChildNodes ( ) )
267+ var name = parentName . Text ;
268+ if ( node . GetOrDefault ( "bIsParentComponentNative" , false ) )
269+ {
270+ if ( _nativeByName . TryGetValue ( name , out var native ) ) return native ;
271+ }
272+ else
89273 {
90- ProcessNode ( child , component ) ;
274+ if ( node . GetOrDefault < FName ? > ( "ParentComponentOwnerClassName" ) is { IsNone : false } ownerClassName &&
275+ _scsByVariable . TryGetValue ( ( ownerClassName . Text , name ) , out var scoped ) )
276+ return scoped ;
277+ if ( _scsByName . TryGetValue ( name , out var byName ) ) return byName ;
278+ if ( _nativeByName . TryGetValue ( name , out var native ) ) return native ;
91279 }
280+
281+ Log . Warning ( "Node {NodeName} references unknown parent {Parent}, attaching to root" , node . InternalVariableName . Text , name ) ;
282+ return null ;
92283 }
93284
94- private SpatialComponent ? LoadNativeComponent ( string name )
285+ private void TryCreateChildActor ( UChildActorComponent childActorComponent , SpatialComponent component )
95286 {
96- foreach ( var package in _blueprintPackages )
287+ if ( _depth >= MaxChildActorDepth )
288+ {
289+ Log . Warning ( "Child actor depth limit reached at {Component}" , component . Name ) ;
290+ return ;
291+ }
292+
293+ if ( childActorComponent . GetOrDefault < FPackageIndex ? > ( "ChildActorClass" ) is not { IsNull : false } classPtr ||
294+ ! classPtr . TryLoad < UBlueprintGeneratedClass > ( out var childClass ) )
295+ return ;
296+
297+ var childKey = childClass . GetPathName ( ) ;
298+ if ( childKey == _classKey || _ancestry . Contains ( childKey ) )
97299 {
98- var exportIdx = package . GetExportIndex ( name ) ;
99- if ( exportIdx < 0 ) continue ;
300+ Log . Warning ( "Child actor {Class} would recurse into itself, skipping" , childClass . Name ) ;
301+ return ;
302+ }
100303
101- var component = CreateComponentPair ( new FPackageIndex ( package , exportIdx + 1 ) ) . Component ;
102- Components . Add ( component ) ;
103- return component ;
304+ var ancestry = new HashSet < string > ( _ancestry , StringComparer . Ordinal ) { _classKey } ;
305+ var child = new BlueprintActor ( childClass , ancestry , _depth + 1 ) ;
306+ Children . Add ( child ) ;
307+
308+ // OnChildAdded parents the child to our root, re-target it to the spawning component
309+ if ( child . RootComponent is { } childRoot )
310+ {
311+ childRoot . Relation = component ;
312+ childRoot . SetLocalTransform ( Transform . Identity ) ;
104313 }
105- return null ;
106314 }
107-
108- public override string Icon => "\uf46d " ;
109315}
0 commit comments