mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 20:54:16 +01:00
244 lines
9.7 KiB
C#
244 lines
9.7 KiB
C#
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
|
using Penumbra.Api.Enums;
|
|
using Penumbra.Collections;
|
|
using Penumbra.GameData.Actors;
|
|
using Penumbra.Interop.PathResolving;
|
|
using Penumbra.Services;
|
|
using Penumbra.String.Classes;
|
|
|
|
namespace Penumbra.Interop.ResourceTree;
|
|
|
|
public class ResourceTreeFactory
|
|
{
|
|
private readonly IDataManager _gameData;
|
|
private readonly IObjectTable _objects;
|
|
private readonly CollectionResolver _collectionResolver;
|
|
private readonly IdentifierService _identifier;
|
|
private readonly Configuration _config;
|
|
private readonly ActorService _actors;
|
|
private readonly PathState _pathState;
|
|
|
|
public ResourceTreeFactory(IDataManager gameData, IObjectTable objects, CollectionResolver resolver, IdentifierService identifier,
|
|
Configuration config, ActorService actors, PathState pathState)
|
|
{
|
|
_gameData = gameData;
|
|
_objects = objects;
|
|
_collectionResolver = resolver;
|
|
_identifier = identifier;
|
|
_config = config;
|
|
_actors = actors;
|
|
_pathState = pathState;
|
|
}
|
|
|
|
private TreeBuildCache CreateTreeBuildCache()
|
|
=> new(_objects, _gameData, _actors);
|
|
|
|
public IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> GetLocalPlayerRelatedCharacters()
|
|
{
|
|
var cache = CreateTreeBuildCache();
|
|
return cache.GetLocalPlayerRelatedCharacters();
|
|
}
|
|
|
|
public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromObjectTable(
|
|
Flags flags)
|
|
{
|
|
var cache = CreateTreeBuildCache();
|
|
var characters = (flags & Flags.LocalPlayerRelatedOnly) != 0 ? cache.GetLocalPlayerRelatedCharacters() : cache.GetCharacters();
|
|
|
|
foreach (var character in characters)
|
|
{
|
|
var tree = FromCharacter(character, cache, flags);
|
|
if (tree != null)
|
|
yield return (character, tree);
|
|
}
|
|
}
|
|
|
|
public IEnumerable<(Dalamud.Game.ClientState.Objects.Types.Character Character, ResourceTree ResourceTree)> FromCharacters(
|
|
IEnumerable<Dalamud.Game.ClientState.Objects.Types.Character> characters, Flags flags)
|
|
{
|
|
var cache = CreateTreeBuildCache();
|
|
foreach (var character in characters)
|
|
{
|
|
var tree = FromCharacter(character, cache, flags);
|
|
if (tree != null)
|
|
yield return (character, tree);
|
|
}
|
|
}
|
|
|
|
public ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, Flags flags)
|
|
=> FromCharacter(character, CreateTreeBuildCache(), flags);
|
|
|
|
private unsafe ResourceTree? FromCharacter(Dalamud.Game.ClientState.Objects.Types.Character character, TreeBuildCache cache, Flags flags)
|
|
{
|
|
if (!character.IsValid())
|
|
return null;
|
|
|
|
var gameObjStruct = (GameObject*)character.Address;
|
|
var drawObjStruct = gameObjStruct->GetDrawObject();
|
|
if (drawObjStruct == null)
|
|
return null;
|
|
|
|
var collectionResolveData = _collectionResolver.IdentifyCollection(gameObjStruct, true);
|
|
if (!collectionResolveData.Valid)
|
|
return null;
|
|
|
|
var localPlayerRelated = cache.IsLocalPlayerRelated(character);
|
|
var (name, related) = GetCharacterName(character, cache);
|
|
var networked = character.ObjectId != Dalamud.Game.ClientState.Objects.Types.GameObject.InvalidGameObjectId;
|
|
var tree = new ResourceTree(name, character.ObjectIndex, (nint)gameObjStruct, (nint)drawObjStruct, localPlayerRelated, related,
|
|
networked, collectionResolveData.ModCollection.Name);
|
|
var globalContext = new GlobalResolveContext(_identifier.AwaitedService, collectionResolveData.ModCollection,
|
|
cache, (flags & Flags.WithUiData) != 0);
|
|
using (var _ = _pathState.EnterInternalResolve())
|
|
{
|
|
tree.LoadResources(globalContext);
|
|
}
|
|
tree.FlatNodes.UnionWith(globalContext.Nodes.Values);
|
|
tree.ProcessPostfix((node, _) => tree.FlatNodes.Add(node));
|
|
|
|
// This is currently unneeded as we can resolve all paths by querying the draw object:
|
|
// ResolveGamePaths(tree, collectionResolveData.ModCollection);
|
|
if (globalContext.WithUiData)
|
|
ResolveUiData(tree);
|
|
FilterFullPaths(tree, (flags & Flags.RedactExternalPaths) != 0 ? _config.ModDirectory : null);
|
|
Cleanup(tree);
|
|
|
|
return tree;
|
|
}
|
|
|
|
private static void ResolveGamePaths(ResourceTree tree, ModCollection collection)
|
|
{
|
|
var forwardDictionary = new Dictionary<Utf8GamePath, FullPath?>();
|
|
var reverseDictionary = new Dictionary<string, HashSet<Utf8GamePath>>();
|
|
foreach (var node in tree.FlatNodes)
|
|
{
|
|
if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty)
|
|
reverseDictionary.TryAdd(node.FullPath.ToPath(), null!);
|
|
else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1)
|
|
forwardDictionary.TryAdd(node.GamePath, null);
|
|
}
|
|
|
|
foreach (var key in forwardDictionary.Keys)
|
|
forwardDictionary[key] = collection.ResolvePath(key);
|
|
|
|
var reverseResolvedArray = collection.ReverseResolvePaths(reverseDictionary.Keys);
|
|
foreach (var (key, set) in reverseDictionary.Keys.Zip(reverseResolvedArray))
|
|
reverseDictionary[key] = set;
|
|
|
|
foreach (var node in tree.FlatNodes)
|
|
{
|
|
if (node.PossibleGamePaths.Length == 0 && !node.FullPath.InternalName.IsEmpty)
|
|
{
|
|
if (!reverseDictionary.TryGetValue(node.FullPath.ToPath(), out var resolvedSet))
|
|
continue;
|
|
|
|
if (resolvedSet.Count != 1)
|
|
{
|
|
Penumbra.Log.Debug(
|
|
$"Found {resolvedSet.Count} game paths while reverse-resolving {node.FullPath} in {collection.Name}:");
|
|
foreach (var gamePath in resolvedSet)
|
|
Penumbra.Log.Debug($"Game path: {gamePath}");
|
|
}
|
|
|
|
node.PossibleGamePaths = resolvedSet.ToArray();
|
|
}
|
|
else if (node.FullPath.InternalName.IsEmpty && node.PossibleGamePaths.Length == 1)
|
|
{
|
|
if (forwardDictionary.TryGetValue(node.GamePath, out var resolved))
|
|
node.FullPath = resolved ?? new FullPath(node.GamePath);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void ResolveUiData(ResourceTree tree)
|
|
{
|
|
foreach (var node in tree.FlatNodes)
|
|
{
|
|
if (node.Name != null || node.PossibleGamePaths.Length == 0)
|
|
continue;
|
|
|
|
var gamePath = node.PossibleGamePaths[0];
|
|
node.SetUiData(node.Type switch
|
|
{
|
|
ResourceType.Imc => node.ResolveContext!.GuessModelUIData(gamePath).PrependName("IMC: "),
|
|
ResourceType.Mdl => node.ResolveContext!.GuessModelUIData(gamePath),
|
|
_ => node.ResolveContext!.GuessUIDataFromPath(gamePath),
|
|
});
|
|
}
|
|
|
|
tree.ProcessPostfix((node, parent) =>
|
|
{
|
|
if (node.Name == parent?.Name)
|
|
node.Name = null;
|
|
|
|
if (parent != null)
|
|
parent.DescendentIcons |= node.Icon | node.DescendentIcons;
|
|
});
|
|
}
|
|
|
|
private static void FilterFullPaths(ResourceTree tree, string? onlyWithinPath)
|
|
{
|
|
static bool ShallKeepPath(FullPath fullPath, string? onlyWithinPath)
|
|
{
|
|
if (!fullPath.IsRooted)
|
|
return true;
|
|
|
|
if (onlyWithinPath != null)
|
|
{
|
|
var relPath = Path.GetRelativePath(onlyWithinPath, fullPath.FullName);
|
|
if (relPath != "." && (relPath.StartsWith('.') || Path.IsPathRooted(relPath)))
|
|
return false;
|
|
}
|
|
|
|
return fullPath.Exists;
|
|
}
|
|
|
|
foreach (var node in tree.FlatNodes)
|
|
{
|
|
if (!ShallKeepPath(node.FullPath, onlyWithinPath))
|
|
node.FullPath = FullPath.Empty;
|
|
}
|
|
}
|
|
|
|
private static void Cleanup(ResourceTree tree)
|
|
{
|
|
foreach (var node in tree.FlatNodes)
|
|
{
|
|
node.Name ??= node.FallbackName;
|
|
|
|
node.FallbackName = null;
|
|
node.ResolveContext = null;
|
|
}
|
|
}
|
|
|
|
private unsafe (string Name, bool PlayerRelated) GetCharacterName(Dalamud.Game.ClientState.Objects.Types.Character character,
|
|
TreeBuildCache cache)
|
|
{
|
|
var identifier = _actors.AwaitedService.FromObject((GameObject*)character.Address, out var owner, true, false, false);
|
|
switch (identifier.Type)
|
|
{
|
|
case IdentifierType.Player: return (identifier.PlayerName.ToString(), true);
|
|
case IdentifierType.Owned:
|
|
var ownerChara = _objects.CreateObjectReference((nint)owner) as Dalamud.Game.ClientState.Objects.Types.Character;
|
|
if (ownerChara != null)
|
|
{
|
|
var ownerName = GetCharacterName(ownerChara, cache);
|
|
return ($"[{ownerName.Name}] {character.Name} ({identifier.Kind.ToName()})", ownerName.PlayerRelated);
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
return ($"{character.Name} ({identifier.Kind.ToName()})", false);
|
|
}
|
|
|
|
[Flags]
|
|
public enum Flags
|
|
{
|
|
RedactExternalPaths = 1,
|
|
WithUiData = 2,
|
|
LocalPlayerRelatedOnly = 4,
|
|
WithOwnership = 8,
|
|
}
|
|
}
|