Merge branch 'master' into mtrl-improvements

This commit is contained in:
Exter-N 2024-11-20 20:10:33 +01:00
commit ea44835321
34 changed files with 392 additions and 107 deletions

View file

@ -20,7 +20,7 @@ jobs:
run: dotnet restore
- name: Download Dalamud
run: |
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
- name: Build
run: |

@ -1 +1 @@
Subproject commit 3e6b085749741f35dd6732c33d0720c6a51ebb97
Subproject commit 95b8d177883b03f804d77434f45e9de97fdb9adf

@ -1 +1 @@
Subproject commit 27cef2c1b8ef8ce9b73bc658e03f543b5c7ef29d
Subproject commit eaa8a31db7a483a112931c9a676f4ea2ff45520e

@ -1 +1 @@
Subproject commit bd52d080b72d67263dc47068e461f17c93bdc779
Subproject commit dd83f97299ac33cfacb1064bde4f4d1f6a260936

View file

@ -8,7 +8,10 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F89C9EAE-25C8-43BE-8108-5921E5A93502}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
.github\workflows\build.yml = .github\workflows\build.yml
.github\workflows\release.yml = .github\workflows\release.yml
repo.json = repo.json
.github\workflows\test_release.yml = .github\workflows\test_release.yml
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Penumbra.GameData", "Penumbra.GameData\Penumbra.GameData.csproj", "{EE551E87-FDB3-4612-B500-DC870C07C605}"

View file

@ -48,8 +48,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
// Handle generic NPC
var npcIdentifier = _actors.CreateIndividualUnchecked(IdentifierType.Npc, ByteString.Empty,
ushort.MaxValue,
identifier.Kind, identifier.DataId);
ushort.MaxValue, identifier.Kind, identifier.DataId);
if (npcIdentifier.IsValid && _individuals.TryGetValue(npcIdentifier, out collection))
return true;
@ -58,8 +57,7 @@ public sealed partial class IndividualCollections : IReadOnlyList<(string Displa
return false;
identifier = _actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName,
identifier.HomeWorld.Id,
ObjectKind.None, uint.MaxValue);
identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue);
return CheckWorlds(identifier, out collection);
}
case IdentifierType.Npc: return _individuals.TryGetValue(identifier, out collection);

View file

@ -33,25 +33,27 @@ public sealed unsafe class ApricotListenerSoundPlayCaller : FastHook<ApricotList
private nint Detour(nint a1, nint unused, float timeOffset)
{
// Short-circuiting and sanity checks done by game.
var playTime = a1 == nint.Zero ? -1 : *(float*)(a1 + 0x250);
var playTime = a1 == nint.Zero ? -1 : *(float*)(a1 + VolatileOffsets.ApricotListenerSoundPlayCaller.PlayTimeOffset);
if (playTime < 0)
return Task.Result.Original(a1, unused, timeOffset);
var someIntermediate = *(nint*)(a1 + 0x1F8);
var flags = someIntermediate == nint.Zero ? (ushort)0 : *(ushort*)(someIntermediate + 0x49C);
if (((flags >> 13) & 1) == 0)
var someIntermediate = *(nint*)(a1 + VolatileOffsets.ApricotListenerSoundPlayCaller.SomeIntermediate);
var flags = someIntermediate == nint.Zero
? (ushort)0
: *(ushort*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.Flags);
if (((flags >> VolatileOffsets.ApricotListenerSoundPlayCaller.BitShift) & 1) == 0)
return Task.Result.Original(a1, unused, timeOffset);
Penumbra.Log.Excessive(
$"[Apricot Listener Sound Play Caller] Invoked on 0x{a1:X} with {unused}, {timeOffset}.");
// Fetch the IInstanceListenner (sixth argument to inlined call of SoundPlay)
var apricotIInstanceListenner = *(nint*)(someIntermediate + 0x270);
var apricotIInstanceListenner = *(nint*)(someIntermediate + VolatileOffsets.ApricotListenerSoundPlayCaller.IInstanceListenner);
if (apricotIInstanceListenner == nint.Zero)
return Task.Result.Original(a1, unused, timeOffset);
// In some cases we can obtain the associated caster via vfunc 1.
var newData = ResolveData.Invalid;
var gameObject = (*(delegate* unmanaged<nint, GameObject*>**)apricotIInstanceListenner)[1](apricotIInstanceListenner);
var gameObject = (*(delegate* unmanaged<nint, GameObject*>**)apricotIInstanceListenner)[VolatileOffsets.ApricotListenerSoundPlayCaller.CasterVFunc](apricotIInstanceListenner);
if (gameObject != null)
{
newData = _collectionResolver.IdentifyCollection(gameObject, true);

View file

@ -31,7 +31,7 @@ public sealed unsafe class SomePapLoad : FastHook<SomePapLoad.Delegate>
private void Detour(nint a1, int a2, nint a3, int a4)
{
Penumbra.Log.Excessive($"[Some PAP Load] Invoked on 0x{a1:X} with {a2}, {a3}, {a4}.");
var timelinePtr = a1 + Offsets.TimeLinePtr;
var timelinePtr = a1 + VolatileOffsets.AnimationState.TimeLinePtr;
if (timelinePtr != nint.Zero)
{
var actorIdx = (int)(*(*(ulong**)timelinePtr + 1) >> 3);

View file

@ -1,7 +1,7 @@
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using OtterGui.Services;
using Penumbra.Interop.PathResolving;
using Character = FFXIVClientStructs.FFXIV.Client.Game.Character.Character;
namespace Penumbra.Interop.Hooks.Meta;
@ -13,8 +13,9 @@ public sealed unsafe class CalculateHeight : FastHook<CalculateHeight.Delegate>
public CalculateHeight(HookManager hooks, CollectionResolver collectionResolver, MetaState metaState)
{
_collectionResolver = collectionResolver;
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour, !HookOverrides.Instance.Meta.CalculateHeight);
_metaState = metaState;
Task = hooks.CreateHook<Delegate>("Calculate Height", (nint)Character.MemberFunctionPointers.CalculateHeight, Detour,
!HookOverrides.Instance.Meta.CalculateHeight);
}
public delegate ulong Delegate(Character* character);

View file

@ -25,7 +25,7 @@ public sealed unsafe class UpdateModel : FastHook<UpdateModel.Delegate>
{
// Shortcut because this is called all the time.
// Same thing is checked at the beginning of the original function.
if (*(int*)((nint)drawObject + Offsets.UpdateModelSkip) == 0)
if (*(int*)((nint)drawObject + VolatileOffsets.UpdateModel.ShortCircuit) == 0)
return;
Penumbra.Log.Excessive($"[Update Model] Invoked on {(nint)drawObject:X}.");

View file

@ -401,7 +401,6 @@ public sealed unsafe class ShaderReplacementFixer : IDisposable, IRequiredServic
private void ModelRendererUnkFuncDetour(CSModelRenderer* modelRenderer, ModelRendererStructs.UnkPayload* unkPayload, uint unk2, uint unk3,
uint unk4, uint unk5)
{
// If we don't have any on-screen instances of modded iris.shpk or others, we don't need the slow path at all.
if (!Enabled || GetTotalMaterialCountForModelRendererUnk() == 0)
{
_modelRendererUnkFuncHook.Original(modelRenderer, unkPayload, unk2, unk3, unk4, unk5);

View file

@ -15,18 +15,18 @@ public unsafe class ResourceLoader : IDisposable, IService
{
private readonly ResourceService _resources;
private readonly FileReadService _fileReadService;
private readonly TexMdlService _texMdlService;
private readonly TexMdlScdService _texMdlScdService;
private readonly PapHandler _papHandler;
private readonly Configuration _config;
private ResolveData _resolvedData = ResolveData.Invalid;
public event Action<Utf8GamePath, FullPath?, ResolveData>? PapRequested;
public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlService texMdlService, Configuration config)
public ResourceLoader(ResourceService resources, FileReadService fileReadService, TexMdlScdService texMdlScdService, Configuration config)
{
_resources = resources;
_fileReadService = fileReadService;
_texMdlService = texMdlService;
_texMdlScdService = texMdlScdService;
_config = config;
ResetResolvePath();
@ -140,7 +140,7 @@ public unsafe class ResourceLoader : IDisposable, IService
return;
}
_texMdlService.AddCrc(type, resolvedPath);
_texMdlScdService.AddCrc(type, resolvedPath);
// Replace the hash and path with the correct one for the replacement.
hash = ComputeHash(resolvedPath.Value.InternalName, parameters);
var oldPath = path;

View file

@ -10,7 +10,7 @@ using ResourceHandle = FFXIVClientStructs.FFXIV.Client.System.Resource.Handle.Re
namespace Penumbra.Interop.Hooks.ResourceLoading;
public unsafe class TexMdlService : IDisposable, IRequiredService
public unsafe class TexMdlScdService : IDisposable, IRequiredService
{
/// <summary>
/// We need to be able to obtain the requested LoD level.
@ -42,7 +42,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
private readonly LodService _lodService;
public TexMdlService(IGameInteropProvider interop)
public TexMdlScdService(IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
_lodService = new LodService(interop);
@ -52,6 +52,7 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
_loadMdlFileExternHook.Enable();
if (!HookOverrides.Instance.ResourceLoading.TexResourceHandleOnLoad)
_textureOnLoadHook.Enable();
_soundOnLoadHook.Enable();
}
/// <summary> Add CRC64 if the given file is a model or texture file and has an associated path. </summary>
@ -59,8 +60,9 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
{
_ = type switch
{
ResourceType.Mdl when path.HasValue => _customMdlCrc.Add(path.Value.Crc64),
ResourceType.Tex when path.HasValue => _customTexCrc.Add(path.Value.Crc64),
ResourceType.Mdl when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Mdl),
ResourceType.Tex when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Tex),
ResourceType.Scd when path.HasValue => _customFileCrc.TryAdd(path.Value.Crc64, ResourceType.Scd),
_ => false,
};
}
@ -70,15 +72,16 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
_checkFileStateHook.Dispose();
_loadMdlFileExternHook.Dispose();
_textureOnLoadHook.Dispose();
_soundOnLoadHook.Dispose();
}
/// <summary>
/// We need to keep a list of all CRC64 hash values of our replaced Mdl and Tex files,
/// i.e. CRC32 of filename in the lower bytes, CRC32 of parent path in the upper bytes.
/// </summary>
private readonly HashSet<ulong> _customMdlCrc = [];
private readonly HashSet<ulong> _customTexCrc = [];
private readonly Dictionary<ulong, ResourceType> _customFileCrc = [];
public IReadOnlyDictionary<ulong, ResourceType> CustomCache
=> _customFileCrc;
private delegate nint CheckFileStatePrototype(nint unk1, ulong crc64);
@ -86,12 +89,34 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
private readonly Hook<CheckFileStatePrototype> _checkFileStateHook = null!;
private readonly ThreadLocal<bool> _texReturnData = new(() => default);
private readonly ThreadLocal<bool> _scdReturnData = new(() => default);
private delegate void UpdateCategoryDelegate(TextureResourceHandle* resourceHandle);
[Signature(Sigs.TexHandleUpdateCategory)]
private readonly UpdateCategoryDelegate _updateCategory = null!;
private delegate byte SoundOnLoadDelegate(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk);
[Signature("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC 30 8B 79 ?? 48 8B DA 8B D7")]
private readonly delegate* unmanaged<ResourceHandle*, SeFileDescriptor*, byte, byte> _loadScdFileLocal = null!;
[Signature("40 56 57 41 54 48 81 EC 90 00 00 00 80 3A 0B 45 0F B6 E0 48 8B F2", DetourName = nameof(OnScdLoadDetour))]
private readonly Hook<SoundOnLoadDelegate> _soundOnLoadHook = null!;
private byte OnScdLoadDetour(ResourceHandle* handle, SeFileDescriptor* descriptor, byte unk)
{
var ret = _soundOnLoadHook.Original(handle, descriptor, unk);
if (!_scdReturnData.Value)
return ret;
// Function failed on a replaced scd, call local.
_scdReturnData.Value = false;
ret = _loadScdFileLocal(handle, descriptor, unk);
_updateCategory((TextureResourceHandle*)handle);
return ret;
}
/// <summary>
/// The function that checks a files CRC64 to determine whether it is 'protected'.
/// We use it to check against our stored CRC64s and if it corresponds, we return the custom flag for models.
@ -100,14 +125,17 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
/// </summary>
private nint CheckFileStateDetour(nint ptr, ulong crc64)
{
if (_customMdlCrc.Contains(crc64))
return CustomFileFlag;
if (_customTexCrc.Contains(crc64))
{
_texReturnData.Value = true;
return nint.Zero;
}
if (_customFileCrc.TryGetValue(crc64, out var type))
switch (type)
{
case ResourceType.Mdl: return CustomFileFlag;
case ResourceType.Tex:
_texReturnData.Value = true;
return nint.Zero;
case ResourceType.Scd:
_scdReturnData.Value = true;
return nint.Zero;
}
var ret = _checkFileStateHook.Original(ptr, crc64);
Penumbra.Log.Excessive($"[CheckFileState] Called on 0x{ptr:X} with CRC {crc64:X16}, returned 0x{ret:X}.");
@ -128,10 +156,10 @@ public unsafe class TexMdlService : IDisposable, IRequiredService
private delegate byte TexResourceHandleOnLoadPrototype(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2);
[Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnLoadDetour))]
[Signature(Sigs.TexHandleOnLoad, DetourName = nameof(OnTexLoadDetour))]
private readonly Hook<TexResourceHandleOnLoadPrototype> _textureOnLoadHook = null!;
private byte OnLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2)
private byte OnTexLoadDetour(TextureResourceHandle* handle, SeFileDescriptor* descriptor, byte unk2)
{
var ret = _textureOnLoadHook.Original(handle, descriptor, unk2);
if (!_texReturnData.Value)

View file

@ -36,7 +36,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveBnmbPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveSklbPathHook;
private readonly Hook<PerSlotResolveDelegate> _resolveSkpPathHook;
private readonly Hook<TmbResolveDelegate> _resolveTmbPathHook;
@ -54,11 +56,10 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
@ -83,7 +84,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMdlPathHook.Enable();
_resolveMtrlPathHook.Enable();
_resolvePapPathHook.Enable();
_resolveKdbPathHook.Enable();
_resolvePhybPathHook.Enable();
_resolveBnmbPathHook.Enable();
_resolveSklbPathHook.Enable();
_resolveSkpPathHook.Enable();
_resolveTmbPathHook.Enable();
@ -101,7 +104,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMdlPathHook.Disable();
_resolveMtrlPathHook.Disable();
_resolvePapPathHook.Disable();
_resolveKdbPathHook.Disable();
_resolvePhybPathHook.Disable();
_resolveBnmbPathHook.Disable();
_resolveSklbPathHook.Disable();
_resolveSkpPathHook.Disable();
_resolveTmbPathHook.Disable();
@ -119,7 +124,9 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
_resolveMdlPathHook.Dispose();
_resolveMtrlPathHook.Dispose();
_resolvePapPathHook.Dispose();
_resolveKdbPathHook.Dispose();
_resolvePhybPathHook.Dispose();
_resolveBnmbPathHook.Dispose();
_resolveSklbPathHook.Dispose();
_resolveSkpPathHook.Dispose();
_resolveTmbPathHook.Dispose();
@ -149,9 +156,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
private nint ResolveKdb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
=> ResolvePath(drawObject, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
private nint ResolvePhyb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
=> ResolvePath(drawObject, _resolvePhybPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
private nint ResolveBnmb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
=> ResolvePath(drawObject, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
private nint ResolveSklb(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
=> ResolvePath(drawObject, _resolveSklbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
@ -188,6 +201,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
return ret;
}
private nint ResolveKdbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection, _resolveKdbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolvePhybHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
@ -197,6 +219,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
return ret;
}
private nint ResolveBnmbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);
_parent.MetaState.EstCollection.Push(collection);
var ret = ResolvePath(collection, _resolveBnmbPathHook.Original(drawObject, pathBuffer, pathBufferSize, partialSkeletonIndex));
_parent.MetaState.EstCollection.Pop();
return ret;
}
private nint ResolveSklbHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint partialSkeletonIndex)
{
var collection = _parent.CollectionResolver.IdentifyCollection((DrawObject*)drawObject, true);

View file

@ -1,6 +1,7 @@
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using OtterGui;
using OtterGui.Services;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
@ -62,10 +63,11 @@ public sealed unsafe class CollectionResolver(
try
{
if (useCache && cache.TryGetValue(gameObject, out var data))
// Login screen reuses the same actors and can not be cached.
if (LoginScreen(gameObject, out var data))
return data;
if (LoginScreen(gameObject, out data))
if (useCache && cache.TryGetValue(gameObject, out data))
return data;
if (Aesthetician(gameObject, out data))
@ -116,16 +118,17 @@ public sealed unsafe class CollectionResolver(
return true;
}
var notYetReady = false;
var lobby = AgentLobby.Instance();
if (lobby != null)
var notYetReady = false;
var lobby = AgentLobby.Instance();
var characterList = CharaSelectCharacterList.Instance();
if (lobby != null && characterList != null)
{
var span = lobby->LobbyData.CharaSelectEntries.AsSpan();
// The lobby uses the first 8 cutscene actors.
var idx = gameObject->ObjectIndex - ObjectIndex.CutsceneStart.Index;
if (idx >= 0 && idx < span.Length && span[idx].Value != null)
if (characterList->CharacterMapping.FindFirst(m => m.ClientObjectIndex == idx, out var mapping)
&& lobby->LobbyData.CharaSelectEntries.FindFirst(e => e.Value->ContentId == mapping.ContentId, out var charaEntry))
{
var item = span[idx].Value;
var item = charaEntry.Value;
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
Penumbra.Log.Verbose(
$"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}.");
@ -141,7 +144,7 @@ public sealed unsafe class CollectionResolver(
var collection = collectionManager.Active.ByType(CollectionType.Yourself)
?? CollectionByAttributes(gameObject, ref notYetReady)
?? collectionManager.Active.Default;
ret = notYetReady ? collection.ToResolveData(gameObject) : cache.Set(collection, ActorIdentifier.Invalid, gameObject);
ret = collection.ToResolveData(gameObject);
return true;
}
@ -196,10 +199,24 @@ public sealed unsafe class CollectionResolver(
/// <summary> Check both temporary and permanent character collections. Temporary first. </summary>
private ModCollection? CollectionByIdentifier(ActorIdentifier identifier)
=> tempCollections.Collections.TryGetCollection(identifier, out var collection)
|| collectionManager.Active.Individuals.TryGetCollection(identifier, out collection)
? collection
: null;
{
if (tempCollections.Collections.TryGetCollection(identifier, out var collection))
return collection;
// Always inherit ownership for temporary collections.
if (identifier.Type is IdentifierType.Owned)
{
var playerIdentifier = actors.CreateIndividualUnchecked(IdentifierType.Player, identifier.PlayerName,
identifier.HomeWorld.Id, ObjectKind.None, uint.MaxValue);
if (tempCollections.Collections.TryGetCollection(playerIdentifier, out collection))
return collection;
}
if (collectionManager.Active.Individuals.TryGetCollection(identifier, out collection))
return collection;
return null;
}
/// <summary> Check for the Yourself collection. </summary>
private ModCollection? CheckYourself(ActorIdentifier identifier, Actor actor)
@ -223,7 +240,7 @@ public sealed unsafe class CollectionResolver(
}
// Only handle human models.
if (!IsModelHuman((uint)actor.AsCharacter->CharacterData.ModelCharaId))
if (!IsModelHuman((uint)actor.AsCharacter->ModelContainer.ModelCharaId))
return null;
if (actor.Customize->Data[0] == 0)

View file

@ -52,6 +52,10 @@ public class PathResolver : IDisposable, IService
if (resourceType is ResourceType.Lvb or ResourceType.Lgb or ResourceType.Sgb)
return (null, ResolveData.Invalid);
// Prevent .atch loading to prevent crashes on outdated .atch files. TODO: handle atch modding differently.
if (resourceType is ResourceType.Atch)
return (null, ResolveData.Invalid);
return category switch
{
// Only Interface collection.

View file

@ -36,17 +36,16 @@ internal partial record ResolveContext
private Utf8GamePath ResolveEquipmentModelPath()
{
var path = IsEquipmentSlot(SlotIndex)
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), Slot.ToSlot())
? GamePaths.Equipment.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot())
: GamePaths.Accessory.Mdl.Path(Equipment.Set, ResolveModelRaceCode(), SlotIndex.ToEquipSlot());
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
}
private GenderRace ResolveModelRaceCode()
=> ResolveEqdpRaceCode(Slot.ToSlot(), Equipment.Set);
=> ResolveEqdpRaceCode(SlotIndex, Equipment.Set);
private unsafe GenderRace ResolveEqdpRaceCode(EquipSlot slot, PrimaryId primaryId)
private unsafe GenderRace ResolveEqdpRaceCode(uint slotIndex, PrimaryId primaryId)
{
var slotIndex = slot.ToIndex();
if (!IsEquipmentOrAccessorySlot(slotIndex) || ModelType != ModelType.Human)
return GenderRace.MidlanderMale;
@ -61,6 +60,7 @@ internal partial record ResolveContext
var metaCache = Global.Collection.MetaCache;
var entry = metaCache?.GetEqdpEntry(characterRaceCode, accessory, primaryId)
?? ExpandedEqdpFile.GetDefault(Global.MetaFileManager, characterRaceCode, accessory, primaryId);
var slot = slotIndex.ToEquipSlot();
if (entry.ToBits(slot).Item2)
return characterRaceCode;
@ -272,7 +272,7 @@ internal partial record ResolveContext
{
var human = (Human*)CharacterBase;
var equipment = ((CharacterArmor*)&human->Head)[slot.ToIndex()];
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot, equipment.Set), type, equipment.Set);
return ResolveHumanExtraSkeletonData(ResolveEqdpRaceCode(slot.ToIndex(), equipment.Set), type, equipment.Set);
}
private (GenderRace RaceCode, string Slot, PrimaryId Set) ResolveHumanExtraSkeletonData(GenderRace raceCode, EstType type,

View file

@ -68,7 +68,7 @@ public class ResourceTree
Unsafe.AsPointer(ref character->DrawData.EquipmentModelIds[0]), 10),
_ => [],
};
ModelId = character->CharacterData.ModelCharaId;
ModelId = character->ModelContainer.ModelCharaId;
CustomizeData = character->DrawData.CustomizeData;
RaceCode = human != null ? (GenderRace)human->RaceSexId : GenderRace.Unknown;

View file

@ -43,7 +43,7 @@ public unsafe class FontReloader : IService
return;
_atkModule = &atkModule->AtkModule;
_reloadFontsFunc = ((delegate* unmanaged<AtkModule*, bool, bool, void>*)_atkModule->VirtualTable)[Offsets.ReloadFontsVfunc];
_reloadFontsFunc = ((delegate* unmanaged<AtkModule*, bool, bool, void>*)_atkModule->VirtualTable)[VolatileOffsets.FontReloader.ReloadFontsVFunc];
});
}
}

View file

@ -38,10 +38,10 @@ public unsafe partial class RedrawService : IService
// VFuncs that disable and enable draw, used only for GPose actors.
private static void DisableDraw(IGameObject actor)
=> ((delegate* unmanaged<nint, void >**)actor.Address)[0][Offsets.DisableDrawVfunc](actor.Address);
=> ((delegate* unmanaged<nint, void >**)actor.Address)[0][VolatileOffsets.RedrawService.DisableDrawVFunc](actor.Address);
private static void EnableDraw(IGameObject actor)
=> ((delegate* unmanaged<nint, void >**)actor.Address)[0][Offsets.EnableDrawVfunc](actor.Address);
=> ((delegate* unmanaged<nint, void >**)actor.Address)[0][VolatileOffsets.RedrawService.EnableDrawVFunc](actor.Address);
// Check whether we currently are in GPose.
// Also clear the name list.

View file

@ -0,0 +1,35 @@
namespace Penumbra.Interop;
public static class VolatileOffsets
{
public static class ApricotListenerSoundPlayCaller
{
public const int PlayTimeOffset = 0x254;
public const int SomeIntermediate = 0x1F8;
public const int Flags = 0x4A4;
public const int IInstanceListenner = 0x270;
public const int BitShift = 13;
public const int CasterVFunc = 1;
}
public static class AnimationState
{
public const int TimeLinePtr = 0x50;
}
public static class UpdateModel
{
public const int ShortCircuit = 0xA2C;
}
public static class FontReloader
{
public const int ReloadFontsVFunc = 43;
}
public static class RedrawService
{
public const int EnableDrawVFunc = 12;
public const int DisableDrawVFunc = 13;
}
}

View file

@ -79,6 +79,17 @@ public class MetaDictionary
_globalEqp.Clear();
}
public void ClearForDefault()
{
Count = _globalEqp.Count;
_imc.Clear();
_eqp.Clear();
_eqdp.Clear();
_est.Clear();
_rsp.Clear();
_gmp.Clear();
}
public bool Equals(MetaDictionary other)
=> Count == other.Count
&& _imc.SetEquals(other._imc)

View file

@ -69,7 +69,8 @@ public class ModMetaEditor(
public static bool DeleteDefaultValues(MetaFileManager metaFileManager, MetaDictionary dict)
{
var clone = dict.Clone();
dict.Clear();
dict.ClearForDefault();
var count = 0;
foreach (var (key, value) in clone.Imc)
{

View file

@ -138,7 +138,7 @@ public sealed class ImcModGroupEditor(CommunicatorService communicator, SaveServ
protected override bool MoveOption(ImcModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);

View file

@ -90,7 +90,7 @@ public class ModGroupEditor(
{
var mod = group.Mod;
var idxFrom = group.GetIndex();
if (!mod.Groups.Move(idxFrom, groupIdxTo))
if (!mod.Groups.Move(ref idxFrom, ref groupIdxTo))
return;
saveService.SaveAllOptionGroups(mod, false, config.ReplaceNonAsciiOnImport);

View file

@ -75,7 +75,7 @@ public sealed class MultiModGroupEditor(CommunicatorService communicator, SaveSe
protected override bool MoveOption(MultiModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveBit(optionIdxFrom, optionIdxTo);

View file

@ -48,7 +48,7 @@ public sealed class SingleModGroupEditor(CommunicatorService communicator, SaveS
protected override bool MoveOption(SingleModGroup group, int optionIdxFrom, int optionIdxTo)
{
if (!group.OptionData.Move(optionIdxFrom, optionIdxTo))
if (!group.OptionData.Move(ref optionIdxFrom, ref optionIdxTo))
return false;
group.DefaultSettings = group.DefaultSettings.MoveSingle(optionIdxFrom, optionIdxTo);

View file

@ -1,6 +1,5 @@
using Dalamud.Plugin;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using OtterGui;
using OtterGui.Log;
using OtterGui.Services;
@ -20,6 +19,7 @@ using OtterGui.Tasks;
using Penumbra.UI;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
using Dalamud.Plugin.Services;
using Lumina.Excel.Sheets;
using Penumbra.GameData.Data;
using Penumbra.Interop.Hooks;
using Penumbra.Interop.Hooks.ResourceLoading;
@ -111,7 +111,7 @@ public class Penumbra : IDalamudPlugin
private void SetupApi()
{
_services.GetService<IpcProviders>();
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>()!;
var itemSheet = _services.GetService<IDataManager>().GetExcelSheet<Item>();
_communicatorService.ChangedItemHover.Subscribe(it =>
{
if (it is IdentifiedItem { Item.Id.IsItem: true })

View file

@ -8,7 +8,7 @@
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"Tags": [ "modding" ],
"DalamudApiLevel": 10,
"DalamudApiLevel": 11,
"LoadPriority": 69420,
"LoadState": 2,
"LoadSync": true,

View file

@ -4,7 +4,7 @@ using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
using Lumina.Excel.Sheets;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.Mods.Manager;
@ -16,7 +16,7 @@ namespace Penumbra.Services;
public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INotificationManager notificationManager)
: OtterGui.Classes.MessageService(log, builder, chat, notificationManager), IService
{
public void LinkItem(Item item)
public void LinkItem(in Item item)
{
// @formatter:off
var payloadList = new List<Payload>
@ -29,7 +29,7 @@ public class MessageService(Logger log, IUiBuilder builder, IChatGui chat, INoti
new TextPayload($"{(char)SeIconChar.LinkMarker}"),
new UIForegroundPayload(0),
new UIGlowPayload(0),
new TextPayload(item.Name),
new TextPayload(item.Name.ExtractText()),
new RawPayload([0x02, 0x27, 0x07, 0xCF, 0x01, 0x01, 0x01, 0xFF, 0x01, 0x03]),
new RawPayload([0x02, 0x13, 0x02, 0xEC, 0x03]),
};

View file

@ -0,0 +1,56 @@
using ImGuiNET;
using OtterGui;
using OtterGui.Text;
using Penumbra.GameData.Files;
namespace Penumbra.UI.Tabs.Debug;
public static class AtchDrawer
{
public static void Draw(AtchFile file)
{
using (ImUtf8.Group())
{
ImUtf8.Text("Entries: "u8);
ImUtf8.Text("States: "u8);
}
ImGui.SameLine();
using (ImUtf8.Group())
{
ImUtf8.Text($"{file.Entries.Count}");
if (file.Entries.Count == 0)
{
ImUtf8.Text("0"u8);
return;
}
ImUtf8.Text($"{file.Entries[0].States.Count}");
}
foreach (var (entry, index) in file.Entries.WithIndex())
{
using var id = ImUtf8.PushId(index);
using var tree = ImUtf8.TreeNode($"{index:D3}: {entry.Name.Span}");
if (tree)
{
ImUtf8.TreeNode(entry.Accessory ? "Accessory"u8 : "Weapon"u8, ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
foreach (var (state, i) in entry.States.WithIndex())
{
id.Push(i);
using var t = ImUtf8.TreeNode(state.Bone.Span);
if (t)
{
ImUtf8.TreeNode($"Scale: {state.Scale}", ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
ImUtf8.TreeNode($"Offset: {state.Offset.X} | {state.Offset.Y} | {state.Offset.Z}",
ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
ImUtf8.TreeNode($"Rotation: {state.Rotation.X} | {state.Rotation.Y} | {state.Rotation.Z}",
ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
id.Pop();
}
}
}
}
}

View file

@ -42,6 +42,7 @@ using Penumbra.Api.IpcTester;
using Penumbra.Interop.Hooks.PostProcessing;
using Penumbra.Interop.Hooks.ResourceLoading;
using Penumbra.GameData.Files.StainMapStructs;
using Penumbra.String.Classes;
using Penumbra.UI.AdvancedWindow.Materials;
namespace Penumbra.UI.Tabs.Debug;
@ -54,6 +55,9 @@ public class Diagnostics(ServiceManager provider) : IUiService
return;
using var table = ImRaii.Table("##data", 4, ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var type in typeof(ActorManager).Assembly.GetTypes()
.Where(t => t is { IsAbstract: false, IsInterface: false } && t.IsAssignableTo(typeof(IAsyncDataContainer))))
{
@ -95,13 +99,15 @@ public class DebugTab : Window, ITab, IUiService
private readonly Diagnostics _diagnostics;
private readonly ObjectManager _objects;
private readonly IClientState _clientState;
private readonly IDataManager _dataManager;
private readonly IpcTester _ipcTester;
private readonly CrashHandlerPanel _crashHandlerPanel;
private readonly TexHeaderDrawer _texHeaderDrawer;
private readonly HookOverrideDrawer _hookOverrides;
private readonly TexMdlScdService _texMdlScdService;
public DebugTab(PerformanceTracker performance, Configuration config, CollectionManager collectionManager, ObjectManager objects,
IClientState clientState,
IClientState clientState, IDataManager dataManager,
ValidityChecker validityChecker, ModManager modManager, HttpApi httpApi, ActorManager actors, StainService stains,
CharacterUtility characterUtility, ResidentResourceManager residentResources,
ResourceManagerService resourceManager, CollectionResolver collectionResolver,
@ -109,7 +115,7 @@ public class DebugTab : Window, ITab, IUiService
CutsceneService cutsceneService, ModImportManager modImporter, ImportPopup importPopup, FrameworkManager framework,
TextureManager textureManager, ShaderReplacementFixer shaderReplacementFixer, RedrawService redraws, DictEmote emotes,
Diagnostics diagnostics, IpcTester ipcTester, CrashHandlerPanel crashHandlerPanel, TexHeaderDrawer texHeaderDrawer,
HookOverrideDrawer hookOverrides)
HookOverrideDrawer hookOverrides, TexMdlScdService texMdlScdService)
: base("Penumbra Debug Window", ImGuiWindowFlags.NoCollapse)
{
IsOpen = true;
@ -147,8 +153,10 @@ public class DebugTab : Window, ITab, IUiService
_crashHandlerPanel = crashHandlerPanel;
_texHeaderDrawer = texHeaderDrawer;
_hookOverrides = hookOverrides;
_texMdlScdService = texMdlScdService;
_objects = objects;
_clientState = clientState;
_dataManager = dataManager;
}
public ReadOnlySpan<byte> Label
@ -180,6 +188,7 @@ public class DebugTab : Window, ITab, IUiService
DrawDebugCharacterUtility();
DrawShaderReplacementFixer();
DrawData();
DrawCrcCache();
DrawResourceProblems();
_hookOverrides.Draw();
DrawPlayerModelInfo();
@ -188,7 +197,7 @@ public class DebugTab : Window, ITab, IUiService
}
private void DrawCollectionCaches()
private unsafe void DrawCollectionCaches()
{
if (!ImGui.CollapsingHeader(
$"Collections ({_collectionManager.Caches.Count}/{_collectionManager.Storage.Count - 1} Caches)###Collections"))
@ -199,25 +208,35 @@ public class DebugTab : Window, ITab, IUiService
if (collection.HasCache)
{
using var color = PushColor(ImGuiCol.Text, ColorId.FolderExpanded.Value());
using var node = TreeNode($"{collection.AnonymizedName} (Change Counter {collection.ChangeCounter})");
using var node = TreeNode($"{collection.Name} (Change Counter {collection.ChangeCounter})###{collection.Name}");
if (!node)
continue;
color.Pop();
foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name))
using (var resourceNode = ImUtf8.TreeNode("Custom Resources"u8))
{
using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name);
using var node2 = TreeNode(mod.Name.Text);
if (!node2)
continue;
foreach (var path in paths)
TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
foreach (var manip in manips)
TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
if (resourceNode)
foreach (var (path, resource) in collection._cache!.CustomResources)
ImUtf8.TreeNode($"{path} -> 0x{(ulong)resource.ResourceHandle:X}",
ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
using var modNode = ImUtf8.TreeNode("Enabled Mods"u8);
if (modNode)
foreach (var (mod, paths, manips) in collection._cache!.ModData.Data.OrderBy(t => t.Item1.Name))
{
using var id = mod is TemporaryMod t ? PushId(t.Priority.Value) : PushId(((Mod)mod).ModPath.Name);
using var node2 = TreeNode(mod.Name.Text);
if (!node2)
continue;
foreach (var path in paths)
TreeNode(path.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
foreach (var manip in manips)
TreeNode(manip.ToString(), ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf).Dispose();
}
}
else
{
@ -659,11 +678,36 @@ public class DebugTab : Window, ITab, IUiService
DrawEmotes();
DrawStainTemplates();
DrawAtch();
}
private string _emoteSearchFile = string.Empty;
private string _emoteSearchName = string.Empty;
private AtchFile? _atchFile;
private void DrawAtch()
{
try
{
_atchFile ??= new AtchFile(_dataManager.GetFile("chara/xls/attachOffset/c0101.atch")!.Data);
}
catch
{
// ignored
}
if (_atchFile == null)
return;
using var mainTree = ImUtf8.TreeNode("Atch File C0101"u8);
if (!mainTree)
return;
AtchDrawer.Draw(_atchFile);
}
private void DrawEmotes()
{
using var mainTree = TreeNode("Emotes");
@ -1018,6 +1062,40 @@ public class DebugTab : Window, ITab, IUiService
DrawDebugResidentResources();
}
private string _crcInput = string.Empty;
private FullPath _crcPath = FullPath.Empty;
private unsafe void DrawCrcCache()
{
var header = ImUtf8.CollapsingHeader("CRC Cache"u8);
if (!header)
return;
if (ImUtf8.InputText("##crcInput"u8, ref _crcInput, "Input path for CRC..."u8))
_crcPath = new FullPath(_crcInput);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImUtf8.Text($" CRC32: {_crcPath.InternalName.CiCrc32:X8}");
ImUtf8.Text($"CI CRC32: {_crcPath.InternalName.Crc32:X8}");
ImUtf8.Text($" CRC64: {_crcPath.Crc64:X16}");
using var table = ImUtf8.Table("table"u8, 2);
if (!table)
return;
ImUtf8.TableSetupColumn("Hash"u8, ImGuiTableColumnFlags.WidthFixed, 18 * UiBuilder.MonoFont.GetCharAdvance('0'));
ImUtf8.TableSetupColumn("Type"u8, ImGuiTableColumnFlags.WidthFixed, 5 * UiBuilder.MonoFont.GetCharAdvance('0'));
ImGui.TableHeadersRow();
foreach (var (hash, type) in _texMdlScdService.CustomCache)
{
ImGui.TableNextColumn();
ImUtf8.Text($"{hash:X16}");
ImGui.TableNextColumn();
ImUtf8.Text($"{type}");
}
}
/// <summary> Draw resources with unusual reference count. </summary>
private unsafe void DrawResourceProblems()
{

View file

@ -34,28 +34,49 @@ public class HookOverrideDrawer(IDalamudPluginInterface pluginInterface) : IUiSe
Penumbra.Log.Error($"Could not delete hook override file at {path}:\n{ex}");
}
bool? allVisible = null;
ImGui.SameLine();
if (ImUtf8.Button("Disable All Visible Hooks"u8))
allVisible = true;
ImGui.SameLine();
if (ImUtf8.Button("Enable All VisibleHooks"u8))
allVisible = false;
bool? all = null;
ImGui.SameLine();
if (ImUtf8.Button("Disable All Hooks"u8))
if (ImUtf8.Button("Disable All Hooks"))
all = true;
ImGui.SameLine();
if (ImUtf8.Button("Enable All Hooks"u8))
if (ImUtf8.Button("Enable All Hooks"))
all = false;
foreach (var propertyField in typeof(HookOverrides).GetFields().Where(f => f is { IsStatic: false, FieldType.IsValueType: true }))
{
using var tree = ImUtf8.TreeNode(propertyField.Name);
if (!tree)
continue;
var property = propertyField.GetValue(_overrides);
foreach (var valueField in propertyField.FieldType.GetFields())
{
var value = valueField.GetValue(property) as bool? ?? false;
if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || all.HasValue)
if (all.HasValue)
{
valueField.SetValue(property, all ?? value);
propertyField.SetValue(_overrides, property);
var property = propertyField.GetValue(_overrides);
foreach (var valueField in propertyField.FieldType.GetFields())
{
valueField.SetValue(property, all.Value);
propertyField.SetValue(_overrides, property);
}
}
}
else
{
allVisible ??= all;
var property = propertyField.GetValue(_overrides);
foreach (var valueField in propertyField.FieldType.GetFields())
{
var value = valueField.GetValue(property) as bool? ?? false;
if (ImUtf8.Checkbox($"Disable {valueField.Name}", ref value) || allVisible.HasValue)
{
valueField.SetValue(property, allVisible ?? value);
propertyField.SetValue(_overrides, property);
}
}
}
}

View file

@ -6,11 +6,11 @@
"Description": "Runtime mod loader and manager.",
"InternalName": "Penumbra",
"AssemblyVersion": "1.3.0.0",
"TestingAssemblyVersion": "1.3.0.0",
"TestingAssemblyVersion": "1.3.0.4",
"RepoUrl": "https://github.com/xivdev/Penumbra",
"ApplicableVersion": "any",
"DalamudApiLevel": 10,
"TestingDalamudApiLevel": 10,
"TestingDalamudApiLevel": 11,
"IsHide": "False",
"IsTestingExclusive": "False",
"DownloadCount": 0,
@ -19,7 +19,7 @@
"LoadRequiredState": 2,
"LoadSync": true,
"DownloadLinkInstall": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip",
"DownloadLinkTesting": "https://github.com/xivdev/Penumbra/releases/download/testing_1.3.0.4/Penumbra.zip",
"DownloadLinkUpdate": "https://github.com/xivdev/Penumbra/releases/download/1.3.0.0/Penumbra.zip",
"IconUrl": "https://raw.githubusercontent.com/xivdev/Penumbra/master/images/icon.png"
}