Skip to content

Commit 397984a

Browse files
committed
new blueprint actor
1 parent 04eb130 commit 397984a

7 files changed

Lines changed: 273 additions & 63 deletions

File tree

Snooper/Core/Containers/Resources/IndirectResources.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ public ResourcesMetadata Add(PrimitiveComponent<TVertex, TInstanceData, TPerMate
9999
drawAllocations[i] = new DrawBufferAllocation(buffer.Add(command, draw, new PerDrawCulled(geometryHandle, section)), bufferType, section.MaterialIndex);
100100
}
101101

102-
component.MarkClean(DirtyFlags.All);
102+
component.MarkClean(DirtyFlags.InstanceData | DirtyFlags.Visibility | DirtyFlags.Opacity | DirtyFlags.Outline | DirtyFlags.ManualLodSwap);
103103
return new ResourcesMetadata(geometryHandle, instanceAllocation, materialAllocation, drawAllocations);
104104
}
105105

Lines changed: 260 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using CUE4Parse.UE4.Assets;
1+
using CUE4Parse.UE4.Assets.Exports;
2+
using CUE4Parse.UE4.Assets.Exports.Component;
23
using CUE4Parse.UE4.Assets.Exports.Engine;
34
using CUE4Parse.UE4.Objects.Core.Misc;
45
using CUE4Parse.UE4.Objects.Engine;
@@ -10,100 +11,305 @@ namespace Snooper.Rendering.Actors;
1011

1112
public 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
}

Snooper/Rendering/Actors/MeshActor.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using CUE4Parse.UE4.Assets.Exports.Animation;
2+
using CUE4Parse.UE4.Assets.Exports.Engine;
23
using CUE4Parse.UE4.Assets.Exports.GeometryCollection;
3-
using CUE4Parse.UE4.Assets.Exports.SkeletalMesh;
44
using CUE4Parse.UE4.Assets.Exports.StaticMesh;
55
using Snooper.Rendering.Components.Mesh;
66
using Snooper.Rendering.Components.Transforms;
@@ -19,9 +19,9 @@ public MeshActor(UGeometryCollection geometryCollection, Transform? transform =
1919
Components.Add(new GeometryCollectionComponent(geometryCollection, transform));
2020
}
2121

22-
public MeshActor(USkeletalMesh skeletalMesh, Transform? transform = null) : base(skeletalMesh)
22+
public MeshActor(USkinnedAsset skinnedAsset, Transform? transform = null) : base(skinnedAsset)
2323
{
24-
Components.Add(new SkeletalMeshComponent(skeletalMesh, transform));
24+
Components.Add(new SkeletalMeshComponent(skinnedAsset, transform));
2525
}
2626

2727
public MeshActor(UAnimationAsset animation, float playPosition = 0f, float playRate = 1f) : base(animation)

0 commit comments

Comments
 (0)