Merge branch 'master' into AeAstralis/tagging

This commit is contained in:
Ottermandias 2024-03-17 13:36:15 +01:00
commit b725d919bb
44 changed files with 1563 additions and 296 deletions

View file

@ -1,5 +1,6 @@
using OtterGui.Classes;
using Penumbra.Api;
using Penumbra.Services;
namespace Penumbra.Communication;
@ -19,5 +20,8 @@ public sealed class CreatingCharacterBase()
{
/// <seealso cref="PenumbraApi.CreatingCharacterBase"/>
Api = 0,
/// <seealso cref="CrashHandlerService.OnCreatingCharacterBase"/>
CrashHandler = 0,
}
}

View file

@ -33,6 +33,7 @@ public class Configuration : IPluginConfiguration, ISavable
public string ModDirectory { get; set; } = string.Empty;
public string ExportDirectory { get; set; } = string.Empty;
public bool UseCrashHandler { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool HideUiInGPose { get; set; } = false;
public bool HideUiInCutscenes { get; set; } = true;

View file

@ -47,7 +47,7 @@ public class EphemeralConfig : ISavable, IDisposable
/// </summary>
public EphemeralConfig(SaveService saveService, ModPathChanged modPathChanged)
{
_saveService = saveService;
_saveService = saveService;
_modPathChanged = modPathChanged;
Load();
_modPathChanged.Subscribe(OnModPathChanged, ModPathChanged.Priority.EphemeralConfig);
@ -94,13 +94,13 @@ public class EphemeralConfig : ISavable, IDisposable
public void Save(StreamWriter writer)
{
using var jWriter = new JsonTextWriter(writer);
using var jWriter = new JsonTextWriter(writer);
jWriter.Formatting = Formatting.Indented;
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
var serializer = new JsonSerializer { Formatting = Formatting.Indented };
serializer.Serialize(jWriter, this);
}
/// <summary> Overwrite the last saved mod path if it changes. </summary>
/// <summary> Overwrite the last saved mod path if it changes. </summary>
private void OnModPathChanged(ModPathChangeType type, Mod mod, DirectoryInfo? old, DirectoryInfo? _)
{
if (type is not ModPathChangeType.Moved || !string.Equals(old?.Name, LastModPath, StringComparison.OrdinalIgnoreCase))

View file

@ -53,7 +53,7 @@ public class MaterialExporter
var normal = material.Textures[TextureUsage.SamplerNormal];
var operation = new ProcessCharacterNormalOperation(normal, table);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds(), in operation);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, normal.Bounds, in operation);
// Check if full textures are provided, and merge in if available.
var baseColor = operation.BaseColor;
@ -199,7 +199,7 @@ public class MaterialExporter
small.Mutate(context => context.Resize(large.Width, large.Height));
var operation = new MultiplyOperation<TPixel1, TPixel2>(target, multiplier);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds(), in operation);
ParallelRowIterator.IterateRows(ImageSharpConfiguration.Default, target.Bounds, in operation);
}
}

View file

@ -1,4 +1,5 @@
using Newtonsoft.Json;
using OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Import.Structs;
using Penumbra.Mods;
@ -35,7 +36,8 @@ public partial class TexToolsImporter
var modList = modListRaw.Select(m => JsonConvert.DeserializeObject<SimpleMod>(m, JsonSettings)!).ToList();
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name), _config.ReplaceNonAsciiOnImport, true);
_currentModDirectory = ModCreator.CreateModFolder(_baseDirectory, Path.GetFileNameWithoutExtension(modPackFile.Name),
_config.ReplaceNonAsciiOnImport, true);
// Create a new ModMeta from the TTMP mod list info
_modManager.DataEditor.CreateMeta(_currentModDirectory, _currentModName, DefaultTexToolsData.Author, DefaultTexToolsData.Description,
null, null);
@ -193,15 +195,17 @@ public partial class TexToolsImporter
optionIdx += maxOptions;
// Handle empty options for single select groups without creating a folder for them.
// We only want one of those at most, and it should usually be the first option.
// We only want one of those at most.
if (group.SelectionType == GroupType.Single)
{
var empty = group.OptionList.FirstOrDefault(o => o.Name.Length > 0 && o.ModsJsons.Length == 0);
if (empty != null)
var idx = group.OptionList.IndexOf(o => o.Name.Length > 0 && o.ModsJsons.Length == 0);
if (idx >= 0)
{
_currentOptionName = empty.Name;
options.Insert(0, ModCreator.CreateEmptySubMod(empty.Name));
defaultSettings = defaultSettings == null ? 0 : defaultSettings.Value + 1;
var option = group.OptionList[idx];
_currentOptionName = option.Name;
options.Insert(idx, ModCreator.CreateEmptySubMod(option.Name));
if (option.IsChecked)
defaultSettings = (uint) idx;
}
}

View file

@ -2,21 +2,25 @@ using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Called for some sound effects caused by animations or VFX. </summary>
public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSoundPlay.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly CrashHandlerService _crashHandler;
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver)
public ApricotListenerSoundPlay(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Apricot Listener Sound Play", Sigs.ApricotListenerSoundPlay, Detour, true);
}
@ -46,6 +50,7 @@ public sealed unsafe class ApricotListenerSoundPlay : FastHook<ApricotListenerSo
newData = _collectionResolver.IdentifyCollection(drawObject, true);
}
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ApricotSoundPlay);
var last = _state.SetAnimationData(newData);
var ret = Task.Result.Original(a1, a2, a3, a4, a5, a6);
_state.RestoreAnimationData(last);

View file

@ -1,8 +1,10 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
@ -12,16 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
/// </summary>
public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLoadAnimation.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly DrawObjectState _drawObjectState;
private readonly CrashHandlerService _crashHandler;
public CharacterBaseLoadAnimation(HookManager hooks, GameState state, CollectionResolver collectionResolver,
DrawObjectState drawObjectState)
DrawObjectState drawObjectState, CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_drawObjectState = drawObjectState;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("CharacterBase Load Animation", Sigs.CharacterBaseLoadAnimation, Detour, true);
}
@ -33,7 +37,9 @@ public sealed unsafe class CharacterBaseLoadAnimation : FastHook<CharacterBaseLo
var lastObj = _state.LastGameObject;
if (lastObj == nint.Zero && _drawObjectState.TryGetValue((nint)drawObject, out var p))
lastObj = p.Item1;
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)lastObj, true));
var data = _collectionResolver.IdentifyCollection((GameObject*)lastObj, true);
var last = _state.SetAnimationData(data);
_crashHandler.LogAnimation(data.AssociatedGameObject, data.ModCollection, AnimationInvocationType.CharacterBaseLoadAnimation);
Penumbra.Log.Excessive($"[CharacterBase Load Animation] Invoked on {(nint)drawObject:X}");
Task.Result.Original(drawObject);
_state.RestoreAnimationData(last);

View file

@ -1,21 +1,25 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Load a ground-based area VFX. </summary>
public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly CrashHandlerService _crashHandler;
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver)
public LoadAreaVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Area VFX", Sigs.LoadAreaVfx, Detour, true);
}
@ -25,10 +29,11 @@ public sealed unsafe class LoadAreaVfx : FastHook<LoadAreaVfx.Delegate>
private nint Detour(uint vfxId, float* pos, GameObject* caster, float unk1, float unk2, byte unk3)
{
var newData = caster != null
? _collectionResolver.IdentifyCollection(caster, true)
: ResolveData.Invalid;
? _collectionResolver.IdentifyCollection(caster, true)
: ResolveData.Invalid;
var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadAreaVfx);
var ret = Task.Result.Original(vfxId, pos, caster, unk1, unk2, unk3);
Penumbra.Log.Excessive(
$"[Load Area VFX] Invoked with {vfxId}, [{pos[0]} {pos[1]} {pos[2]}], 0x{(nint)caster:X}, {unk1}, {unk2}, {unk3} -> 0x{ret:X}.");

View file

@ -1,19 +1,23 @@
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Characters load some of their voice lines or whatever with this function. </summary>
public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly CrashHandlerService _crashHandler;
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver)
public LoadCharacterSound(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
{
_state = state;
_state = state;
_collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Character Sound",
(nint)FFXIVClientStructs.FFXIV.Client.Game.Character.Character.VfxContainer.MemberFunctionPointers.LoadCharacterSound, Detour,
true);
@ -25,7 +29,9 @@ public sealed unsafe class LoadCharacterSound : FastHook<LoadCharacterSound.Dele
private nint Detour(nint container, int unk1, int unk2, nint unk3, ulong unk4, int unk5, int unk6, ulong unk7)
{
var character = *(GameObject**)(container + 8);
var last = _state.SetSoundData(_collectionResolver.IdentifyCollection(character, true));
var newData = _collectionResolver.IdentifyCollection(character, true);
var last = _state.SetSoundData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterSound);
var ret = Task.Result.Original(container, unk1, unk2, unk3, unk4, unk5, unk6, unk7);
Penumbra.Log.Excessive($"[Load Character Sound] Invoked with {container:X} {unk1} {unk2} {unk3} {unk4} {unk5} {unk6} {unk7} -> {ret}.");
_state.RestoreSoundData(last);

View file

@ -2,9 +2,11 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.Services;
using Penumbra.String;
namespace Penumbra.Interop.Hooks.Animation;
@ -12,15 +14,18 @@ namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Load a VFX specifically for a character. </summary>
public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly CrashHandlerService _crashHandler;
public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
public LoadCharacterVfx(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Character VFX", Sigs.LoadCharacterVfx, Detour, true);
}
@ -45,6 +50,7 @@ public sealed unsafe class LoadCharacterVfx : FastHook<LoadCharacterVfx.Delegate
}
var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadCharacterVfx);
var ret = Task.Result.Original(vfxPath, vfxParams, unk1, unk2, unk3, unk4);
Penumbra.Log.Excessive(
$"[Load Character VFX] Invoked with {new ByteString(vfxPath)}, 0x{vfxParams->GameObjectId:X}, {vfxParams->TargetCount}, {unk1}, {unk2}, {unk3}, {unk4} -> 0x{ret:X}.");

View file

@ -3,8 +3,10 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.CrashHandler;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
@ -14,19 +16,21 @@ namespace Penumbra.Interop.Hooks.Animation;
/// </summary>
public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResources.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly ICondition _conditions;
private readonly IObjectTable _objects;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly ICondition _conditions;
private readonly IObjectTable _objects;
private readonly CrashHandlerService _crashHandler;
public LoadTimelineResources(HookManager hooks, GameState state, CollectionResolver collectionResolver, ICondition conditions,
IObjectTable objects)
IObjectTable objects, CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_conditions = conditions;
_objects = objects;
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Load Timeline Resources", Sigs.LoadTimelineResources, Detour, true);
}
public delegate ulong Delegate(nint timeline);
@ -39,7 +43,13 @@ public sealed unsafe class LoadTimelineResources : FastHook<LoadTimelineResource
if (_conditions[ConditionFlag.OccupiedInCutSceneEvent] || _conditions[ConditionFlag.WatchingCutscene78])
return Task.Result.Original(timeline);
var last = _state.SetAnimationData(GetDataFromTimeline(_objects, _collectionResolver, timeline));
var newData = GetDataFromTimeline(_objects, _collectionResolver, timeline);
var last = _state.SetAnimationData(newData);
#if false
// This is called far too often and spams the log too much.
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.LoadTimelineResources);
#endif
var ret = Task.Result.Original(timeline);
_state.RestoreAnimationData(last);
return ret;

View file

@ -1,23 +1,28 @@
using Dalamud.Plugin.Services;
using OtterGui.Services;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
namespace Penumbra.Interop.Hooks.Animation;
using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Interop.Structs;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Called when some action timelines update. </summary>
public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly CrashHandlerService _crashHandler;
public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
public ScheduleClipUpdate(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Schedule Clip Update", Sigs.ScheduleClipUpdate, Detour, true);
}
@ -27,8 +32,9 @@ public sealed unsafe class ScheduleClipUpdate : FastHook<ScheduleClipUpdate.Dele
private void Detour(ClipScheduler* clipScheduler)
{
Penumbra.Log.Excessive($"[Schedule Clip Update] Invoked on {(nint)clipScheduler:X}.");
var last = _state.SetAnimationData(
LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline));
var newData = LoadTimelineResources.GetDataFromTimeline(_objects, _collectionResolver, clipScheduler->SchedulerTimeline);
var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ScheduleClipUpdate);
Task.Result.Original(clipScheduler);
_state.RestoreAnimationData(last);
}

View file

@ -1,21 +1,25 @@
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.GameData;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Seems to load character actions when zoning or changing class, maybe. </summary>
public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly CrashHandlerService _crashHandler;
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver)
public SomeActionLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Some Action Load", Sigs.LoadSomeAction, Detour, true);
}
@ -24,8 +28,10 @@ public sealed unsafe class SomeActionLoad : FastHook<SomeActionLoad.Delegate>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private void Detour(ActionTimelineManager* timelineManager)
{
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true));
var newData = _collectionResolver.IdentifyCollection((GameObject*)timelineManager->Parent, true);
var last = _state.SetAnimationData(newData);
Penumbra.Log.Excessive($"[Some Action Load] Invoked on 0x{(nint)timelineManager:X}.");
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.ActionLoad);
Task.Result.Original(timelineManager);
_state.RestoreAnimationData(last);
}

View file

@ -1,23 +1,28 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData;
using Penumbra.Interop.PathResolving;
using Penumbra.Services;
namespace Penumbra.Interop.Hooks.Animation;
/// <summary> Unknown what exactly this is, but it seems to load a bunch of paps. </summary>
public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
{
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly GameState _state;
private readonly CollectionResolver _collectionResolver;
private readonly IObjectTable _objects;
private readonly CrashHandlerService _crashHandler;
public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects)
public SomePapLoad(HookManager hooks, GameState state, CollectionResolver collectionResolver, IObjectTable objects,
CrashHandlerService crashHandler)
{
_state = state;
_collectionResolver = collectionResolver;
_objects = objects;
_crashHandler = crashHandler;
Task = hooks.CreateHook<Delegate>("Some PAP Load", Sigs.LoadSomePap, Detour, true);
}
@ -33,8 +38,9 @@ public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);
if (actorIdx >= 0 && actorIdx < _objects.Length)
{
var last = _state.SetAnimationData(_collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx),
true));
var newData = _collectionResolver.IdentifyCollection((GameObject*)_objects.GetObjectAddress(actorIdx), true);
var last = _state.SetAnimationData(newData);
_crashHandler.LogAnimation(newData.AssociatedGameObject, newData.ModCollection, AnimationInvocationType.PapLoad);
Task.Result.Original(a1, a2, a3, a4);
_state.RestoreAnimationData(last);
return;

View file

@ -62,6 +62,7 @@ public sealed unsafe class LiveColorTablePreviewer : LiveMaterialPreviewerBase
_updatePending = true;
}
[SkipLocalsInit]
private void OnFrameworkUpdate(IFramework _)
{
if (!_updatePending)

View file

@ -89,6 +89,7 @@ internal partial record ResolveContext
};
}
[SkipLocalsInit]
private unsafe Utf8GamePath ResolveEquipmentMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
var variant = ResolveMaterialVariant(imc, Equipment.Variant);
@ -100,6 +101,7 @@ internal partial record ResolveContext
return Utf8GamePath.FromSpan(pathBuffer, out var path) ? path.Clone() : Utf8GamePath.Empty;
}
[SkipLocalsInit]
private unsafe Utf8GamePath ResolveWeaponMaterialPath(Utf8GamePath modelPath, ResourceHandle* imc, byte* mtrlFileName)
{
var setIdHigh = Equipment.Set.Id / 100;
@ -168,7 +170,7 @@ internal partial record ResolveContext
{
var modelPosition = modelPath.IndexOf("/model/"u8);
if (modelPosition < 0)
return Span<byte>.Empty;
return [];
var baseDirectory = modelPath[..modelPosition];

View file

@ -54,6 +54,7 @@ internal unsafe partial record ResolveContext(
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
}
[SkipLocalsInit]
private ResourceNode? CreateNodeFromTex(TextureResourceHandle* resourceHandle, ByteString gamePath, bool dx11)
{
if (resourceHandle == null)

View file

@ -7,8 +7,8 @@ namespace Penumbra.Mods.Editor;
public class DuplicateManager(ModManager modManager, SaveService saveService, Configuration config)
{
private readonly SHA256 _hasher = SHA256.Create();
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = [];
private readonly SHA256 _hasher = SHA256.Create();
private readonly List<(FullPath[] Paths, long Size, byte[] Hash)> _duplicates = [];
public IReadOnlyList<(FullPath[] Paths, long Size, byte[] Hash)> Duplicates
=> _duplicates;
@ -164,17 +164,17 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
}
/// <summary> Check if two files are identical on a binary level. Returns true if they are identical. </summary>
[SkipLocalsInit]
public static unsafe bool CompareFilesDirectly(FullPath f1, FullPath f2)
{
const int size = 256;
if (!f1.Exists || !f2.Exists)
return false;
using var s1 = File.OpenRead(f1.FullName);
using var s2 = File.OpenRead(f2.FullName);
var buffer1 = stackalloc byte[256];
var buffer2 = stackalloc byte[256];
var span1 = new Span<byte>(buffer1, 256);
var span2 = new Span<byte>(buffer2, 256);
using var s1 = File.OpenRead(f1.FullName);
using var s2 = File.OpenRead(f2.FullName);
Span<byte> span1 = stackalloc byte[size];
Span<byte> span2 = stackalloc byte[size];
while (true)
{
@ -186,7 +186,7 @@ public class DuplicateManager(ModManager modManager, SaveService saveService, Co
if (!span1[..bytes1].SequenceEqual(span2[..bytes2]))
return false;
if (bytes1 < 256)
if (bytes1 < size)
return true;
}
}

View file

@ -19,7 +19,6 @@ using ChangedItemClick = Penumbra.Communication.ChangedItemClick;
using ChangedItemHover = Penumbra.Communication.ChangedItemHover;
using OtterGui.Tasks;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Structs;
using Penumbra.UI;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
@ -74,8 +73,8 @@ public class Penumbra : IDalamudPlugin
_tempCollections = _services.GetService<TempCollectionManager>();
_redrawService = _services.GetService<RedrawService>();
_communicatorService = _services.GetService<CommunicatorService>();
_services.GetService<ResourceService>(); // Initialize because not required anywhere else.
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
_services.GetService<ResourceService>(); // Initialize because not required anywhere else.
_services.GetService<ModCacheManager>(); // Initialize because not required anywhere else.
_collectionManager.Caches.CreateNecessaryCaches();
_services.GetService<PathResolver>();
_services.GetService<ShaderReplacementFixer>();
@ -245,38 +244,38 @@ public class Penumbra : IDalamudPlugin
return sb.ToString();
}
private static string CollectLocaleEnvironmentVariables()
{
var variableNames = new List<string>();
var variables = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables())
{
var key = (string)variable.Key;
if (key.Equals("LANG", StringComparison.Ordinal) || key.StartsWith("LC_", StringComparison.Ordinal))
{
variableNames.Add(key);
variables.Add(key, ((string?)variable.Value) ?? string.Empty);
}
}
variableNames.Sort();
var pos = variableNames.IndexOf("LC_ALL");
if (pos > 0) // If it's == 0, we're going to do a no-op.
{
variableNames.RemoveAt(pos);
variableNames.Insert(0, "LC_ALL");
}
pos = variableNames.IndexOf("LANG");
if (pos >= 0 && pos < variableNames.Count - 1)
{
variableNames.RemoveAt(pos);
variableNames.Add("LANG");
}
return variableNames.Count == 0
? "None"
: string.Join(", ", variableNames.Select(name => $"`{name}={variables[name]}`"));
private static string CollectLocaleEnvironmentVariables()
{
var variableNames = new List<string>();
var variables = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (DictionaryEntry variable in Environment.GetEnvironmentVariables())
{
var key = (string)variable.Key;
if (key.Equals("LANG", StringComparison.Ordinal) || key.StartsWith("LC_", StringComparison.Ordinal))
{
variableNames.Add(key);
variables.Add(key, (string?)variable.Value ?? string.Empty);
}
}
variableNames.Sort();
var pos = variableNames.IndexOf("LC_ALL");
if (pos > 0) // If it's == 0, we're going to do a no-op.
{
variableNames.RemoveAt(pos);
variableNames.Insert(0, "LC_ALL");
}
pos = variableNames.IndexOf("LANG");
if (pos >= 0 && pos < variableNames.Count - 1)
{
variableNames.RemoveAt(pos);
variableNames.Add("LANG");
}
return variableNames.Count == 0
? "None"
: string.Join(", ", variableNames.Select(name => $"`{name}={variables[name]}`"));
}
}

View file

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<LangVersion>preview</LangVersion>
@ -69,8 +69,7 @@
<ItemGroup>
<PackageReference Include="EmbedIO" Version="3.4.3" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="4.8.0" />
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.2" />
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.3" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="SharpGLTF.Core" Version="1.0.0-alpha0030" />
@ -79,8 +78,10 @@
<ItemGroup>
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Penumbra.CrashHandler\Penumbra.CrashHandler.csproj" />
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.String.csproj" />
</ItemGroup>
<ItemGroup>
@ -103,4 +104,4 @@
<InformationalVersion>$(GitCommitHash)</InformationalVersion>
</PropertyGroup>
</Target>
</Project>
</Project>

View file

@ -0,0 +1,300 @@
using System.Text.Json;
using System.Text.Json.Nodes;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Communication;
using Penumbra.CrashHandler;
using Penumbra.CrashHandler.Buffers;
using Penumbra.GameData.Actors;
using Penumbra.Interop.ResourceLoading;
using Penumbra.Interop.Structs;
using Penumbra.String;
using Penumbra.String.Classes;
using FileMode = System.IO.FileMode;
namespace Penumbra.Services;
public sealed class CrashHandlerService : IDisposable, IService
{
private readonly FilenameService _files;
private readonly CommunicatorService _communicator;
private readonly ActorManager _actors;
private readonly ResourceLoader _resourceLoader;
private readonly Configuration _config;
private readonly ValidityChecker _validityChecker;
public CrashHandlerService(FilenameService files, CommunicatorService communicator, ActorManager actors, ResourceLoader resourceLoader,
Configuration config, ValidityChecker validityChecker)
{
_files = files;
_communicator = communicator;
_actors = actors;
_resourceLoader = resourceLoader;
_config = config;
_validityChecker = validityChecker;
if (!_config.UseCrashHandler)
return;
OpenEventWriter();
LaunchCrashHandler();
if (_eventWriter != null)
Subscribe();
}
public void Dispose()
{
CloseEventWriter();
_eventWriter?.Dispose();
if (_child != null)
{
_child.Kill();
Penumbra.Log.Debug($"Killed crash handler child process {_child.Id}.");
}
Unsubscribe();
}
private Process? _child;
private GameEventLogWriter? _eventWriter;
public string CopiedExe = string.Empty;
public string OriginalExe
=> _files.CrashHandlerExe;
public string LogPath
=> _files.LogFileName;
public int ChildProcessId
=> _child?.Id ?? -1;
public int ProcessId
=> Environment.ProcessId;
public bool IsRunning
=> _eventWriter != null && _child is { HasExited: false };
public int ChildExitCode
=> IsRunning ? 0 : _child?.ExitCode ?? 0;
public void Enable()
{
if (_config.UseCrashHandler)
return;
_config.UseCrashHandler = true;
_config.Save();
OpenEventWriter();
LaunchCrashHandler();
if (_eventWriter != null)
Subscribe();
}
public void Disable()
{
if (!_config.UseCrashHandler)
return;
_config.UseCrashHandler = false;
_config.Save();
CloseEventWriter();
CloseCrashHandler();
Unsubscribe();
}
public JsonObject? Load(string fileName)
{
if (!File.Exists(fileName))
return null;
try
{
var data = File.ReadAllText(fileName);
return JsonNode.Parse(data) as JsonObject;
}
catch (Exception ex)
{
Penumbra.Log.Error($"Could not parse crash dump at {fileName}:\n{ex}");
return null;
}
}
public void CloseCrashHandler()
{
if (_child == null)
return;
try
{
if (_child.HasExited)
return;
_child.Kill();
Penumbra.Log.Debug($"Closed Crash Handler at {CopiedExe}.");
}
catch (Exception ex)
{
_child = null;
Penumbra.Log.Debug($"Closed not close Crash Handler at {CopiedExe}:\n{ex}.");
}
}
public void LaunchCrashHandler()
{
try
{
CloseCrashHandler();
CopiedExe = CopyExecutables();
var info = new ProcessStartInfo()
{
CreateNoWindow = true,
FileName = CopiedExe,
};
info.ArgumentList.Add(_files.LogFileName);
info.ArgumentList.Add(Environment.ProcessId.ToString());
info.ArgumentList.Add($"{_validityChecker.Version} ({_validityChecker.CommitHash})");
info.ArgumentList.Add(_validityChecker.GameVersion);
_child = Process.Start(info);
if (_child == null)
throw new Exception("Child Process could not be created.");
Penumbra.Log.Information($"Opened Crash Handler at {CopiedExe}, PID {_child.Id}.");
}
catch (Exception ex)
{
Penumbra.Log.Error($"Could not launch crash handler process:\n{ex}");
CloseCrashHandler();
_child = null;
}
}
public JsonObject? Dump()
{
if (_eventWriter == null)
return null;
try
{
using var reader = new GameEventLogReader();
JsonObject jObj;
lock (_eventWriter)
{
jObj = reader.Dump("Manual Dump", Environment.ProcessId, 0, $"{_validityChecker.Version} ({_validityChecker.CommitHash})", _validityChecker.GameVersion);
}
var logFile = _files.LogFileName;
using var s = File.Open(logFile, FileMode.Create);
using var jw = new Utf8JsonWriter(s, new JsonWriterOptions() { Indented = true });
jObj.WriteTo(jw);
Penumbra.Log.Information($"Dumped crash handler memory to {logFile}.");
return jObj;
}
catch (Exception ex)
{
Penumbra.Log.Error($"Error dumping crash handler memory to file:\n{ex}");
return null;
}
}
private string CopyExecutables()
{
var parent = Path.GetDirectoryName(_files.CrashHandlerExe)!;
var folder = Path.Combine(parent, "temp");
Directory.CreateDirectory(folder);
foreach (var file in Directory.EnumerateFiles(parent, "Penumbra.CrashHandler.*"))
File.Copy(file, Path.Combine(folder, Path.GetFileName(file)), true);
return Path.Combine(folder, Path.GetFileName(_files.CrashHandlerExe));
}
public void LogAnimation(nint character, ModCollection collection, AnimationInvocationType type)
{
if (_eventWriter == null)
return;
var name = GetActorName(character);
lock (_eventWriter)
{
_eventWriter?.AnimationFuncInvoked.WriteLine(character, name.Span, collection.Name, type);
}
}
private void OnCreatingCharacterBase(nint address, string collection, nint _1, nint _2, nint _3)
{
if (_eventWriter == null)
return;
var name = GetActorName(address);
lock (_eventWriter)
{
_eventWriter?.CharacterBase.WriteLine(address, name.Span, collection);
}
}
private unsafe ByteString GetActorName(nint address)
{
var obj = (GameObject*)address;
if (obj == null)
return ByteString.FromSpanUnsafe("Unknown"u8, true, false, true);
var id = _actors.FromObject(obj, out _, false, true, false);
return id.IsValid ? ByteString.FromStringUnsafe(id.Incognito(null), false) :
obj->Name[0] != 0 ? new ByteString(obj->Name) : ByteString.FromStringUnsafe($"Actor #{obj->ObjectIndex}", false);
}
private unsafe void OnResourceLoaded(ResourceHandle* handle, Utf8GamePath originalPath, FullPath? manipulatedPath, ResolveData resolveData)
{
if (manipulatedPath == null || _eventWriter == null)
return;
var dashIdx = manipulatedPath.Value.InternalName[0] == (byte)'|' ? manipulatedPath.Value.InternalName.IndexOf((byte)'|', 1) : -1;
if (dashIdx >= 0 && !Utf8GamePath.IsRooted(manipulatedPath.Value.InternalName.Substring(dashIdx + 1)))
return;
var name = GetActorName(resolveData.AssociatedGameObject);
lock (_eventWriter)
{
_eventWriter!.FileLoaded.WriteLine(resolveData.AssociatedGameObject, name.Span, resolveData.ModCollection.Name,
manipulatedPath.Value.InternalName.Span, originalPath.Path.Span);
}
}
private void CloseEventWriter()
{
if (_eventWriter == null)
return;
_eventWriter.Dispose();
_eventWriter = null;
Penumbra.Log.Debug("Closed Event Writer for crash handler.");
}
private void OpenEventWriter()
{
try
{
CloseEventWriter();
_eventWriter = new GameEventLogWriter();
Penumbra.Log.Debug("Opened new Event Writer for crash handler.");
}
catch (Exception ex)
{
Penumbra.Log.Error($"Could not open Event Writer:\n{ex}");
CloseEventWriter();
}
}
private unsafe void Subscribe()
{
_communicator.CreatingCharacterBase.Subscribe(OnCreatingCharacterBase, CreatingCharacterBase.Priority.CrashHandler);
_resourceLoader.ResourceLoaded += OnResourceLoaded;
}
private unsafe void Unsubscribe()
{
_communicator.CreatingCharacterBase.Unsubscribe(OnCreatingCharacterBase);
_resourceLoader.ResourceLoaded -= OnResourceLoaded;
}
}

View file

@ -16,6 +16,12 @@ public class FilenameService(DalamudPluginInterface pi) : IService
public readonly string ActiveCollectionsFile = Path.Combine(pi.ConfigDirectory.FullName, "active_collections.json");
public readonly string SharedTagFile = Path.Combine(pi.ConfigDirectory.FullName, "shared_tags.json");
public readonly string CrashHandlerExe =
Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "Penumbra.CrashHandler.exe");
public readonly string LogFileName =
Path.Combine(Path.GetDirectoryName(Path.GetDirectoryName(pi.ConfigDirectory.FullName)!)!, "Penumbra.log");
/// <summary> Obtain the path of a collection file given its name.</summary>
public string CollectionFile(ModCollection collection)
=> CollectionFile(collection.Name);

View file

@ -1,5 +1,6 @@
using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using OtterGui.Classes;
using OtterGui.Services;
@ -20,6 +21,7 @@ public class ValidityChecker : IService
public readonly string Version;
public readonly string CommitHash;
public readonly string GameVersion;
public ValidityChecker(DalamudPluginInterface pi)
{
@ -28,14 +30,19 @@ public class ValidityChecker : IService
IsValidSourceRepo = CheckSourceRepo(pi);
var assembly = GetType().Assembly;
Version = assembly.GetName().Version?.ToString() ?? string.Empty;
CommitHash = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
Version = assembly.GetName().Version?.ToString() ?? string.Empty;
CommitHash = assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion ?? "Unknown";
GameVersion = GetGameVersion();
}
private static unsafe string GetGameVersion()
=> Framework.Instance()->GameVersion[0];
public void LogExceptions()
{
if (ImcExceptions.Count > 0)
Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.", NotificationType.Warning);
Penumbra.Messager.NotificationMessage($"{ImcExceptions} IMC Exceptions thrown during Penumbra load. Please repair your game files.",
NotificationType.Warning);
}
// Because remnants of penumbra in devPlugins cause issues, we check for them to warn users to remove them.

View file

@ -12,22 +12,13 @@ using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public class ChangedItemsTab : ITab
public class ChangedItemsTab(
CollectionManager collectionManager,
CollectionSelectHeader collectionHeader,
ChangedItemDrawer drawer,
CommunicatorService communicator)
: ITab
{
private readonly CollectionManager _collectionManager;
private readonly ChangedItemDrawer _drawer;
private readonly CollectionSelectHeader _collectionHeader;
private readonly CommunicatorService _communicator;
public ChangedItemsTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader, ChangedItemDrawer drawer,
CommunicatorService communicator)
{
_collectionManager = collectionManager;
_collectionHeader = collectionHeader;
_drawer = drawer;
_communicator = communicator;
}
public ReadOnlySpan<byte> Label
=> "Changed Items"u8;
@ -36,8 +27,8 @@ public class ChangedItemsTab : ITab
public void DrawContent()
{
_collectionHeader.Draw(true);
_drawer.DrawTypeFilter();
collectionHeader.Draw(true);
drawer.DrawTypeFilter();
var varWidth = DrawFilters();
using var child = ImRaii.Child("##changedItemsChild", -Vector2.One);
if (!child)
@ -54,7 +45,7 @@ public class ChangedItemsTab : ITab
ImGui.TableSetupColumn("mods", flags, varWidth - 130 * UiHelpers.Scale);
ImGui.TableSetupColumn("id", flags, 130 * UiHelpers.Scale);
var items = _collectionManager.Active.Current.ChangedItems;
var items = collectionManager.Active.Current.ChangedItems;
var rest = ImGuiClip.FilteredClippedDraw(items, skips, FilterChangedItem, DrawChangedItemColumn);
ImGuiClip.DrawEndDummy(rest, height);
}
@ -75,21 +66,21 @@ public class ChangedItemsTab : ITab
/// <summary> Apply the current filters. </summary>
private bool FilterChangedItem(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
=> _drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
=> drawer.FilterChangedItem(item.Key, item.Value.Item2, _changedItemFilter)
&& (_changedItemModFilter.IsEmpty || item.Value.Item1.Any(m => m.Name.Contains(_changedItemModFilter)));
/// <summary> Draw a full column for a changed item. </summary>
private void DrawChangedItemColumn(KeyValuePair<string, (SingleArray<IMod>, object?)> item)
{
ImGui.TableNextColumn();
_drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
drawer.DrawCategoryIcon(item.Key, item.Value.Item2);
ImGui.SameLine();
_drawer.DrawChangedItem(item.Key, item.Value.Item2);
drawer.DrawChangedItem(item.Key, item.Value.Item2);
ImGui.TableNextColumn();
DrawModColumn(item.Value.Item1);
ImGui.TableNextColumn();
_drawer.DrawModelData(item.Value.Item2);
drawer.DrawModelData(item.Value.Item2);
}
private void DrawModColumn(SingleArray<IMod> mods)
@ -102,7 +93,7 @@ public class ChangedItemsTab : ITab
if (ImGui.Selectable(first.Name, false, ImGuiSelectableFlags.None, new Vector2(0, ImGui.GetFrameHeight()))
&& ImGui.GetIO().KeyCtrl
&& first is Mod mod)
_communicator.SelectTab.Invoke(TabType.Mods, mod);
communicator.SelectTab.Invoke(TabType.Mods, mod);
if (ImGui.IsItemHovered())
{

View file

@ -44,8 +44,8 @@ public class ConfigTabBar : IDisposable
Watcher = watcher;
OnScreen = onScreen;
Messages = messages;
Tabs = new ITab[]
{
Tabs =
[
Settings,
Collections,
Mods,
@ -56,7 +56,7 @@ public class ConfigTabBar : IDisposable
Resource,
Watcher,
Messages,
};
];
_communicator.SelectTab.Subscribe(OnSelectTab, Communication.SelectTab.Priority.ConfigTabBar);
}

View file

@ -0,0 +1,110 @@
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.CrashHandler;
namespace Penumbra.UI.Tabs.Debug;
public static class CrashDataExtensions
{
public static void DrawMeta(this CrashData data)
{
using (ImRaii.Group())
{
ImGui.TextUnformatted(nameof(data.Mode));
ImGui.TextUnformatted(nameof(data.CrashTime));
ImGui.TextUnformatted("Current Age");
ImGui.TextUnformatted(nameof(data.Version));
ImGui.TextUnformatted(nameof(data.GameVersion));
ImGui.TextUnformatted(nameof(data.ExitCode));
ImGui.TextUnformatted(nameof(data.ProcessId));
ImGui.TextUnformatted(nameof(data.TotalModdedFilesLoaded));
ImGui.TextUnformatted(nameof(data.TotalCharactersLoaded));
ImGui.TextUnformatted(nameof(data.TotalVFXFuncsInvoked));
}
ImGui.SameLine();
using (ImRaii.Group())
{
ImGui.TextUnformatted(data.Mode);
ImGui.TextUnformatted(data.CrashTime.ToString());
ImGui.TextUnformatted((DateTimeOffset.UtcNow - data.CrashTime).ToString(@"dd\.hh\:mm\:ss"));
ImGui.TextUnformatted(data.Version);
ImGui.TextUnformatted(data.GameVersion);
ImGui.TextUnformatted(data.ExitCode.ToString());
ImGui.TextUnformatted(data.ProcessId.ToString());
ImGui.TextUnformatted(data.TotalModdedFilesLoaded.ToString());
ImGui.TextUnformatted(data.TotalCharactersLoaded.ToString());
ImGui.TextUnformatted(data.TotalVFXFuncsInvoked.ToString());
}
}
public static void DrawCharacters(this CrashData data)
{
using var tree = ImRaii.TreeNode("Last Characters");
if (!tree)
return;
using var table = ImRaii.Table("##characterTable", 6,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
if (!table)
return;
ImGuiClip.ClippedDraw(data.LastCharactersLoaded, character =>
{
ImGuiUtil.DrawTableColumn(character.Age.ToString(CultureInfo.InvariantCulture));
ImGuiUtil.DrawTableColumn(character.ThreadId.ToString());
ImGuiUtil.DrawTableColumn(character.CharacterName);
ImGuiUtil.DrawTableColumn(character.CollectionName);
ImGuiUtil.DrawTableColumn(character.CharacterAddress);
ImGuiUtil.DrawTableColumn(character.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
}
public static void DrawFiles(this CrashData data)
{
using var tree = ImRaii.TreeNode("Last Files");
if (!tree)
return;
using var table = ImRaii.Table("##filesTable", 8,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
if (!table)
return;
ImGuiClip.ClippedDraw(data.LastModdedFilesLoaded, file =>
{
ImGuiUtil.DrawTableColumn(file.Age.ToString(CultureInfo.InvariantCulture));
ImGuiUtil.DrawTableColumn(file.ThreadId.ToString());
ImGuiUtil.DrawTableColumn(file.ActualFileName);
ImGuiUtil.DrawTableColumn(file.RequestedFileName);
ImGuiUtil.DrawTableColumn(file.CharacterName);
ImGuiUtil.DrawTableColumn(file.CollectionName);
ImGuiUtil.DrawTableColumn(file.CharacterAddress);
ImGuiUtil.DrawTableColumn(file.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
}
public static void DrawVfxInvocations(this CrashData data)
{
using var tree = ImRaii.TreeNode("Last VFX Invocations");
if (!tree)
return;
using var table = ImRaii.Table("##vfxTable", 7,
ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg | ImGuiTableFlags.BordersInner);
if (!table)
return;
ImGuiClip.ClippedDraw(data.LastVfxFuncsInvoked, vfx =>
{
ImGuiUtil.DrawTableColumn(vfx.Age.ToString(CultureInfo.InvariantCulture));
ImGuiUtil.DrawTableColumn(vfx.ThreadId.ToString());
ImGuiUtil.DrawTableColumn(vfx.InvocationType);
ImGuiUtil.DrawTableColumn(vfx.CharacterName);
ImGuiUtil.DrawTableColumn(vfx.CollectionName);
ImGuiUtil.DrawTableColumn(vfx.CharacterAddress);
ImGuiUtil.DrawTableColumn(vfx.Timestamp.ToString());
}, ImGui.GetTextLineHeightWithSpacing());
}
}

View file

@ -0,0 +1,136 @@
using System.Text.Json;
using Dalamud.Interface.DragDrop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.CrashHandler;
using Penumbra.Services;
namespace Penumbra.UI.Tabs.Debug;
public class CrashHandlerPanel(CrashHandlerService _service, Configuration _config, IDragDropManager _dragDrop) : IService
{
private CrashData? _lastDump;
private string _lastLoadedFile = string.Empty;
private CrashData? _lastLoad;
private Exception? _lastLoadException;
public void Draw()
{
DrawDropSource();
DrawData();
DrawDropTarget();
}
private void DrawData()
{
using var _ = ImRaii.Group();
using var header = ImRaii.CollapsingHeader("Crash Handler");
if (!header)
return;
DrawButtons();
DrawMainData();
DrawObject("Last Manual Dump", _lastDump, null);
DrawObject(_lastLoadedFile.Length > 0 ? $"Loaded File ({_lastLoadedFile})###Loaded File" : "Loaded File", _lastLoad,
_lastLoadException);
}
private void DrawMainData()
{
using var table = ImRaii.Table("##CrashHandlerTable", 2, ImGuiTableFlags.SizingFixedFit);
if (!table)
return;
PrintValue("Enabled", _config.UseCrashHandler);
PrintValue("Copied Executable Path", _service.CopiedExe);
PrintValue("Original Executable Path", _service.OriginalExe);
PrintValue("Log File Path", _service.LogPath);
PrintValue("XIV Process ID", _service.ProcessId.ToString());
PrintValue("Crash Handler Running", _service.IsRunning.ToString());
PrintValue("Crash Handler Process ID", _service.ChildProcessId.ToString());
PrintValue("Crash Handler Exit Code", _service.ChildExitCode.ToString());
}
private void DrawButtons()
{
if (ImGui.Button("Dump Crash Handler Memory"))
_lastDump = _service.Dump()?.Deserialize<CrashData>();
if (ImGui.Button("Enable"))
_service.Enable();
ImGui.SameLine();
if (ImGui.Button("Disable"))
_service.Disable();
if (ImGui.Button("Shutdown Crash Handler"))
_service.CloseCrashHandler();
ImGui.SameLine();
if (ImGui.Button("Relaunch Crash Handler"))
_service.LaunchCrashHandler();
}
private void DrawDropSource()
{
_dragDrop.CreateImGuiSource("LogDragDrop", m => m.Files.Any(f => f.EndsWith("Penumbra.log")), m =>
{
ImGui.TextUnformatted("Dragging Penumbra.log for import.");
return true;
});
}
private void DrawDropTarget()
{
if (!_dragDrop.CreateImGuiTarget("LogDragDrop", out var files, out _))
return;
var file = files.FirstOrDefault(f => f.EndsWith("Penumbra.log"));
if (file == null)
return;
_lastLoadedFile = file;
try
{
var jObj = _service.Load(file);
_lastLoad = jObj?.Deserialize<CrashData>();
_lastLoadException = null;
}
catch (Exception ex)
{
_lastLoad = null;
_lastLoadException = ex;
}
}
private static void DrawObject(string name, CrashData? data, Exception? ex)
{
using var tree = ImRaii.TreeNode(name);
if (!tree)
return;
if (ex != null)
{
ImGuiUtil.TextWrapped(ex.ToString());
return;
}
if (data == null)
{
ImGui.TextUnformatted("Nothing loaded.");
return;
}
data.DrawMeta();
data.DrawFiles();
data.DrawCharacters();
data.DrawVfxInvocations();
}
private static void PrintValue<T>(string label, in T data)
{
ImGuiUtil.DrawTableColumn(label);
ImGuiUtil.DrawTableColumn(data?.ToString() ?? "NULL");
}
}

View file

@ -53,7 +53,7 @@ public class Diagnostics(IServiceProvider provider)
foreach (var type in typeof(ActorManager).Assembly.GetTypes()
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer))))
{
var container = (IAsyncDataContainer) provider.GetRequiredService(type);
var container = (IAsyncDataContainer)provider.GetRequiredService(type);
ImGuiUtil.DrawTableColumn(container.Name);
ImGuiUtil.DrawTableColumn(container.Time.ToString());
ImGuiUtil.DrawTableColumn(Functions.HumanReadableSize(container.Memory));
@ -88,18 +88,22 @@ public class DebugTab : Window, ITab
private readonly TextureManager _textureManager;
private readonly ShaderReplacementFixer _shaderReplacementFixer;
private readonly RedrawService _redraws;
private readonly DictEmote _emotes;
private readonly DictEmote _emotes;
private readonly Diagnostics _diagnostics;
private readonly IObjectTable _objects;
private readonly IClientState _clientState;
private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects, IClientState clientState,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains, CharacterUtility characterUtility, ResidentResourceManager residentResources,
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, IObjectTable objects,
IClientState clientState,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, PenumbraIpcProviders ipc, CollectionResolver collectionResolver,
DrawObjectState drawObjectState, PathState pathState, SubfileHelper subfileHelper, IdentifiedCollectionCache identifiedCollectionCache,
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes, Diagnostics diagnostics, IpcTester ipcTester)
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel)
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
{
IsOpen = true;
@ -134,7 +138,8 @@ public class DebugTab : Window, ITab
_redraws = redraws;
_emotes = emotes;
_diagnostics = diagnostics;
_ipcTester = ipcTester;
_ipcTester = ipcTester;
_crashHandlerPanel = crashHandlerPanel;
_objects = objects;
_clientState = clientState;
}
@ -158,6 +163,9 @@ public class DebugTab : Window, ITab
return;
DrawDebugTabGeneral();
ImGui.NewLine();
_crashHandlerPanel.Draw();
ImGui.NewLine();
_diagnostics.DrawDiagnostics();
DrawPerformanceTab();
ImGui.NewLine();
@ -257,6 +265,7 @@ public class DebugTab : Window, ITab
}
}
var issues = _modManager.WithIndex().Count(p => p.Index != p.Value.Index);
using (var tree = TreeNode($"Mods ({issues} Issues)###Mods"))
{
@ -394,7 +403,7 @@ public class DebugTab : Window, ITab
private void DrawPerformanceTab()
{
ImGui.NewLine();
if (ImGui.CollapsingHeader("Performance"))
if (!ImGui.CollapsingHeader("Performance"))
return;
using (var start = TreeNode("Startup Performance", ImGuiTreeNodeFlags.DefaultOpen))

View file

@ -8,31 +8,22 @@ using Penumbra.Collections;
using Penumbra.Collections.Cache;
using Penumbra.Collections.Manager;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.Mods.Editor;
using Penumbra.String.Classes;
using Penumbra.UI.Classes;
namespace Penumbra.UI.Tabs;
public class EffectiveTab : ITab
public class EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
: ITab
{
private readonly CollectionManager _collectionManager;
private readonly CollectionSelectHeader _collectionHeader;
public EffectiveTab(CollectionManager collectionManager, CollectionSelectHeader collectionHeader)
{
_collectionManager = collectionManager;
_collectionHeader = collectionHeader;
}
public ReadOnlySpan<byte> Label
=> "Effective Changes"u8;
public void DrawContent()
{
SetupEffectiveSizes();
_collectionHeader.Draw(true);
collectionHeader.Draw(true);
DrawFilters();
using var child = ImRaii.Child("##EffectiveChangesTab", -Vector2.One, false);
if (!child)
@ -48,7 +39,7 @@ public class EffectiveTab : ITab
ImGui.TableSetupColumn(string.Empty, ImGuiTableColumnFlags.WidthFixed, _effectiveArrowLength);
ImGui.TableSetupColumn("##file", ImGuiTableColumnFlags.WidthFixed, _effectiveRightTextLength);
DrawEffectiveRows(_collectionManager.Active.Current, skips, height,
DrawEffectiveRows(collectionManager.Active.Current, skips, height,
_effectiveFilePathFilter.Length > 0 || _effectiveGamePathFilter.Length > 0);
}

View file

@ -17,68 +17,53 @@ using Penumbra.Collections.Manager;
namespace Penumbra.UI.Tabs;
public class ModsTab : ITab
public class ModsTab(
ModManager modManager,
CollectionManager collectionManager,
ModFileSystemSelector selector,
ModPanel panel,
TutorialService tutorial,
RedrawService redrawService,
Configuration config,
IClientState clientState,
CollectionSelectHeader collectionHeader,
ITargetManager targets,
IObjectTable objectTable)
: ITab
{
private readonly ModFileSystemSelector _selector;
private readonly ModPanel _panel;
private readonly TutorialService _tutorial;
private readonly ModManager _modManager;
private readonly ActiveCollections _activeCollections;
private readonly RedrawService _redrawService;
private readonly Configuration _config;
private readonly IClientState _clientState;
private readonly CollectionSelectHeader _collectionHeader;
private readonly ITargetManager _targets;
private readonly IObjectTable _objectTable;
public ModsTab(ModManager modManager, CollectionManager collectionManager, ModFileSystemSelector selector, ModPanel panel,
TutorialService tutorial, RedrawService redrawService, Configuration config, IClientState clientState,
CollectionSelectHeader collectionHeader, ITargetManager targets, IObjectTable objectTable)
{
_modManager = modManager;
_activeCollections = collectionManager.Active;
_selector = selector;
_panel = panel;
_tutorial = tutorial;
_redrawService = redrawService;
_config = config;
_clientState = clientState;
_collectionHeader = collectionHeader;
_targets = targets;
_objectTable = objectTable;
}
private readonly ActiveCollections _activeCollections = collectionManager.Active;
public bool IsVisible
=> _modManager.Valid;
=> modManager.Valid;
public ReadOnlySpan<byte> Label
=> "Mods"u8;
public void DrawHeader()
=> _tutorial.OpenTutorial(BasicTutorialSteps.Mods);
=> tutorial.OpenTutorial(BasicTutorialSteps.Mods);
public Mod SelectMod
{
set => _selector.SelectByValue(value);
set => selector.SelectByValue(value);
}
public void DrawContent()
{
try
{
_selector.Draw(GetModSelectorSize(_config));
selector.Draw(GetModSelectorSize(config));
ImGui.SameLine();
using var group = ImRaii.Group();
_collectionHeader.Draw(false);
collectionHeader.Draw(false);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, _config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()),
using (var child = ImRaii.Child("##ModsTabMod", new Vector2(-1, config.HideRedrawBar ? 0 : -ImGui.GetFrameHeight()),
true, ImGuiWindowFlags.HorizontalScrollbar))
{
style.Pop();
if (child)
_panel.Draw();
panel.Draw();
style.Push(ImGuiStyleVar.ItemSpacing, Vector2.Zero);
}
@ -89,14 +74,14 @@ public class ModsTab : ITab
catch (Exception e)
{
Penumbra.Log.Error($"Exception thrown during ModPanel Render:\n{e}");
Penumbra.Log.Error($"{_modManager.Count} Mods\n"
Penumbra.Log.Error($"{modManager.Count} Mods\n"
+ $"{_activeCollections.Current.AnonymizedName} Current Collection\n"
+ $"{_activeCollections.Current.Settings.Count} Settings\n"
+ $"{_selector.SortMode.Name} Sort Mode\n"
+ $"{_selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{_selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{selector.SortMode.Name} Sort Mode\n"
+ $"{selector.SelectedLeaf?.Name ?? "NULL"} Selected Leaf\n"
+ $"{selector.Selected?.Name ?? "NULL"} Selected Mod\n"
+ $"{string.Join(", ", _activeCollections.Current.DirectlyInheritsFrom.Select(c => c.AnonymizedName))} Inheritances\n"
+ $"{_selector.SelectedSettingCollection.AnonymizedName} Collection\n");
+ $"{selector.SelectedSettingCollection.AnonymizedName} Collection\n");
}
}
@ -115,9 +100,9 @@ public class ModsTab : ITab
private void DrawRedrawLine()
{
if (_config.HideRedrawBar)
if (config.HideRedrawBar)
{
_tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
tutorial.SkipTutorial(BasicTutorialSteps.Redrawing);
return;
}
@ -135,15 +120,15 @@ public class ModsTab : ITab
}
var hovered = ImGui.IsItemHovered();
_tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
tutorial.OpenTutorial(BasicTutorialSteps.Redrawing);
if (hovered)
ImGui.SetTooltip($"The supported modifiers for '/penumbra redraw' are:\n{TutorialService.SupportedRedrawModifiers}");
using var id = ImRaii.PushId("Redraw");
using var disabled = ImRaii.Disabled(_clientState.LocalPlayer == null);
using var disabled = ImRaii.Disabled(clientState.LocalPlayer == null);
ImGui.SameLine();
var buttonWidth = frameHeight with { X = ImGui.GetContentRegionAvail().X / 5 };
var tt = _objectTable.GetObjectAddress(0) == nint.Zero
var tt = objectTable.GetObjectAddress(0) == nint.Zero
? "\nCan only be used when you are logged in and your character is available."
: string.Empty;
DrawButton(buttonWidth, "All", string.Empty, tt);
@ -151,13 +136,13 @@ public class ModsTab : ITab
DrawButton(buttonWidth, "Self", "self", tt);
ImGui.SameLine();
tt = _targets.Target == null && _targets.GPoseTarget == null
tt = targets.Target == null && targets.GPoseTarget == null
? "\nCan only be used when you have a target."
: string.Empty;
DrawButton(buttonWidth, "Target", "target", tt);
ImGui.SameLine();
tt = _targets.FocusTarget == null
tt = targets.FocusTarget == null
? "\nCan only be used when you have a focus target."
: string.Empty;
DrawButton(buttonWidth, "Focus", "focus", tt);
@ -176,9 +161,9 @@ public class ModsTab : ITab
if (ImGui.Button(label, size))
{
if (lower.Length > 0)
_redrawService.RedrawObject(lower, RedrawType.Redraw);
redrawService.RedrawObject(lower, RedrawType.Redraw);
else
_redrawService.RedrawAll(RedrawType.Redraw);
redrawService.RedrawAll(RedrawType.Redraw);
}
}

View file

@ -7,7 +7,7 @@ namespace Penumbra.UI.Tabs;
public class OnScreenTab : ITab
{
private readonly Configuration _config;
private ResourceTreeViewer _viewer;
private readonly ResourceTreeViewer _viewer;
public OnScreenTab(Configuration config, ResourceTreeFactory treeFactory, ChangedItemDrawer changedItemDrawer)
{

View file

@ -12,24 +12,14 @@ using Penumbra.String.Classes;
namespace Penumbra.UI.Tabs;
public class ResourceTab : ITab
public class ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
: ITab
{
private readonly Configuration _config;
private readonly ResourceManagerService _resourceManager;
private readonly ISigScanner _sigScanner;
public ResourceTab(Configuration config, ResourceManagerService resourceManager, ISigScanner sigScanner)
{
_config = config;
_resourceManager = resourceManager;
_sigScanner = sigScanner;
}
public ReadOnlySpan<byte> Label
=> "Resource Manager"u8;
public bool IsVisible
=> _config.DebugMode;
=> config.DebugMode;
/// <summary> Draw a tab to iterate over the main resource maps and see what resources are currently loaded. </summary>
public void DrawContent()
@ -44,15 +34,15 @@ public class ResourceTab : ITab
unsafe
{
_resourceManager.IterateGraphs(DrawCategoryContainer);
resourceManager.IterateGraphs(DrawCategoryContainer);
}
ImGui.NewLine();
unsafe
{
ImGui.TextUnformatted(
$"Static Address: 0x{(ulong)_resourceManager.ResourceManagerAddress:X} (+0x{(ulong)_resourceManager.ResourceManagerAddress - (ulong)_sigScanner.Module.BaseAddress:X})");
ImGui.TextUnformatted($"Actual Address: 0x{(ulong)_resourceManager.ResourceManager:X}");
$"Static Address: 0x{(ulong)resourceManager.ResourceManagerAddress:X} (+0x{(ulong)resourceManager.ResourceManagerAddress - (ulong)sigScanner.Module.BaseAddress:X})");
ImGui.TextUnformatted($"Actual Address: 0x{(ulong)resourceManager.ResourceManager:X}");
}
}
@ -82,7 +72,7 @@ public class ResourceTab : ITab
ImGui.TableSetupColumn("Refs", ImGuiTableColumnFlags.WidthFixed, _refsColumnWidth);
ImGui.TableHeadersRow();
_resourceManager.IterateResourceMap(map, (hash, r) =>
resourceManager.IterateResourceMap(map, (hash, r) =>
{
// Filter unwanted names.
if (_resourceManagerFilter.Length != 0
@ -125,7 +115,7 @@ public class ResourceTab : ITab
if (tree)
{
SetTableWidths();
_resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m));
resourceManager.IterateExtMap(map, (ext, m) => DrawResourceMap(category, ext, m));
}
}

View file

@ -11,18 +11,6 @@
"Unosquare.Swan.Lite": "3.0.0"
}
},
"Microsoft.CodeAnalysis.Common": {
"type": "Direct",
"requested": "[4.8.0, )",
"resolved": "4.8.0",
"contentHash": "/jR+e/9aT+BApoQJABlVCKnnggGQbvGh7BKq2/wI1LamxC+LbzhcLj4Vj7gXCofl1n4E521YfF9w0WcASGg/KA==",
"dependencies": {
"Microsoft.CodeAnalysis.Analyzers": "3.3.4",
"System.Collections.Immutable": "7.0.0",
"System.Reflection.Metadata": "7.0.0",
"System.Runtime.CompilerServices.Unsafe": "6.0.0"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Direct",
"requested": "[7.0.0, )",
@ -55,29 +43,15 @@
},
"SixLabors.ImageSharp": {
"type": "Direct",
"requested": "[2.1.2, )",
"resolved": "2.1.2",
"contentHash": "In0pC521LqJXJXZgFVHegvSzES10KkKRN31McxqA1+fKtKsNe+EShWavBFQnKRlXCdeAmfx/wDjLILbvCaq+8Q==",
"dependencies": {
"System.Runtime.CompilerServices.Unsafe": "5.0.0",
"System.Text.Encoding.CodePages": "5.0.0"
}
},
"Microsoft.CodeAnalysis.Analyzers": {
"type": "Transitive",
"resolved": "3.3.4",
"contentHash": "AxkxcPR+rheX0SmvpLVIGLhOUXAKG56a64kV9VQZ4y9gR9ZmPXnqZvHJnmwLSwzrEP6junUF11vuc+aqo5r68g=="
"requested": "[3.1.3, )",
"resolved": "3.1.3",
"contentHash": "wybtaqZQ1ZRZ4ZeU+9h+PaSeV14nyiGKIy7qRbDfSHzHq4ybqyOcjoifeaYbiKLO1u+PVxLBuy7MF/DMmwwbfg=="
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
},
"Microsoft.NETCore.Platforms": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "VyPlqzH2wavqquTcYpkIIAQ6WdenuKoFN0BdYBbCWsclXacSOHNQn66Gt4z5NBqEYW0FAPm5rlvki9ZiCij5xQ=="
},
"SharpGLTF.Runtime": {
"type": "Transitive",
"resolved": "1.0.0-alpha0030",
@ -86,32 +60,6 @@
"SharpGLTF.Core": "1.0.0-alpha0030"
}
},
"System.Collections.Immutable": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "MclTG61lsD9sYdpNz9xsKBzjsmsfCtcMZYXz/IUr2zlhaTaABonlr1ESeompTgM+Xk+IwtGYU7/voh3YWB/fWw==",
"dependencies": {
"System.Collections.Immutable": "7.0.0"
}
},
"System.Runtime.CompilerServices.Unsafe": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "/iUeP3tq1S0XdNNoMz5C9twLSrM/TH+qElHkXWaPvuNOt+99G75NrV0OS2EqHx5wMN7popYjpc8oTjC1y16DLg=="
},
"System.Text.Encoding.CodePages": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "NyscU59xX6Uo91qvhOs2Ccho3AR2TnZPomo1Z0K6YpyztBPM/A5VbkzOO19sy3A3i1TtEnTxA7bCe3Us+r5MWg==",
"dependencies": {
"Microsoft.NETCore.Platforms": "5.0.0"
}
},
"System.ValueTuple": {
"type": "Transitive",
"resolved": "4.5.0",
@ -134,11 +82,14 @@
"penumbra.api": {
"type": "Project"
},
"penumbra.crashhandler": {
"type": "Project"
},
"penumbra.gamedata": {
"type": "Project",
"dependencies": {
"OtterGui": "[1.0.0, )",
"Penumbra.Api": "[1.0.13, )",
"Penumbra.Api": "[1.0.14, )",
"Penumbra.String": "[1.0.4, )"
}
},