mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Compare commits
119 commits
testing_1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb5b01290 | ||
|
|
5dd74297c6 | ||
|
|
ce54aa5d25 | ||
|
|
c4b6e4e00b | ||
|
|
912c183fc6 | ||
|
|
5bf901d0c4 | ||
|
|
cbedc878b9 | ||
|
|
c8cf560fc1 | ||
|
|
f05cb52da2 | ||
|
|
7ed81a9823 | ||
|
|
60aa23efcd | ||
|
|
ebbe957c95 | ||
|
|
300e0e6d84 | ||
|
|
049baa4fe4 | ||
|
|
0881dfde8a | ||
|
|
23c0506cb8 | ||
|
|
699745413e | ||
|
|
eb53f04c6b | ||
|
|
c6b596169c | ||
|
|
a0c3e820b0 | ||
|
|
a59689ebfe | ||
|
|
e9f67a009b | ||
|
|
97c8d82b33 | ||
|
|
c3b00ff426 | ||
|
|
6348c4a639 | ||
|
|
5a6e06df3b | ||
|
|
f5f6dd3246 | ||
|
|
4e788f7c2b | ||
|
|
ad1659caf6 | ||
|
|
18a6ce2a5f | ||
|
|
e68e821b2a | ||
|
|
96764b34ca | ||
|
|
2cf60b78cd | ||
|
|
d59be1e660 | ||
|
|
5503bb32e0 | ||
|
|
f3ec4b2e08 | ||
|
|
b3379a9710 | ||
|
|
8c25ef4b47 | ||
|
|
912020cc3f | ||
|
|
be8987a451 | ||
|
|
f7cf5503bb | ||
|
|
a04a5a071c | ||
|
|
71e24c13c7 | ||
|
|
c0120f81af | ||
|
|
da47c19aeb | ||
|
|
e16800f216 | ||
|
|
79a4fc5904 | ||
|
|
bf90725dd2 | ||
|
|
a14347f73a | ||
|
|
1e07e43498 | ||
|
|
f51f8a7bf8 | ||
|
|
1fca78fa71 | ||
|
|
c8b6325a87 | ||
|
|
6079103505 | ||
|
|
d302a17f1f | ||
|
|
0d64384059 | ||
|
|
10894d451a | ||
|
|
fb34238530 | ||
|
|
8043e6fb6b | ||
|
|
e3b7f72893 | ||
|
|
b7f326e29c | ||
|
|
dad01e1af8 | ||
|
|
10b71930a1 | ||
|
|
23257f94a4 | ||
|
|
83a36ed4cb | ||
|
|
8304579d29 | ||
|
|
24cbc6c5e1 | ||
|
|
41edc23820 | ||
|
|
aa920b5e9b | ||
|
|
87ace28bcf | ||
|
|
5917f5fad1 | ||
|
|
f69c264317 | ||
|
|
a7246b9d98 | ||
|
|
9aff388e21 | ||
|
|
091aff1b8a | ||
|
|
9f8185f67b | ||
|
|
b112d75a27 | ||
|
|
7af81a6c18 | ||
|
|
12a218bb2b | ||
|
|
f6bac93db7 | ||
|
|
155d3d49aa | ||
|
|
9aae2210a2 | ||
|
|
3785a629ce | ||
|
|
02af52671f | ||
|
|
391c9d727e | ||
|
|
ff2b2be953 | ||
|
|
6242b30f93 | ||
|
|
11cd08a9de | ||
|
|
46cfbcb115 | ||
|
|
66543cc671 | ||
|
|
13283c9690 | ||
|
|
bedfb22466 | ||
|
|
13df8b2248 | ||
|
|
93406e4d4e | ||
|
|
8140d08557 | ||
|
|
2b36f39848 | ||
|
|
a69811800d | ||
|
|
3f18ad50de | ||
|
|
6689e326ee | ||
|
|
bdcab22a55 | ||
|
|
f5f4fe7259 | ||
|
|
898963fea5 | ||
|
|
8527bfa29c | ||
|
|
baca3cdec2 | ||
|
|
dc93eba34c | ||
|
|
012052daa0 | ||
|
|
a9546e31ee | ||
|
|
a4a6283e7b | ||
|
|
00c02fd16e | ||
|
|
140d150bb4 | ||
|
|
49a6d935f3 | ||
|
|
692beacc2e | ||
|
|
a953febfba | ||
|
|
c0aa2e36ea | ||
|
|
278bf43b29 | ||
|
|
a97d9e4953 | ||
|
|
30e3cd1f38 | ||
|
|
62e9dc164d | ||
|
|
9fc572ba0c |
186 changed files with 3314 additions and 545 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873
|
||||
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f
|
||||
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Dalamud.NET.Sdk/12.0.2">
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit 10fdb025436f7ea9f1f5e97635c19eee0578de7b
|
||||
Subproject commit d889f9ef918514a46049725052d378b441915b00
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5
|
||||
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793
|
||||
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Api.Api;
|
||||
|
||||
public static class IdentityChecker
|
||||
{
|
||||
public static bool Check(string identity)
|
||||
=> true;
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Compression;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
{
|
||||
switch (type)
|
||||
{
|
||||
case ModPathChangeType.Deleted when oldDirectory != null:
|
||||
ModDeleted?.Invoke(oldDirectory.Name);
|
||||
break;
|
||||
case ModPathChangeType.Added when newDirectory != null:
|
||||
ModAdded?.Invoke(newDirectory.Name);
|
||||
break;
|
||||
case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
|
||||
case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
|
||||
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
||||
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
||||
break;
|
||||
|
|
@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
{
|
||||
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||
}
|
||||
|
||||
public Dictionary<string, string> GetModList()
|
||||
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
|
||||
|
|
@ -109,6 +108,18 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
public event Action<string>? ModAdded;
|
||||
public event Action<string, string>? ModMoved;
|
||||
|
||||
public event Action<JObject, ushort, string>? CreatingPcp
|
||||
{
|
||||
add => _communicator.PcpCreation.Subscribe(value!, PcpCreation.Priority.ModsApi);
|
||||
remove => _communicator.PcpCreation.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public event Action<JObject, string, Guid>? ParsingPcp
|
||||
{
|
||||
add => _communicator.PcpParsing.Subscribe(value!, PcpParsing.Priority.ModsApi);
|
||||
remove => _communicator.PcpParsing.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
|
||||
{
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class PenumbraApi(
|
|||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||
{
|
||||
public const int BreakingVersion = 5;
|
||||
public const int FeatureVersion = 10;
|
||||
public const int FeatureVersion = 13;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -2,11 +2,14 @@ using Dalamud.Game.ClientState.Objects.Types;
|
|||
using Dalamud.Plugin.Services;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
|
||||
{
|
||||
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||
{
|
||||
|
|
@ -28,9 +31,27 @@ public class RedrawApi(RedrawService redrawService, IFramework framework) : IPen
|
|||
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
|
||||
}
|
||||
|
||||
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
|
||||
{
|
||||
|
||||
if (!collections.Storage.ById(collectionId, out var collection))
|
||||
collection = ModCollection.Empty;
|
||||
framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var actor in objects.Objects)
|
||||
{
|
||||
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
|
||||
if (collection == modCollection)
|
||||
{
|
||||
redrawService.RedrawObject(actor.ObjectIndex, setting);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||
{
|
||||
add => redrawService.GameObjectRedrawn += value;
|
||||
remove => redrawService.GameObjectRedrawn -= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@ public class TemporaryApi(
|
|||
ApiHelpers apiHelpers,
|
||||
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
||||
{
|
||||
public Guid CreateTemporaryCollection(string name)
|
||||
=> tempCollections.CreateTemporaryCollection(name);
|
||||
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
|
||||
{
|
||||
if (!IdentityChecker.Check(identity))
|
||||
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
|
||||
|
||||
var collection = tempCollections.CreateTemporaryCollection(name);
|
||||
if (collection == Guid.Empty)
|
||||
return (PenumbraApiEc.UnknownError, collection);
|
||||
return (PenumbraApiEc.Success, collection);
|
||||
}
|
||||
|
||||
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
|
||||
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using EmbedIO.WebApi;
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
|
|
@ -13,12 +14,15 @@ public class HttpApi : IDisposable, IApiService
|
|||
private partial class Controller : WebApiController
|
||||
{
|
||||
// @formatter:off
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
||||
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +68,12 @@ public class HttpApi : IDisposable, IApiService
|
|||
|
||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
public partial string GetModDirectory()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
|
||||
return api.PluginState.GetModDirectory();
|
||||
}
|
||||
|
||||
public partial object? GetMods()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
||||
|
|
@ -116,6 +126,38 @@ public class HttpApi : IDisposable, IApiService
|
|||
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public async partial Task FocusMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
|
||||
if (data.Path.Length != 0)
|
||||
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task SetModSettings()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
|
||||
if (data.Inherit.HasValue)
|
||||
{
|
||||
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
|
||||
if (data.Inherit.Value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.State.HasValue)
|
||||
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
|
||||
if (data.Priority.HasValue)
|
||||
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
|
||||
foreach (var (group, settings) in data.Settings ?? [])
|
||||
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private record ModReloadData(string Path, string Name)
|
||||
{
|
||||
public ModReloadData()
|
||||
|
|
@ -123,6 +165,13 @@ public class HttpApi : IDisposable, IApiService
|
|||
{ }
|
||||
}
|
||||
|
||||
private record ModFocusData(string Path, string Name)
|
||||
{
|
||||
public ModFocusData()
|
||||
: this(string.Empty, string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record ModInstallData(string Path)
|
||||
{
|
||||
public ModInstallData()
|
||||
|
|
@ -136,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
|
|||
: this(string.Empty, RedrawType.Redraw, -1)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record SetModSettingsData(
|
||||
Guid? CollectionId,
|
||||
string ModPath,
|
||||
string ModName,
|
||||
bool? Inherit,
|
||||
bool? State,
|
||||
int? Priority,
|
||||
Dictionary<string, List<string>>? Settings)
|
||||
{
|
||||
public SetModSettingsData()
|
||||
: this(null, string.Empty, string.Empty, null, null, null, null)
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ModMoved.Provider(pi, api.Mods),
|
||||
IpcSubscribers.CreatingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.ParsingPcp.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
|
||||
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
|
||||
|
|
@ -86,6 +88,7 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
||||
|
||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
}).ToArray();
|
||||
ImGui.OpenPopup("Changed Item List");
|
||||
}
|
||||
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
|
||||
if (ImGui.Button("Redraw##ObjectCollection"))
|
||||
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
|
||||
|
||||
}
|
||||
|
||||
private void DrawChangedItemPopup()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game.ClientState.Objects.Enums;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
|
|
@ -38,6 +38,7 @@ public class TemporaryIpcTester(
|
|||
private string _tempGamePath = "test/game/path.mtrl";
|
||||
private string _tempFilePath = "test/success.mtrl";
|
||||
private string _tempManipulation = string.Empty;
|
||||
private string _identity = string.Empty;
|
||||
private PenumbraApiEc _lastTempError;
|
||||
private int _tempActorIndex;
|
||||
private bool _forceOverwrite;
|
||||
|
|
@ -48,6 +49,7 @@ public class TemporaryIpcTester(
|
|||
if (!_)
|
||||
return;
|
||||
|
||||
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
|
||||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||
|
|
@ -73,7 +75,7 @@ public class TemporaryIpcTester(
|
|||
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
|
||||
if (ImGui.Button("Create##Collection"))
|
||||
{
|
||||
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
|
||||
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
|
||||
if (_tempGuid == null)
|
||||
{
|
||||
_tempGuid = LastCreatedCollectionId;
|
||||
|
|
@ -282,7 +284,7 @@ public class TemporaryIpcTester(
|
|||
foreach (var mod in list)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Name);
|
||||
ImGui.TextUnformatted(mod.Name.Text);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Priority.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace Penumbra;
|
||||
|
|
|
|||
|
|
@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
|
|||
return;
|
||||
|
||||
var collection = _resolver.PlayerCollection();
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
if (collection.Identity.Id == Guid.Empty)
|
||||
{
|
||||
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api;
|
|||
using Penumbra.Api.Api;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
|
|||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="PcpService.OnModPathChange"/>
|
||||
PcpService = int.MinValue,
|
||||
|
||||
/// <seealso cref="ModsApi.OnModPathChange"/>
|
||||
ApiMods = int.MinValue,
|
||||
ApiMods = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
||||
ApiModSettings = int.MinValue,
|
||||
ApiModSettings = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||
EphemeralConfig = -500,
|
||||
|
|
|
|||
21
Penumbra/Communication/PcpCreation.cs
Normal file
21
Penumbra/Communication/PcpCreation.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the character.json file for a .pcp file is written.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the JObject that gets written to file. </item>
|
||||
/// <item>Parameter is the object index of the game object this is written for. </item>
|
||||
/// <item>Parameter is the full path to the directory being set up for the PCP creation. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PcpCreation() : EventWrapper<JObject, ushort, string, PcpCreation.Priority>(nameof(PcpCreation))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.Api.ModsApi"/>
|
||||
ModsApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
21
Penumbra/Communication/PcpParsing.cs
Normal file
21
Penumbra/Communication/PcpParsing.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the character.json file for a .pcp file is parsed and applied.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is parsed JObject that contains the data. </item>
|
||||
/// <item>Parameter is the identifier of the created mod. </item>
|
||||
/// <item>Parameter is the GUID of the created collection. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PcpParsing() : EventWrapper<JObject, string, Guid, PcpParsing.Priority>(nameof(PcpParsing))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.Api.ModsApi"/>
|
||||
ModsApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
|||
|
||||
namespace Penumbra;
|
||||
|
||||
public record PcpSettings
|
||||
{
|
||||
public bool CreateCollection { get; set; } = true;
|
||||
public bool AssignCollection { get; set; } = true;
|
||||
public bool AllowIpc { get; set; } = true;
|
||||
public bool DisableHandling { get; set; } = false;
|
||||
public string FolderName { get; set; } = "PCP";
|
||||
}
|
||||
|
||||
[Serializable]
|
||||
public class Configuration : IPluginConfiguration, ISavable, IService
|
||||
{
|
||||
|
|
@ -44,6 +53,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
|
||||
public string ModDirectory { get; set; } = string.Empty;
|
||||
public string ExportDirectory { get; set; } = string.Empty;
|
||||
public string WatchDirectory { get; set; } = string.Empty;
|
||||
|
||||
public bool? UseCrashHandler { get; set; } = null;
|
||||
public bool OpenWindowAtStart { get; set; } = false;
|
||||
|
|
@ -67,10 +77,13 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public bool HideRedrawBar { get; set; } = false;
|
||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||
public bool DefaultTemporaryMode { get; set; } = false;
|
||||
public bool EnableDirectoryWatch { get; set; } = false;
|
||||
public bool EnableAutomaticModImport { get; set; } = false;
|
||||
public bool EnableCustomShapes { get; set; } = true;
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||
public PcpSettings PcpSettings = new();
|
||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||
|
||||
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ namespace Penumbra;
|
|||
|
||||
public class DebugConfiguration
|
||||
{
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool UseSkinMaterialProcessing = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.UI.AdvancedWindow.Materials;
|
||||
using SharpGLTF.Materials;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Advanced;
|
||||
|
|
@ -140,13 +141,13 @@ public class MaterialExporter
|
|||
|
||||
// Lerp between table row values to fetch final pixel values for each subtexture.
|
||||
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
|
||||
baseColorSpan[x].FromVector4(new Vector4(lerpedDiffuse, 1));
|
||||
baseColorSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedDiffuse), 1));
|
||||
|
||||
var lerpedSpecularColor = Vector3.Lerp((Vector3)prevRow.SpecularColor, (Vector3)nextRow.SpecularColor, rowBlend);
|
||||
specularSpan[x].FromVector4(new Vector4(lerpedSpecularColor, 1));
|
||||
specularSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedSpecularColor), 1));
|
||||
|
||||
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
|
||||
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
|
||||
emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -360,11 +360,11 @@ public class MeshExporter
|
|||
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
|
||||
// TODO: While this assumption is safe, it would be sensible to actually check.
|
||||
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
|
||||
|
||||
|
||||
return new VertexPositionNormalTangent(
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
|
||||
bitangent
|
||||
bitangent.SanitizeTangent()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -390,23 +390,30 @@ public class MeshExporter
|
|||
}
|
||||
}
|
||||
|
||||
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
|
||||
var nColors = colours?.Count ?? 0;
|
||||
|
||||
var materialUsages = (
|
||||
uvCount,
|
||||
usages.ContainsKey(MdlFile.VertexUsage.Color)
|
||||
nColors
|
||||
);
|
||||
|
||||
return materialUsages switch
|
||||
{
|
||||
(3, true) => typeof(VertexTexture3ColorFfxiv),
|
||||
(3, false) => typeof(VertexTexture3),
|
||||
(2, true) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, false) => typeof(VertexTexture2),
|
||||
(1, true) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, false) => typeof(VertexTexture1),
|
||||
(0, true) => typeof(VertexColorFfxiv),
|
||||
(0, false) => typeof(VertexEmpty),
|
||||
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
|
||||
(3, 1) => typeof(VertexTexture3ColorFfxiv),
|
||||
(3, 0) => typeof(VertexTexture3),
|
||||
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
|
||||
(2, 1) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, 0) => typeof(VertexTexture2),
|
||||
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
|
||||
(1, 1) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, 0) => typeof(VertexTexture1),
|
||||
(0, 2) => typeof(VertexColor2Ffxiv),
|
||||
(0, 1) => typeof(VertexColorFfxiv),
|
||||
(0, 0) => typeof(VertexEmpty),
|
||||
|
||||
_ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."),
|
||||
_ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -419,6 +426,12 @@ public class MeshExporter
|
|||
if (_materialType == typeof(VertexColorFfxiv))
|
||||
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
|
||||
|
||||
if (_materialType == typeof(VertexColor2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture1))
|
||||
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
|
||||
|
||||
|
|
@ -428,6 +441,16 @@ public class MeshExporter
|
|||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
|
||||
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexTexture1Color2Ffxiv(
|
||||
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
// XIV packs two UVs into a single vec4 attribute.
|
||||
|
||||
if (_materialType == typeof(VertexTexture2))
|
||||
|
|
@ -448,6 +471,20 @@ public class MeshExporter
|
|||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
|
||||
{
|
||||
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture2Color2Ffxiv(
|
||||
new Vector2(uv.X, uv.Y),
|
||||
new Vector2(uv.Z, uv.W),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3))
|
||||
{
|
||||
// Not 100% sure about this
|
||||
|
|
@ -472,6 +509,21 @@ public class MeshExporter
|
|||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
|
||||
{
|
||||
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
|
||||
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture3Color2Ffxiv(
|
||||
new Vector2(uv0.X, uv0.Y),
|
||||
new Vector2(uv0.Z, uv0.W),
|
||||
new Vector2(uv1.X, uv1.Y),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
throw _notifier.Exception($"Unknown material type {_skinningType}");
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +589,17 @@ public class MeshExporter
|
|||
|
||||
return list[0];
|
||||
}
|
||||
|
||||
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
|
||||
{
|
||||
var list = attributes[usage];
|
||||
if (list.Count != 2)
|
||||
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
|
||||
|
||||
return (list[0], list[1]);
|
||||
}
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private static Vector2 ToVector2(object data)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 0;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{ }
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{ }
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
|
|
@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) :
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 1;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex >= 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
|
|
@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 2;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex >= 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
|
||||
: IVertexCustom
|
||||
{
|
||||
|
|
@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
|
|||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
|
||||
: IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector2 TexCoord2 = texCoord2;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 3;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
TexCoord2 += delta.TexCoord2Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
2 => TexCoord2,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex == 2)
|
||||
TexCoord2 = coord;
|
||||
if (setIndex >= 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ public class VertexAttribute
|
|||
|
||||
var normals = normalAccessor.AsVector3Array();
|
||||
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
||||
? accessor.AsVector4Array()
|
||||
? accessor.AsVector4Array().ToArray()
|
||||
: CalculateTangents(accessors, indices, normals, notifier);
|
||||
|
||||
if (tangents == null)
|
||||
|
|
|
|||
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
namespace Penumbra.Import.Models;
|
||||
|
||||
public static class ModelExtensions
|
||||
{
|
||||
// https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158
|
||||
private const float UnitLengthThresholdVec3 = 0.00674f;
|
||||
private const float UnitLengthThresholdVec4 = 0.00769f;
|
||||
|
||||
internal static bool _IsFinite(this float value)
|
||||
{
|
||||
return float.IsFinite(value);
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this Vector2 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite();
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this Vector3 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite();
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this in Vector4 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
|
||||
}
|
||||
|
||||
internal static Boolean IsNormalized(this Vector3 normal)
|
||||
{
|
||||
if (!normal._IsFinite()) return false;
|
||||
|
||||
return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3;
|
||||
}
|
||||
|
||||
internal static void ValidateNormal(this Vector3 normal, string msg)
|
||||
{
|
||||
if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
|
||||
|
||||
if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length.");
|
||||
}
|
||||
|
||||
internal static void ValidateTangent(this Vector4 tangent, string msg)
|
||||
{
|
||||
if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg);
|
||||
|
||||
new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg);
|
||||
}
|
||||
|
||||
internal static Vector3 SanitizeNormal(this Vector3 normal)
|
||||
{
|
||||
if (normal == Vector3.Zero) return Vector3.UnitX;
|
||||
return normal.IsNormalized() ? normal : Vector3.Normalize(normal);
|
||||
}
|
||||
|
||||
internal static bool IsValidTangent(this Vector4 tangent)
|
||||
{
|
||||
if (tangent.W != 1 && tangent.W != -1) return false;
|
||||
|
||||
return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized();
|
||||
}
|
||||
|
||||
internal static Vector4 SanitizeTangent(this Vector4 tangent)
|
||||
{
|
||||
var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal();
|
||||
var s = float.IsNaN(tangent.W) ? 1 : tangent.W;
|
||||
return new Vector4(n, s > 0 ? 1 : -1);
|
||||
}
|
||||
}
|
||||
|
|
@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable
|
|||
// Puts out warnings if extension does not correspond to data.
|
||||
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
||||
{
|
||||
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".zip" or ".7z" or ".rar")
|
||||
if (modPackFile.Extension.ToLowerInvariant() is ".pmp" or ".pcp" or ".zip" or ".7z" or ".rar")
|
||||
return HandleRegularArchive(modPackFile);
|
||||
|
||||
using var zfs = modPackFile.OpenRead();
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Import.Structs;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@ public static class TexFileParser
|
|||
|
||||
if (width == minSize && height == minSize)
|
||||
{
|
||||
newSize = totalSize;
|
||||
++i;
|
||||
newSize = totalSize + requiredSize;
|
||||
if (header.MipCount != i)
|
||||
{
|
||||
Penumbra.Log.Debug($"-- Reduced number of Mip Maps from {header.MipCount} to {i} due to minimum size constraints.");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Files;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
|
|
@ -20,7 +20,7 @@ public static class TextureDrawer
|
|||
{
|
||||
size = texture.TextureWrap.Size.Contain(size);
|
||||
|
||||
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
|
||||
ImGui.Image(texture.TextureWrap.Handle, size);
|
||||
DrawData(texture);
|
||||
}
|
||||
else if (texture.LoadError != null)
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
// See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition.
|
||||
if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)
|
||||
{
|
||||
var device = uiBuilder.Device;
|
||||
var device = new Device(uiBuilder.DeviceHandle);
|
||||
var dxgiDevice = device.QueryInterface<DxgiDevice>();
|
||||
|
||||
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);
|
||||
|
|
|
|||
47
Penumbra/Interop/CloudApi.cs
Normal file
47
Penumbra/Interop/CloudApi.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
namespace Penumbra.Interop;
|
||||
|
||||
public static unsafe partial class CloudApi
|
||||
{
|
||||
private const int CfSyncRootInfoBasic = 0;
|
||||
|
||||
/// <summary> Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. </summary>
|
||||
/// <remarks> Can be expensive. Callers should cache the result when relevant. </remarks>
|
||||
public static bool IsCloudSynced(string path)
|
||||
{
|
||||
var buffer = stackalloc long[1];
|
||||
int hr;
|
||||
uint length;
|
||||
try
|
||||
{
|
||||
hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length);
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException");
|
||||
return false;
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException");
|
||||
return false;
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}");
|
||||
if (hr < 0)
|
||||
return false;
|
||||
|
||||
if (length != sizeof(long))
|
||||
{
|
||||
Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength,
|
||||
out uint returnedLength);
|
||||
}
|
||||
|
|
@ -59,9 +59,6 @@ public class GameState : IService
|
|||
|
||||
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
||||
|
||||
public ResolveData SoundData
|
||||
=> _animationLoadData.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public ResolveData SetSoundData(ResolveData data)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
|
|||
{
|
||||
_collectionResolver = collectionResolver;
|
||||
_metaState = metaState;
|
||||
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.ChangeCustomize, Detour, !HookOverrides.Instance.Meta.ChangeCustomize);
|
||||
Task = hooks.CreateHook<Delegate>("Change Customize", Sigs.UpdateDrawData, Detour, !HookOverrides.Instance.Meta.ChangeCustomize);
|
||||
}
|
||||
|
||||
public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment);
|
||||
|
|
|
|||
|
|
@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Cha
|
|||
public bool Finished
|
||||
=> _task.IsCompletedSuccessfully;
|
||||
|
||||
private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g);
|
||||
private delegate void Delegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h);
|
||||
|
||||
private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g)
|
||||
private void Detour(DrawDataContainer* drawData, uint slot, ulong weapon, byte d, byte e, byte f, byte g, byte h)
|
||||
{
|
||||
var gameObject = drawData->OwnerObject;
|
||||
Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}.");
|
||||
Penumbra.Log.Verbose($"[{Name}] Triggered with drawData: 0x{(nint)drawData:X}, {slot}, {weapon}, {d}, {e}, {f}, {g}, {h}.");
|
||||
Invoke(drawData, gameObject, (CharacterWeapon*)(&weapon));
|
||||
_task.Result.Original(drawData, slot, weapon, d, e, f, g);
|
||||
_task.Result.Original(drawData, slot, weapon, d, e, f, g, h);
|
||||
_postEvent.Invoke(drawData, gameObject);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi
|
|||
if (!_framework.IsInFrameworkUpdateThread)
|
||||
Penumbra.Log.Warning(
|
||||
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
|
||||
|
||||
|
||||
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
|
|
@ -85,7 +86,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8);
|
||||
|
||||
private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
||||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9);
|
||||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8,
|
||||
uint unk9);
|
||||
|
||||
[Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))]
|
||||
private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!;
|
||||
|
|
@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
unk9);
|
||||
}
|
||||
|
||||
var original = gamePath;
|
||||
if (gamePath.IsEmpty)
|
||||
{
|
||||
Penumbra.Log.Error($"[ResourceService] Empty resource path requested with category {*categoryId}, type {*resourceType}, hash {*resourceHash}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var original = gamePath;
|
||||
ResourceHandle* returnValue = null;
|
||||
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync,
|
||||
ref returnValue);
|
||||
if (returnValue != null)
|
||||
return returnValue;
|
||||
|
||||
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9);
|
||||
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8,
|
||||
unk9);
|
||||
}
|
||||
|
||||
/// <summary> Call the original GetResource function. </summary>
|
||||
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original,
|
||||
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path,
|
||||
Utf8GamePath original,
|
||||
GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0)
|
||||
{
|
||||
var previous = _currentGetResourcePath.Value;
|
||||
|
|
@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
resourceParameters, unk8, unk9)
|
||||
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
|
||||
resourceParameters, unk, unk8, unk9);
|
||||
} finally
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentGetResourcePath.Value = previous;
|
||||
}
|
||||
|
|
@ -163,7 +174,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
/// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
|
||||
/// <param name="previousState">The previous state of the resource.</param>
|
||||
/// <param name="returnValue">The return value to use.</param>
|
||||
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
|
||||
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal,
|
||||
(byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
|
||||
|
|
@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread)
|
||||
{
|
||||
var previousState = (handle->UnkState, handle->LoadState);
|
||||
var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty;
|
||||
var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty;
|
||||
ResourceStateUpdating?.Invoke(handle, syncOriginal);
|
||||
var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread);
|
||||
ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Penumbra.Collections;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Processing;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Resources;
|
||||
|
|
@ -35,6 +36,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
|
||||
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveSkinMtrlPathHook;
|
||||
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
|
||||
|
|
@ -52,22 +54,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
{
|
||||
_parent = parent;
|
||||
// @formatter:off
|
||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
||||
_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);
|
||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
||||
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
||||
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
||||
_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);
|
||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
||||
_resolveSkinMtrlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl);
|
||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
||||
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
||||
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
||||
|
||||
|
||||
// @formatter:on
|
||||
|
|
@ -83,6 +86,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Enable();
|
||||
_resolveMdlPathHook.Enable();
|
||||
_resolveMtrlPathHook.Enable();
|
||||
_resolveSkinMtrlPathHook.Enable();
|
||||
_resolvePapPathHook.Enable();
|
||||
_resolveKdbPathHook.Enable();
|
||||
_resolvePhybPathHook.Enable();
|
||||
|
|
@ -103,6 +107,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Disable();
|
||||
_resolveMdlPathHook.Disable();
|
||||
_resolveMtrlPathHook.Disable();
|
||||
_resolveSkinMtrlPathHook.Disable();
|
||||
_resolvePapPathHook.Disable();
|
||||
_resolveKdbPathHook.Disable();
|
||||
_resolvePhybPathHook.Disable();
|
||||
|
|
@ -123,6 +128,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Dispose();
|
||||
_resolveMdlPathHook.Dispose();
|
||||
_resolveMtrlPathHook.Dispose();
|
||||
_resolveSkinMtrlPathHook.Dispose();
|
||||
_resolvePapPathHook.Dispose();
|
||||
_resolveKdbPathHook.Dispose();
|
||||
_resolvePhybPathHook.Dispose();
|
||||
|
|
@ -153,6 +159,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName)
|
||||
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName));
|
||||
|
||||
private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
|
||||
{
|
||||
var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex);
|
||||
if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer)
|
||||
SkinMtrlPathEarlyProcessing.Process(new Span<byte>((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex);
|
||||
|
||||
return ResolvePath(drawObject, finalPathBuffer);
|
||||
}
|
||||
|
||||
private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
|
||||
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
|
||||
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
|
|||
if (mtrlHandle == null)
|
||||
continue;
|
||||
|
||||
PathDataHandler.Split(mtrlHandle->ResourceHandle.FileName.AsSpan(), out var path, out _);
|
||||
PathDataHandler.Split(mtrlHandle->FileName.AsSpan(), out var path, out _);
|
||||
var fileName = CiByteString.FromSpanUnsafe(path, true);
|
||||
if (fileName == needle)
|
||||
result.Add(new MaterialInfo(index, type, i, j));
|
||||
|
|
|
|||
|
|
@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
|
|||
{
|
||||
var item = charaEntry.Value;
|
||||
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
|
||||
Penumbra.Log.Verbose(
|
||||
Penumbra.Log.Excessive(
|
||||
$"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")}.");
|
||||
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
|||
return false;
|
||||
|
||||
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
||||
_objects.InvokeRequiredUpdates();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Interop;
|
||||
|
||||
public static partial class ProcessThreadApi
|
||||
{
|
||||
[LibraryImport("kernel32.dll")]
|
||||
public static partial uint GetCurrentThreadId();
|
||||
}
|
||||
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Interop.Processing;
|
||||
|
||||
public sealed class PbdFilePostProcessor : IFilePostProcessor
|
||||
{
|
||||
private readonly IFileAllocator _allocator;
|
||||
private byte[] _epbdData;
|
||||
private unsafe delegate* unmanaged<ResourceHandle*, void> _loadEpbdData;
|
||||
|
||||
public ResourceType Type
|
||||
=> ResourceType.Pbd;
|
||||
|
||||
public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner)
|
||||
{
|
||||
_allocator = allocator;
|
||||
_epbdData = SetEpbdData(dataManager);
|
||||
_loadEpbdData = (delegate* unmanaged<ResourceHandle*, void>)scanner.ScanText(Sigs.LoadEpbdData);
|
||||
}
|
||||
|
||||
public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan<byte> additionalData)
|
||||
{
|
||||
if (_epbdData.Length is 0)
|
||||
return;
|
||||
|
||||
if (resource->LoadState is not LoadState.Success)
|
||||
{
|
||||
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState}).");
|
||||
return;
|
||||
}
|
||||
|
||||
var (data, length) = resource->GetData();
|
||||
if (length is 0 || data == nint.Zero)
|
||||
{
|
||||
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data.");
|
||||
return;
|
||||
}
|
||||
|
||||
var span = new ReadOnlySpan<byte>((void*)data, (int)resource->FileSize);
|
||||
var reader = new PackReader(span);
|
||||
if (reader.HasData)
|
||||
{
|
||||
Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data.");
|
||||
return;
|
||||
}
|
||||
|
||||
var newData = AppendData(span);
|
||||
fixed (byte* ptr = newData)
|
||||
{
|
||||
// Set the appended data and the actual file size, then re-load the EPBD data via game function call.
|
||||
if (resource->SetData((nint)ptr, newData.Length))
|
||||
{
|
||||
resource->FileSize = (uint)newData.Length;
|
||||
resource->CsHandle.FileSize2 = (uint)newData.Length;
|
||||
resource->CsHandle.FileSize3 = (uint)newData.Length;
|
||||
_loadEpbdData(resource);
|
||||
// Free original data.
|
||||
_allocator.Release((void*)data, length);
|
||||
Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Warning(
|
||||
$"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Combine the given data with the default PBD data using the game's file allocator. </summary>
|
||||
private unsafe ReadOnlySpan<byte> AppendData(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// offset has to be set, otherwise not called.
|
||||
var newLength = data.Length + _epbdData.Length;
|
||||
var memory = _allocator.Allocate(newLength);
|
||||
var span = new Span<byte>(memory, newLength);
|
||||
data.CopyTo(span);
|
||||
_epbdData.CopyTo(span[data.Length..]);
|
||||
return span;
|
||||
}
|
||||
|
||||
/// <summary> Fetch the default EPBD data from the .pbd file of the game's installation. </summary>
|
||||
private static byte[] SetEpbdData(IDataManager dataManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = dataManager.GetFile(GamePaths.Pbd.Path);
|
||||
if (file is null || file.Data.Length is 0)
|
||||
{
|
||||
Penumbra.Log.Warning("Default PBD file has no data.");
|
||||
return [];
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> span = file.Data;
|
||||
var reader = new PackReader(span);
|
||||
if (!reader.HasData)
|
||||
{
|
||||
Penumbra.Log.Warning("Default PBD file has no EPBD section.");
|
||||
return [];
|
||||
}
|
||||
|
||||
var offset = span.Length - (int)reader.PackLength;
|
||||
var ret = span[offset..];
|
||||
Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}.");
|
||||
return ret.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
|
||||
namespace Penumbra.Interop.Processing;
|
||||
|
||||
public static unsafe class SkinMtrlPathEarlyProcessing
|
||||
{
|
||||
public static void Process(Span<byte> path, CharacterBase* character, uint slotIndex)
|
||||
{
|
||||
var end = path.IndexOf(MaterialExtension());
|
||||
if (end < 0)
|
||||
return;
|
||||
|
||||
var suffixPos = path[..end].LastIndexOf((byte)'_');
|
||||
if (suffixPos < 0)
|
||||
return;
|
||||
|
||||
var handle = GetModelResourceHandle(character, slotIndex);
|
||||
if (handle == null)
|
||||
return;
|
||||
|
||||
var skinSuffix = GetSkinSuffix(handle);
|
||||
if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7)
|
||||
return;
|
||||
|
||||
++suffixPos;
|
||||
skinSuffix.CopyTo(path[suffixPos..]);
|
||||
suffixPos += skinSuffix.Length;
|
||||
MaterialExtension().CopyTo(path[suffixPos..]);
|
||||
return;
|
||||
|
||||
static ReadOnlySpan<byte> MaterialExtension()
|
||||
=> ".mtrl\0"u8;
|
||||
}
|
||||
|
||||
private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex)
|
||||
{
|
||||
if (character is null)
|
||||
return null;
|
||||
|
||||
if (character->PerSlotStagingArea is not null)
|
||||
{
|
||||
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
|
||||
if (handle != null)
|
||||
return handle;
|
||||
}
|
||||
|
||||
var model = character->Models[slotIndex];
|
||||
return model is null ? null : model->ModelResourceHandle;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> GetSkinSuffix(ModelResourceHandle* handle)
|
||||
{
|
||||
foreach (var (attribute, _) in handle->Attributes)
|
||||
{
|
||||
var attributeSpan = attribute.AsSpan();
|
||||
if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_')
|
||||
return attributeSpan[12..];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -338,6 +338,34 @@ internal partial record ResolveContext
|
|||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex)
|
||||
{
|
||||
// Correctness and Safety:
|
||||
// Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons.
|
||||
// Additionally, it can dereference null pointers for human equipment skeletons.
|
||||
return ModelType switch
|
||||
{
|
||||
ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex),
|
||||
_ => ResolveKineDriverModulePathNative(partialSkeletonIndex),
|
||||
};
|
||||
}
|
||||
|
||||
private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex)
|
||||
{
|
||||
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||
if (set.Id is 0)
|
||||
return Utf8GamePath.Empty;
|
||||
|
||||
var path = GamePaths.Kdb.Customization(raceCode, slot, set);
|
||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex)
|
||||
{
|
||||
var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex);
|
||||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
|
||||
{
|
||||
var animation = ResolveImcData(imc).MaterialAnimationId;
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext(
|
|||
if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path))
|
||||
return null;
|
||||
|
||||
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
|
||||
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path);
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
|
|
@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext(
|
|||
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
|
||||
}
|
||||
|
||||
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle)
|
||||
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle,
|
||||
MaterialResourceHandle* skinMtrlHandle, ResourceHandle* mpapHandle)
|
||||
{
|
||||
if (mdl is null || mdl->ModelResourceHandle is null)
|
||||
return null;
|
||||
|
|
@ -218,6 +219,12 @@ internal unsafe partial record ResolveContext(
|
|||
}
|
||||
}
|
||||
|
||||
if (skinMtrlHandle is not null
|
||||
&& Utf8GamePath.FromByteString(CharacterBase->ResolveSkinMtrlPathAsByteString(SlotIndex), out var skinMtrlPath)
|
||||
&& CreateNodeFromMaterial(skinMtrlHandle->Material, skinMtrlPath) is
|
||||
{ } skinMaaterialNode)
|
||||
node.Children.Add(skinMaaterialNode);
|
||||
|
||||
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
|
||||
node.Children.Add(decalNode);
|
||||
|
||||
|
|
@ -238,7 +245,7 @@ internal unsafe partial record ResolveContext(
|
|||
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||
return cached;
|
||||
|
||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, (ResourceHandle*)resource, path, false);
|
||||
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value));
|
||||
if (shpkNode is not null)
|
||||
{
|
||||
|
|
@ -364,7 +371,8 @@ internal unsafe partial record ResolveContext(
|
|||
return node;
|
||||
}
|
||||
|
||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex)
|
||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle,
|
||||
uint partialSkeletonIndex)
|
||||
{
|
||||
if (sklb is null || sklb->SkeletonResourceHandle is null)
|
||||
return null;
|
||||
|
|
@ -379,6 +387,8 @@ internal unsafe partial record ResolveContext(
|
|||
node.Children.Add(skpNode);
|
||||
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
||||
node.Children.Add(phybNode);
|
||||
if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode)
|
||||
node.Children.Add(kdbNode);
|
||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||
|
||||
return node;
|
||||
|
|
@ -420,6 +430,24 @@ internal unsafe partial record ResolveContext(
|
|||
return node;
|
||||
}
|
||||
|
||||
private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex)
|
||||
{
|
||||
if (kdbHandle is null)
|
||||
return null;
|
||||
|
||||
var path = ResolveKineDriverModulePath(partialSkeletonIndex);
|
||||
|
||||
if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached))
|
||||
return cached;
|
||||
|
||||
var node = CreateNode(ResourceType.Kdb, 0, kdbHandle, path, false);
|
||||
if (Global.WithUiData)
|
||||
node.FallbackName = "KineDriver Module";
|
||||
Global.Nodes.Add((path, (nint)kdbHandle), node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||
{
|
||||
var path = gamePath.Path.Split((byte)'/');
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ public class ResourceNode : ICloneable
|
|||
|
||||
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
|
||||
public bool Protected
|
||||
=> ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd;
|
||||
=> ForceProtected
|
||||
|| Internal
|
||||
|| Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd;
|
||||
|
||||
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,12 +70,16 @@ public class ResourceTree(
|
|||
|
||||
var genericContext = globalContext.CreateContext(model);
|
||||
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
|
||||
var mpapArrayPtr = model->MaterialAnimationPacks;
|
||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
|
||||
var skinMtrlArray = modelType switch
|
||||
{
|
||||
ModelType.Human => ((Human*) model)->SlotSkinMaterials,
|
||||
_ => [],
|
||||
};
|
||||
var decalArray = modelType switch
|
||||
{
|
||||
ModelType.Human => human->SlotDecalsSpan,
|
||||
ModelType.Human => human->SlotDecals,
|
||||
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
|
||||
ModelType.Weapon => [((Weapon*)model)->Decal],
|
||||
ModelType.Monster => [((Monster*)model)->Decal],
|
||||
|
|
@ -108,7 +112,8 @@ public class ResourceTree(
|
|||
|
||||
var mdl = model->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
|
||||
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode)
|
||||
i < skinMtrlArray.Length ? skinMtrlArray[(int)i].Value : null, i < mpapArray.Length ? mpapArray[(int)i].Value : null) is
|
||||
{ } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Model #{i}";
|
||||
|
|
@ -116,9 +121,8 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
|
||||
AddSkeleton(Nodes, genericContext, model);
|
||||
AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton);
|
||||
|
||||
AddWeapons(globalContext, model);
|
||||
|
||||
|
|
@ -149,8 +153,7 @@ public class ResourceTree(
|
|||
|
||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
|
||||
var mpapArrayPtr = subObject->MaterialAnimationPacks;
|
||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
|
||||
|
||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||
|
|
@ -166,7 +169,8 @@ public class ResourceTree(
|
|||
}
|
||||
|
||||
var mdl = subObject->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode)
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, null, i < mpapArray.Length ? mpapArray[i].Value : null) is
|
||||
{ } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||
|
|
@ -174,10 +178,8 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule,
|
||||
$"Weapon #{weaponIndex}, ");
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940),
|
||||
AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, ");
|
||||
AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton,
|
||||
$"Weapon #{weaponIndex}, ");
|
||||
|
||||
++weaponIndex;
|
||||
|
|
@ -239,8 +241,11 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
|
||||
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix);
|
||||
|
||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
|
||||
string prefix = "")
|
||||
BoneKineDriverModule* kineDriver, string prefix = "")
|
||||
{
|
||||
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||
if (eidNode != null)
|
||||
|
|
@ -255,9 +260,9 @@ public class ResourceTree(
|
|||
|
||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
|
||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
|
||||
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
|
||||
var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
|
||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
|
||||
{
|
||||
if (context.Global.WithUiData)
|
||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||
|
|
|
|||
|
|
@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
|
|||
return;
|
||||
|
||||
|
||||
foreach (ref var f in currentTerritory->Furniture)
|
||||
foreach (ref var f in currentTerritory->FurnitureManager.FurnitureMemory)
|
||||
{
|
||||
var gameObject = f.Index >= 0 ? currentTerritory->HousingObjectManager.Objects[f.Index].Value : null;
|
||||
var gameObject = f.Index >= 0 ? currentTerritory->FurnitureManager.ObjectManager.ObjectArray.Objects[f.Index].Value : null;
|
||||
if (gameObject == null)
|
||||
continue;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||
using OtterGui.Services;
|
||||
using SharpDX.Direct3D;
|
||||
|
|
@ -16,7 +17,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
|||
private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
|
||||
|
||||
/// <remarks> Caching this across frames will cause a crash to desktop. </remarks>
|
||||
public nint GetImGuiHandle(Texture* texture, byte sliceIndex)
|
||||
public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex)
|
||||
{
|
||||
if (texture == null)
|
||||
throw new ArgumentNullException(nameof(texture));
|
||||
|
|
@ -25,7 +26,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
|||
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
|
||||
{
|
||||
state.Refresh();
|
||||
return (nint)state.ShaderResourceView;
|
||||
return new ImTextureID((nint)state.ShaderResourceView);
|
||||
}
|
||||
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
|
||||
var description = srv.Description;
|
||||
|
|
@ -60,7 +61,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
|||
}
|
||||
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
|
||||
_activeSlices.Add(((nint)texture, sliceIndex), state);
|
||||
return (nint)state.ShaderResourceView;
|
||||
return new ImTextureID((nint)state.ShaderResourceView);
|
||||
}
|
||||
|
||||
public void Tick()
|
||||
|
|
|
|||
|
|
@ -10,28 +10,34 @@ internal static class StructExtensions
|
|||
public static CiByteString AsByteString(in this StdString str)
|
||||
=> CiByteString.FromSpanUnsafe(str.AsSpan(), true);
|
||||
|
||||
public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveEidPath(pathBuffer));
|
||||
}
|
||||
|
||||
public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
|
||||
public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveEidPath(pathBuffer));
|
||||
}
|
||||
|
||||
public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex));
|
||||
public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
|
||||
}
|
||||
|
||||
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName)
|
||||
{
|
||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
||||
public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex));
|
||||
}
|
||||
|
||||
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName)
|
||||
{
|
||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
||||
}
|
||||
|
||||
public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||
{
|
||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveSkinMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex));
|
||||
}
|
||||
|
||||
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
|
||||
|
|
@ -40,16 +46,16 @@ internal static class StructExtensions
|
|||
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
|
||||
}
|
||||
|
||||
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex));
|
||||
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex));
|
||||
}
|
||||
|
||||
public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
||||
public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
{
|
||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
||||
}
|
||||
|
||||
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
|
|
@ -58,6 +64,12 @@ internal static class StructExtensions
|
|||
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
|
||||
}
|
||||
|
||||
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||
{
|
||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||
return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex));
|
||||
}
|
||||
|
||||
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
|
||||
=> str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
using System.Collections.Frozen;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Penumbra.Collections.Cache;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files.AtchStructs;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Util;
|
||||
using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
|
||||
|
|
@ -40,6 +43,165 @@ public class MetaDictionary
|
|||
foreach (var geqp in cache.GlobalEqp.Keys)
|
||||
Add(geqp);
|
||||
}
|
||||
|
||||
public static unsafe Wrapper Filtered(MetaCache cache, Actor actor)
|
||||
{
|
||||
if (!actor.IsCharacter)
|
||||
return new Wrapper(cache);
|
||||
|
||||
var model = actor.Model;
|
||||
if (!model.IsHuman)
|
||||
return new Wrapper(cache);
|
||||
|
||||
var headId = model.GetModelId(HumanSlot.Head);
|
||||
var bodyId = model.GetModelId(HumanSlot.Body);
|
||||
var equipIdSet = ((IEnumerable<PrimaryId>)
|
||||
[
|
||||
headId,
|
||||
bodyId,
|
||||
model.GetModelId(HumanSlot.Hands),
|
||||
model.GetModelId(HumanSlot.Legs),
|
||||
model.GetModelId(HumanSlot.Feet),
|
||||
]).ToFrozenSet();
|
||||
var earsId = model.GetModelId(HumanSlot.Ears);
|
||||
var neckId = model.GetModelId(HumanSlot.Neck);
|
||||
var wristId = model.GetModelId(HumanSlot.Wrists);
|
||||
var rFingerId = model.GetModelId(HumanSlot.RFinger);
|
||||
var lFingerId = model.GetModelId(HumanSlot.LFinger);
|
||||
|
||||
var wrapper = new Wrapper();
|
||||
// Check for all relevant primary IDs due to slot overlap.
|
||||
foreach (var (eqp, value) in cache.Eqp)
|
||||
{
|
||||
if (eqp.Slot.IsEquipment())
|
||||
{
|
||||
if (equipIdSet.Contains(eqp.SetId))
|
||||
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (eqp.Slot)
|
||||
{
|
||||
case EquipSlot.Ears when eqp.SetId == earsId:
|
||||
case EquipSlot.Neck when eqp.SetId == neckId:
|
||||
case EquipSlot.Wrists when eqp.SetId == wristId:
|
||||
case EquipSlot.RFinger when eqp.SetId == rFingerId:
|
||||
case EquipSlot.LFinger when eqp.SetId == lFingerId:
|
||||
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check also for body IDs due to body occupying head.
|
||||
foreach (var (gmp, value) in cache.Gmp)
|
||||
{
|
||||
if (gmp.SetId == headId || gmp.SetId == bodyId)
|
||||
wrapper.Gmp.Add(gmp, value.Entry);
|
||||
}
|
||||
|
||||
// Check for all races due to inheritance and all slots due to overlap.
|
||||
foreach (var (eqdp, value) in cache.Eqdp)
|
||||
{
|
||||
if (eqdp.Slot.IsEquipment())
|
||||
{
|
||||
if (equipIdSet.Contains(eqdp.SetId))
|
||||
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (eqdp.Slot)
|
||||
{
|
||||
case EquipSlot.Ears when eqdp.SetId == earsId:
|
||||
case EquipSlot.Neck when eqdp.SetId == neckId:
|
||||
case EquipSlot.Wrists when eqdp.SetId == wristId:
|
||||
case EquipSlot.RFinger when eqdp.SetId == rFingerId:
|
||||
case EquipSlot.LFinger when eqdp.SetId == lFingerId:
|
||||
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var genderRace = (GenderRace)model.AsHuman->RaceSexId;
|
||||
var hairId = model.GetModelId(HumanSlot.Hair);
|
||||
var faceId = model.GetModelId(HumanSlot.Face);
|
||||
// We do not need to care for racial inheritance for ESTs.
|
||||
foreach (var (est, value) in cache.Est)
|
||||
{
|
||||
switch (est.Slot)
|
||||
{
|
||||
case EstType.Hair when est.SetId == hairId && est.GenderRace == genderRace:
|
||||
case EstType.Face when est.SetId == faceId && est.GenderRace == genderRace:
|
||||
case EstType.Body when est.SetId == bodyId && est.GenderRace == genderRace:
|
||||
case EstType.Head when (est.SetId == headId || est.SetId == bodyId) && est.GenderRace == genderRace:
|
||||
wrapper.Est.Add(est, value.Entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var (geqp, _) in cache.GlobalEqp)
|
||||
{
|
||||
switch (geqp.Type)
|
||||
{
|
||||
case GlobalEqpType.DoNotHideEarrings when geqp.Condition != earsId:
|
||||
case GlobalEqpType.DoNotHideNecklace when geqp.Condition != neckId:
|
||||
case GlobalEqpType.DoNotHideBracelets when geqp.Condition != wristId:
|
||||
case GlobalEqpType.DoNotHideRingR when geqp.Condition != rFingerId:
|
||||
case GlobalEqpType.DoNotHideRingL when geqp.Condition != lFingerId:
|
||||
continue;
|
||||
default: wrapper.Add(geqp); break;
|
||||
}
|
||||
}
|
||||
|
||||
var (_, _, main, off) = model.GetWeapons(actor);
|
||||
foreach (var (imc, value) in cache.Imc)
|
||||
{
|
||||
switch (imc.ObjectType)
|
||||
{
|
||||
case ObjectType.Equipment when equipIdSet.Contains(imc.PrimaryId): wrapper.Imc.Add(imc, value.Entry); break;
|
||||
|
||||
case ObjectType.Weapon:
|
||||
if (imc.PrimaryId == main.Skeleton && imc.SecondaryId == main.Weapon)
|
||||
wrapper.Imc.Add(imc, value.Entry);
|
||||
else if (imc.PrimaryId == off.Skeleton && imc.SecondaryId == off.Weapon)
|
||||
wrapper.Imc.Add(imc, value.Entry);
|
||||
break;
|
||||
case ObjectType.Accessory:
|
||||
switch (imc.EquipSlot)
|
||||
{
|
||||
case EquipSlot.Ears when imc.PrimaryId == earsId:
|
||||
case EquipSlot.Neck when imc.PrimaryId == neckId:
|
||||
case EquipSlot.Wrists when imc.PrimaryId == wristId:
|
||||
case EquipSlot.RFinger when imc.PrimaryId == rFingerId:
|
||||
case EquipSlot.LFinger when imc.PrimaryId == lFingerId:
|
||||
wrapper.Imc.Add(imc, value.Entry);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var subRace = (SubRace)model.AsHuman->Customize[4];
|
||||
foreach (var (rsp, value) in cache.Rsp)
|
||||
{
|
||||
if (rsp.SubRace == subRace)
|
||||
wrapper.Rsp.Add(rsp, value.Entry);
|
||||
}
|
||||
|
||||
// Keep all atch, atr and shp.
|
||||
wrapper.Atch.EnsureCapacity(cache.Atch.Count);
|
||||
wrapper.Shp.EnsureCapacity(cache.Shp.Count);
|
||||
wrapper.Atr.EnsureCapacity(cache.Atr.Count);
|
||||
foreach (var (atch, value) in cache.Atch)
|
||||
wrapper.Atch.Add(atch, value.Entry);
|
||||
foreach (var (shp, value) in cache.Shp)
|
||||
wrapper.Shp.Add(shp, value.Entry);
|
||||
foreach (var (atr, value) in cache.Atr)
|
||||
wrapper.Atr.Add(atr, value.Entry);
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
private Wrapper? _data;
|
||||
|
|
@ -126,6 +288,7 @@ public class MetaDictionary
|
|||
{
|
||||
_data = null;
|
||||
Count = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
Count = GlobalEqp.Count + Shp.Count + Atr.Count;
|
||||
|
|
@ -933,4 +1096,24 @@ public class MetaDictionary
|
|||
_data = new Wrapper(cache);
|
||||
Count = cache.Count;
|
||||
}
|
||||
|
||||
public MetaDictionary(MetaCache? cache, Actor actor)
|
||||
{
|
||||
if (cache is null)
|
||||
return;
|
||||
|
||||
_data = Wrapper.Filtered(cache, actor);
|
||||
Count = _data.Count
|
||||
+ _data.Eqp.Count
|
||||
+ _data.Eqdp.Count
|
||||
+ _data.Est.Count
|
||||
+ _data.Gmp.Count
|
||||
+ _data.Imc.Count
|
||||
+ _data.Rsp.Count
|
||||
+ _data.Atch.Count
|
||||
+ _data.Atr.Count
|
||||
+ _data.Shp.Count;
|
||||
if (Count is 0)
|
||||
_data = null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,11 +58,72 @@ public unsafe class ShapeAttributeManager : IRequiredService, IDisposable
|
|||
_ids[(int)_modelIndex] = model.GetModelId(_modelIndex);
|
||||
CheckShapes(collection.MetaCache!.Shp);
|
||||
CheckAttributes(collection.MetaCache!.Atr);
|
||||
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
|
||||
AccessoryImcCheck(model);
|
||||
}
|
||||
|
||||
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
|
||||
}
|
||||
|
||||
private void AccessoryImcCheck(Model model)
|
||||
{
|
||||
var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex));
|
||||
|
||||
Span<byte> attr =
|
||||
[
|
||||
(byte)'a',
|
||||
(byte)'t',
|
||||
(byte)'r',
|
||||
(byte)'_',
|
||||
AccessoryByte(_modelIndex),
|
||||
(byte)'v',
|
||||
(byte)'_',
|
||||
(byte)'a',
|
||||
0,
|
||||
];
|
||||
for (var i = 1; i < 10; ++i)
|
||||
{
|
||||
var flag = (ushort)(1 << i);
|
||||
if ((imcMask & flag) is not 0)
|
||||
continue;
|
||||
|
||||
attr[^2] = (byte)('a' + i);
|
||||
|
||||
foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes)
|
||||
{
|
||||
if (!EqualAttribute(attr, attribute.Value))
|
||||
continue;
|
||||
|
||||
_model->EnabledAttributeIndexMask &= ~(1u << index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||
private static bool EqualAttribute(Span<byte> needle, byte* haystack)
|
||||
{
|
||||
foreach (var character in needle)
|
||||
{
|
||||
if (*haystack++ != character)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte AccessoryByte(HumanSlot slot)
|
||||
=> slot switch
|
||||
{
|
||||
HumanSlot.Head => (byte)'m',
|
||||
HumanSlot.Ears => (byte)'e',
|
||||
HumanSlot.Neck => (byte)'n',
|
||||
HumanSlot.Wrists => (byte)'w',
|
||||
HumanSlot.RFinger => (byte)'r',
|
||||
HumanSlot.LFinger => (byte)'r',
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
private void CheckAttributes(AtrCache attributeCache)
|
||||
{
|
||||
if (attributeCache.DisabledCount is 0)
|
||||
|
|
|
|||
|
|
@ -372,7 +372,6 @@ public class ModMerger : IDisposable, IService
|
|||
}
|
||||
else
|
||||
{
|
||||
// TODO DataContainer <> Option.
|
||||
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
|
||||
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
|
||||
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
|||
else
|
||||
{
|
||||
var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport);
|
||||
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport);
|
||||
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetDirectoryName(), config.ReplaceNonAsciiOnImport);
|
||||
containers[container] = optionDir.FullName;
|
||||
}
|
||||
}
|
||||
|
|
@ -286,7 +286,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
|||
|
||||
void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
|
||||
{
|
||||
var name = option.GetName();
|
||||
var name = option.GetDirectoryName();
|
||||
var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true);
|
||||
|
||||
newDict.Clear();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Collections.Frozen;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.UI.Classes;
|
||||
|
|
|
|||
180
Penumbra/Mods/Groups/ComplexModGroup.cs
Normal file
180
Penumbra/Mods/Groups/ComplexModGroup.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Settings;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.String.Classes;
|
||||
using Penumbra.UI.ModsTab.Groups;
|
||||
using Penumbra.Util;
|
||||
|
||||
namespace Penumbra.Mods.Groups;
|
||||
|
||||
public sealed class ComplexModGroup(Mod mod) : IModGroup
|
||||
{
|
||||
public Mod Mod { get; } = mod;
|
||||
public string Name { get; set; } = "Option";
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public string Image { get; set; } = string.Empty;
|
||||
|
||||
public GroupType Type
|
||||
=> GroupType.Complex;
|
||||
|
||||
public GroupDrawBehaviour Behaviour
|
||||
=> GroupDrawBehaviour.Complex;
|
||||
|
||||
public ModPriority Priority { get; set; }
|
||||
public int Page { get; set; }
|
||||
public Setting DefaultSettings { get; set; }
|
||||
|
||||
public readonly List<ComplexSubMod> Options = [];
|
||||
public readonly List<ComplexDataContainer> Containers = [];
|
||||
|
||||
|
||||
public FullPath? FindBestMatch(Utf8GamePath gamePath)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public IModOption? AddOption(string name, string description = "")
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
IReadOnlyList<IModOption> IModGroup.Options
|
||||
=> Options;
|
||||
|
||||
IReadOnlyList<IModDataContainer> IModGroup.DataContainers
|
||||
=> Containers;
|
||||
|
||||
public bool IsOption
|
||||
=> Options.Count > 0;
|
||||
|
||||
public int GetIndex()
|
||||
=> ModGroup.GetIndex(this);
|
||||
|
||||
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
|
||||
=> throw new NotImplementedException();
|
||||
|
||||
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
|
||||
{
|
||||
foreach (var container in Containers.Where(c => c.Association.IsEnabled(setting)))
|
||||
SubMod.AddContainerTo(container, redirections, manipulations);
|
||||
}
|
||||
|
||||
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
||||
{
|
||||
foreach (var container in Containers)
|
||||
identifier.AddChangedItems(container, changedItems);
|
||||
}
|
||||
|
||||
public Setting FixSetting(Setting setting)
|
||||
=> new(setting.Value & ((1ul << Options.Count) - 1));
|
||||
|
||||
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
|
||||
{
|
||||
ModSaveGroup.WriteJsonBase(jWriter, this);
|
||||
jWriter.WritePropertyName("Options");
|
||||
jWriter.WriteStartArray();
|
||||
foreach (var option in Options)
|
||||
{
|
||||
jWriter.WriteStartObject();
|
||||
SubMod.WriteModOption(jWriter, option);
|
||||
if (!option.Conditions.IsZero)
|
||||
{
|
||||
jWriter.WritePropertyName("ConditionMask");
|
||||
jWriter.WriteValue(option.Conditions.Mask.Value);
|
||||
jWriter.WritePropertyName("ConditionValue");
|
||||
jWriter.WriteValue(option.Conditions.Value.Value);
|
||||
}
|
||||
|
||||
if (option.Indentation > 0)
|
||||
{
|
||||
jWriter.WritePropertyName("Indentation");
|
||||
jWriter.WriteValue(option.Indentation);
|
||||
}
|
||||
|
||||
if (option.SubGroupLabel.Length > 0)
|
||||
{
|
||||
jWriter.WritePropertyName("SubGroup");
|
||||
jWriter.WriteValue(option.SubGroupLabel);
|
||||
}
|
||||
|
||||
jWriter.WriteEndObject();
|
||||
}
|
||||
|
||||
jWriter.WriteEndArray();
|
||||
|
||||
jWriter.WritePropertyName("Containers");
|
||||
jWriter.WriteStartArray();
|
||||
foreach (var container in Containers)
|
||||
{
|
||||
jWriter.WriteStartObject();
|
||||
if (container.Name.Length > 0)
|
||||
{
|
||||
jWriter.WritePropertyName("Name");
|
||||
jWriter.WriteValue(container.Name);
|
||||
}
|
||||
|
||||
if (!container.Association.IsZero)
|
||||
{
|
||||
jWriter.WritePropertyName("AssociationMask");
|
||||
jWriter.WriteValue(container.Association.Mask.Value);
|
||||
|
||||
jWriter.WritePropertyName("AssociationValue");
|
||||
jWriter.WriteValue(container.Association.Value.Value);
|
||||
}
|
||||
|
||||
SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath);
|
||||
jWriter.WriteEndObject();
|
||||
}
|
||||
|
||||
jWriter.WriteEndArray();
|
||||
}
|
||||
|
||||
public (int Redirections, int Swaps, int Manips) GetCounts()
|
||||
=> ModGroup.GetCountsBase(this);
|
||||
|
||||
public static ComplexModGroup? Load(Mod mod, JObject json)
|
||||
{
|
||||
var ret = new ComplexModGroup(mod);
|
||||
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||
return null;
|
||||
|
||||
var options = json["Options"];
|
||||
if (options != null)
|
||||
foreach (var child in options.Children())
|
||||
{
|
||||
if (ret.Options.Count == IModGroup.MaxComplexOptions)
|
||||
{
|
||||
Penumbra.Messager.NotificationMessage(
|
||||
$"Complex Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxComplexOptions} options, ignoring excessive options.",
|
||||
NotificationType.Warning);
|
||||
break;
|
||||
}
|
||||
|
||||
var subMod = new ComplexSubMod(ret, child);
|
||||
ret.Options.Add(subMod);
|
||||
}
|
||||
|
||||
// Fix up conditions: No condition on itself.
|
||||
foreach (var (option, index) in ret.Options.WithIndex())
|
||||
{
|
||||
option.Conditions = option.Conditions.Limit(ret.Options.Count);
|
||||
option.Conditions = new MaskedSetting(option.Conditions.Mask.SetBit(index, false), option.Conditions.Value);
|
||||
}
|
||||
|
||||
var containers = json["Containers"];
|
||||
if (containers != null)
|
||||
foreach (var child in containers.Children())
|
||||
{
|
||||
var container = new ComplexDataContainer(ret, child);
|
||||
container.Association = container.Association.Limit(ret.Options.Count);
|
||||
ret.Containers.Add(container);
|
||||
}
|
||||
|
||||
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
|
||||
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
|
@ -18,11 +18,13 @@ public enum GroupDrawBehaviour
|
|||
{
|
||||
SingleSelection,
|
||||
MultiSelection,
|
||||
Complex,
|
||||
}
|
||||
|
||||
public interface IModGroup
|
||||
{
|
||||
public const int MaxMultiOptions = 32;
|
||||
public const int MaxComplexOptions = MaxMultiOptions;
|
||||
public const int MaxCombiningOptions = 8;
|
||||
|
||||
public Mod Mod { get; }
|
||||
|
|
|
|||
|
|
@ -234,9 +234,56 @@ public static class EquipmentSwap
|
|||
mdl.ChildSwaps.Add(mtrl);
|
||||
}
|
||||
|
||||
FixAttributes(mdl, slotFrom, slotTo);
|
||||
|
||||
return mdl;
|
||||
}
|
||||
|
||||
private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo)
|
||||
{
|
||||
if (slotFrom == slotTo)
|
||||
return;
|
||||
|
||||
var needle = slotTo switch
|
||||
{
|
||||
EquipSlot.Head => "atr_mv_",
|
||||
EquipSlot.Ears => "atr_ev_",
|
||||
EquipSlot.Neck => "atr_nv_",
|
||||
EquipSlot.Wrists => "atr_wv_",
|
||||
EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
var replacement = slotFrom switch
|
||||
{
|
||||
EquipSlot.Head => 'm',
|
||||
EquipSlot.Ears => 'e',
|
||||
EquipSlot.Neck => 'n',
|
||||
EquipSlot.Wrists => 'w',
|
||||
EquipSlot.RFinger or EquipSlot.LFinger => 'r',
|
||||
_ => 'm',
|
||||
};
|
||||
|
||||
var attributes = swap.AsMdl()!.Attributes;
|
||||
for (var i = 0; i < attributes.Length; ++i)
|
||||
{
|
||||
if (FixAttribute(ref attributes[i], needle, replacement))
|
||||
swap.DataWasChanged = true;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool FixAttribute(ref string attribute, string from, char to)
|
||||
{
|
||||
if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j')
|
||||
return false;
|
||||
|
||||
Span<char> stack = stackalloc char[attribute.Length];
|
||||
attribute.CopyTo(stack);
|
||||
stack[4] = to;
|
||||
attribute = new string(stack);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant)
|
||||
{
|
||||
slot = i.Type.ToSlot();
|
||||
|
|
@ -399,7 +446,7 @@ public static class EquipmentSwap
|
|||
return null;
|
||||
|
||||
var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo);
|
||||
var pathTo = $"{folderTo}{fileName}";
|
||||
var pathTo = $"{folderTo}{fileName}";
|
||||
|
||||
var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo);
|
||||
var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
|||
|
||||
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
||||
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
||||
string? website)
|
||||
string? website, params string[] tags)
|
||||
{
|
||||
var mod = new Mod(directory);
|
||||
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
|
||||
|
|
@ -44,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
|||
mod.Description = description ?? mod.Description;
|
||||
mod.Version = version ?? mod.Version;
|
||||
mod.Website = website ?? mod.Website;
|
||||
mod.ModTags = tags;
|
||||
saveService.ImmediateSaveSync(new ModMeta(mod));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -37,11 +37,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
|||
|
||||
public struct ImportDate : ISortMode<Mod>
|
||||
{
|
||||
public string Name
|
||||
=> "Import Date (Older First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Import Date (Older First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate));
|
||||
|
|
@ -49,11 +49,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
|||
|
||||
public struct InverseImportDate : ISortMode<Mod>
|
||||
{
|
||||
public string Name
|
||||
=> "Import Date (Newer First)";
|
||||
public ReadOnlySpan<byte> Name
|
||||
=> "Import Date (Newer First)"u8;
|
||||
|
||||
public string Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date.";
|
||||
public ReadOnlySpan<byte> Description
|
||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
|
||||
|
||||
public IEnumerable<IPath> GetChildren(Folder f)
|
||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate));
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Manager.OptionEditor;
|
||||
using Penumbra.Services;
|
||||
|
|
@ -303,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
|||
if (!firstTime && _config.ModDirectory != BasePath.FullName)
|
||||
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
||||
}
|
||||
|
||||
if (CloudApi.IsCloudSynced(BasePath.FullName))
|
||||
Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues.");
|
||||
}
|
||||
|
||||
private void TriggerModDirectoryChange(string newPath, bool valid)
|
||||
|
|
|
|||
|
|
@ -32,12 +32,12 @@ public partial class ModCreator(
|
|||
public readonly Configuration Config = config;
|
||||
|
||||
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
|
||||
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null)
|
||||
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
||||
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty);
|
||||
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags);
|
||||
CreateDefaultFiles(newDir);
|
||||
return newDir;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,25 @@ public class CombinedDataContainer(IModGroup group) : IModDataContainer
|
|||
return sb.ToString(0, sb.Length - 3);
|
||||
}
|
||||
|
||||
public unsafe string GetDirectoryName()
|
||||
{
|
||||
if (Name.Length > 0)
|
||||
return Name;
|
||||
|
||||
var index = GetDataIndex();
|
||||
if (index == 0)
|
||||
return "None";
|
||||
|
||||
var text = stackalloc char[IModGroup.MaxCombiningOptions].Slice(0, Group.Options.Count);
|
||||
for (var i = 0; i < Group.Options.Count; ++i)
|
||||
{
|
||||
text[Group.Options.Count - 1 - i] = (index & 1) is 0 ? '0' : '1';
|
||||
index >>= 1;
|
||||
}
|
||||
|
||||
return new string(text);
|
||||
}
|
||||
|
||||
public string GetFullName()
|
||||
=> $"{Group.Name}: {GetName()}";
|
||||
|
||||
|
|
|
|||
46
Penumbra/Mods/SubMods/ComplexDataContainer.cs
Normal file
46
Penumbra/Mods/SubMods/ComplexDataContainer.cs
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Mods.SubMods;
|
||||
|
||||
public sealed class ComplexDataContainer(ComplexModGroup group) : IModDataContainer
|
||||
{
|
||||
public IMod Mod
|
||||
=> Group.Mod;
|
||||
|
||||
public IModGroup Group { get; } = group;
|
||||
|
||||
public Dictionary<Utf8GamePath, FullPath> Files { get; set; } = [];
|
||||
public Dictionary<Utf8GamePath, FullPath> FileSwaps { get; set; } = [];
|
||||
public MetaDictionary Manipulations { get; set; } = new();
|
||||
|
||||
public MaskedSetting Association = MaskedSetting.Zero;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string GetName()
|
||||
=> Name.Length > 0 ? Name : $"Container {Group.DataContainers.IndexOf(this)}";
|
||||
|
||||
public string GetDirectoryName()
|
||||
=> Name.Length > 0 ? Name : $"{Group.DataContainers.IndexOf(this)}";
|
||||
|
||||
public string GetFullName()
|
||||
=> $"{Group.Name}: {GetName()}";
|
||||
|
||||
public (int GroupIndex, int DataIndex) GetDataIndices()
|
||||
=> (Group.GetIndex(), Group.DataContainers.IndexOf(this));
|
||||
|
||||
public ComplexDataContainer(ComplexModGroup group, JToken json)
|
||||
: this(group)
|
||||
{
|
||||
SubMod.LoadDataContainer(json, this, group.Mod.ModPath);
|
||||
var mask = json["AssociationMask"]?.ToObject<ulong>() ?? 0;
|
||||
var value = json["AssociationMask"]?.ToObject<ulong>() ?? 0;
|
||||
Association = new MaskedSetting(mask, value);
|
||||
Name = json["Name"]?.ToObject<string>() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
37
Penumbra/Mods/SubMods/ComplexSubMod.cs
Normal file
37
Penumbra/Mods/SubMods/ComplexSubMod.cs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Mods.Groups;
|
||||
|
||||
namespace Penumbra.Mods.SubMods;
|
||||
|
||||
public sealed class ComplexSubMod(ComplexModGroup group) : IModOption
|
||||
{
|
||||
public Mod Mod
|
||||
=> group.Mod;
|
||||
|
||||
public IModGroup Group { get; } = group;
|
||||
public string Name { get; set; } = "Option";
|
||||
|
||||
public string FullName
|
||||
=> $"{Group.Name}: {Name}";
|
||||
|
||||
public MaskedSetting Conditions = MaskedSetting.Zero;
|
||||
public int Indentation = 0;
|
||||
public string SubGroupLabel = string.Empty;
|
||||
|
||||
public string Description { get; set; } = string.Empty;
|
||||
|
||||
public int GetIndex()
|
||||
=> Group.Options.IndexOf(this);
|
||||
|
||||
public ComplexSubMod(ComplexModGroup group, JToken json)
|
||||
: this(group)
|
||||
{
|
||||
SubMod.LoadOptionData(json, this);
|
||||
var mask = json["ConditionMask"]?.ToObject<ulong>() ?? 0;
|
||||
var value = json["ConditionMask"]?.ToObject<ulong>() ?? 0;
|
||||
Conditions = new MaskedSetting(mask, value);
|
||||
Indentation = json["Indentation"]?.ToObject<int>() ?? 0;
|
||||
SubGroupLabel = json["SubGroup"]?.ToObject<string>() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer
|
|||
public string GetName()
|
||||
=> FullName;
|
||||
|
||||
public string GetDirectoryName()
|
||||
=> GetName();
|
||||
|
||||
public string GetFullName()
|
||||
=> FullName;
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ public interface IModDataContainer
|
|||
public MetaDictionary Manipulations { get; set; }
|
||||
|
||||
public string GetName();
|
||||
public string GetDirectoryName();
|
||||
public string GetFullName();
|
||||
public (int GroupIndex, int DataIndex) GetDataIndices();
|
||||
}
|
||||
|
|
|
|||
27
Penumbra/Mods/SubMods/MaskedSetting.cs
Normal file
27
Penumbra/Mods/SubMods/MaskedSetting.cs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Mods.SubMods;
|
||||
|
||||
public readonly struct MaskedSetting(Setting mask, Setting value)
|
||||
{
|
||||
public const int MaxSettings = IModGroup.MaxMultiOptions;
|
||||
public static readonly MaskedSetting Zero = new(Setting.Zero, Setting.Zero);
|
||||
public static readonly MaskedSetting FullMask = new(Setting.AllBits(IModGroup.MaxComplexOptions), Setting.Zero);
|
||||
|
||||
public readonly Setting Mask = mask;
|
||||
public readonly Setting Value = new(value.Value & mask.Value);
|
||||
|
||||
public MaskedSetting(ulong mask, ulong value)
|
||||
: this(new Setting(mask), new Setting(value))
|
||||
{ }
|
||||
|
||||
public MaskedSetting Limit(int numOptions)
|
||||
=> new(Mask.Value & Setting.AllBits(numOptions).Value, Value.Value);
|
||||
|
||||
public bool IsZero
|
||||
=> Mask.Value is 0;
|
||||
|
||||
public bool IsEnabled(Setting input)
|
||||
=> (input.Value & Mask.Value) == Value.Value;
|
||||
}
|
||||
|
|
@ -41,6 +41,9 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai
|
|||
public string GetName()
|
||||
=> Name;
|
||||
|
||||
public string GetDirectoryName()
|
||||
=> GetName();
|
||||
|
||||
public string GetFullName()
|
||||
=> FullName;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Game;
|
||||
using OtterGui;
|
||||
using OtterGui.Log;
|
||||
using OtterGui.Services;
|
||||
|
|
@ -21,6 +22,7 @@ using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManage
|
|||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Interop;
|
||||
using Penumbra.Interop.Hooks;
|
||||
using Penumbra.Interop.Hooks.PostProcessing;
|
||||
using Penumbra.Interop.Hooks.ResourceLoading;
|
||||
|
|
@ -209,10 +211,11 @@ public class Penumbra : IDalamudPlugin
|
|||
|
||||
public string GatherSupportInformation()
|
||||
{
|
||||
var sb = new StringBuilder(10240);
|
||||
var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory);
|
||||
var hdrEnabler = _services.GetService<RenderTargetHdrEnabler>();
|
||||
var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null;
|
||||
var sb = new StringBuilder(10240);
|
||||
var exists = _config.ModDirectory.Length > 0 && Directory.Exists(_config.ModDirectory);
|
||||
var cloudSynced = exists && CloudApi.IsCloudSynced(_config.ModDirectory);
|
||||
var hdrEnabler = _services.GetService<RenderTargetHdrEnabler>();
|
||||
var drive = exists ? new DriveInfo(new DirectoryInfo(_config.ModDirectory).Root.FullName) : null;
|
||||
sb.AppendLine("**Settings**");
|
||||
sb.Append($"> **`Plugin Version: `** {_validityChecker.Version}\n");
|
||||
sb.Append($"> **`Commit Hash: `** {_validityChecker.CommitHash}\n");
|
||||
|
|
@ -221,7 +224,8 @@ public class Penumbra : IDalamudPlugin
|
|||
sb.Append($"> **`Operating System: `** {(Dalamud.Utility.Util.IsWine() ? "Mac/Linux (Wine)" : "Windows")}\n");
|
||||
if (Dalamud.Utility.Util.IsWine())
|
||||
sb.Append($"> **`Locale Environment Variables:`** {CollectLocaleEnvironmentVariables()}\n");
|
||||
sb.Append($"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}\n");
|
||||
sb.Append(
|
||||
$"> **`Root Directory: `** `{_config.ModDirectory}`, {(exists ? "Exists" : "Not Existing")}{(cloudSynced ? ", Cloud-Synced" : "")}\n");
|
||||
sb.Append(
|
||||
$"> **`Free Drive Space: `** {(drive != null ? Functions.HumanReadableSize(drive.AvailableFreeSpace) : "Unknown")}\n");
|
||||
sb.Append($"> **`Game Data Files: `** {(_gameData.HasModifiedGameDataFiles ? "Modified" : "Pristine")}\n");
|
||||
|
|
@ -229,10 +233,12 @@ public class Penumbra : IDalamudPlugin
|
|||
sb.Append($"> **`Auto-UI-Reduplication: `** {_config.AutoReduplicateUiOnImport}\n");
|
||||
sb.Append($"> **`Debug Mode: `** {_config.DebugMode}\n");
|
||||
sb.Append($"> **`Penumbra Reloads: `** {hdrEnabler.PenumbraReloadCount}\n");
|
||||
sb.Append($"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n");
|
||||
sb.Append(
|
||||
$"> **`HDR Enabled (from Start): `** {_config.HdrRenderTargets} ({hdrEnabler is { FirstLaunchHdrState: true, FirstLaunchHdrHookOverrideState: true }}){(hdrEnabler.HdrEnabledSuccess ? ", Detour Called" : ", **NEVER CALLED**")}\n");
|
||||
sb.Append($"> **`Custom Shapes Enabled: `** {_config.EnableCustomShapes}\n");
|
||||
sb.Append($"> **`Hook Overrides: `** {HookOverrides.Instance.IsCustomLoaded}\n");
|
||||
sb.Append($"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n");
|
||||
sb.Append(
|
||||
$"> **`Synchronous Load (Dalamud): `** {(_services.GetService<DalamudConfigService>().GetDalamudConfig(DalamudConfigService.WaitingForPluginsOption, out bool v) ? v.ToString() : "Unknown")} (first Start: {hdrEnabler.FirstLaunchWaitForPluginsState?.ToString() ?? "Unknown"})\n");
|
||||
sb.Append(
|
||||
$"> **`Logging: `** Log: {_config.Ephemeral.EnableResourceLogging}, Watcher: {_config.Ephemeral.EnableResourceWatcher} ({_config.MaxResourceWatcherRecords})\n");
|
||||
sb.Append($"> **`Use Ownership: `** {_config.UseOwnerNameForCharacterCollection}\n");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Dalamud.NET.Sdk/12.0.2">
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>Penumbra</AssemblyTitle>
|
||||
<Company>absolute gangstas</Company>
|
||||
|
|
@ -57,11 +57,11 @@
|
|||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="EmbedIO" Version="3.5.2" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.7" />
|
||||
<PackageReference Include="SharpCompress" Version="0.39.0" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.3" />
|
||||
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.3" />
|
||||
<PackageReference Include="PeNet" Version="4.1.1" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" />
|
||||
<PackageReference Include="SharpCompress" Version="0.40.0" />
|
||||
<PackageReference Include="SharpGLTF.Core" Version="1.0.5" />
|
||||
<PackageReference Include="SharpGLTF.Toolkit" Version="1.0.5" />
|
||||
<PackageReference Include="PeNet" Version="5.1.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"Author": "Ottermandias, Adam, Wintermute",
|
||||
"Author": "Ottermandias, Nylfae, Adam, Wintermute",
|
||||
"Name": "Penumbra",
|
||||
"Punchline": "Runtime mod loader and manager.",
|
||||
"Description": "Runtime mod loader and manager.",
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
"RepoUrl": "https://github.com/xivdev/Penumbra",
|
||||
"ApplicableVersion": "any",
|
||||
"Tags": [ "modding" ],
|
||||
"DalamudApiLevel": 12,
|
||||
"DalamudApiLevel": 13,
|
||||
"LoadPriority": 69420,
|
||||
"LoadRequiredState": 2,
|
||||
"LoadSync": true,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ public class CommunicatorService : IDisposable, IService
|
|||
/// <inheritdoc cref="Communication.ResolvedFileChanged"/>
|
||||
public readonly ResolvedFileChanged ResolvedFileChanged = new();
|
||||
|
||||
/// <inheritdoc cref="Communication.PcpCreation"/>
|
||||
public readonly PcpCreation PcpCreation = new();
|
||||
|
||||
/// <inheritdoc cref="Communication.PcpParsing"/>
|
||||
public readonly PcpParsing PcpParsing = new();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
CollectionChange.Dispose();
|
||||
|
|
@ -105,5 +111,7 @@ public class CommunicatorService : IDisposable, IService
|
|||
ChangedItemClick.Dispose();
|
||||
SelectTab.Dispose();
|
||||
ResolvedFileChanged.Dispose();
|
||||
PcpCreation.Dispose();
|
||||
PcpParsing.Dispose();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
209
Penumbra/Services/FileWatcher.cs
Normal file
209
Penumbra/Services/FileWatcher.cs
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class FileWatcher : IDisposable, IService
|
||||
{
|
||||
// TODO: use ConcurrentSet when it supports comparers in Luna.
|
||||
private readonly ConcurrentDictionary<string, byte> _pending = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly ModImportManager _modImportManager;
|
||||
private readonly MessageService _messageService;
|
||||
private readonly Configuration _config;
|
||||
|
||||
private bool _pausedConsumer;
|
||||
private FileSystemWatcher? _fsw;
|
||||
private CancellationTokenSource? _cts = new();
|
||||
private Task? _consumer;
|
||||
|
||||
public FileWatcher(ModImportManager modImportManager, MessageService messageService, Configuration config)
|
||||
{
|
||||
_modImportManager = modImportManager;
|
||||
_messageService = messageService;
|
||||
_config = config;
|
||||
|
||||
if (_config.EnableDirectoryWatch)
|
||||
{
|
||||
SetupFileWatcher(_config.WatchDirectory);
|
||||
SetupConsumerTask();
|
||||
}
|
||||
}
|
||||
|
||||
public void Toggle(bool value)
|
||||
{
|
||||
if (_config.EnableDirectoryWatch == value)
|
||||
return;
|
||||
|
||||
_config.EnableDirectoryWatch = value;
|
||||
_config.Save();
|
||||
if (value)
|
||||
{
|
||||
SetupFileWatcher(_config.WatchDirectory);
|
||||
SetupConsumerTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
EndFileWatcher();
|
||||
EndConsumerTask();
|
||||
}
|
||||
}
|
||||
|
||||
internal void PauseConsumer(bool pause)
|
||||
=> _pausedConsumer = pause;
|
||||
|
||||
private void EndFileWatcher()
|
||||
{
|
||||
if (_fsw is null)
|
||||
return;
|
||||
|
||||
_fsw.Dispose();
|
||||
_fsw = null;
|
||||
}
|
||||
|
||||
private void SetupFileWatcher(string directory)
|
||||
{
|
||||
EndFileWatcher();
|
||||
_fsw = new FileSystemWatcher
|
||||
{
|
||||
IncludeSubdirectories = false,
|
||||
NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime,
|
||||
InternalBufferSize = 32 * 1024,
|
||||
};
|
||||
|
||||
// Only wake us for the exact patterns we care about
|
||||
_fsw.Filters.Add("*.pmp");
|
||||
_fsw.Filters.Add("*.pcp");
|
||||
_fsw.Filters.Add("*.ttmp");
|
||||
_fsw.Filters.Add("*.ttmp2");
|
||||
|
||||
_fsw.Created += OnPath;
|
||||
_fsw.Renamed += OnPath;
|
||||
UpdateDirectory(directory);
|
||||
}
|
||||
|
||||
|
||||
private void EndConsumerTask()
|
||||
{
|
||||
if (_cts is not null)
|
||||
{
|
||||
_cts.Cancel();
|
||||
_cts = null;
|
||||
}
|
||||
_consumer = null;
|
||||
}
|
||||
|
||||
private void SetupConsumerTask()
|
||||
{
|
||||
EndConsumerTask();
|
||||
_cts = new CancellationTokenSource();
|
||||
_consumer = Task.Factory.StartNew(
|
||||
() => ConsumerLoopAsync(_cts.Token),
|
||||
_cts.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default).Unwrap();
|
||||
}
|
||||
|
||||
public void UpdateDirectory(string newPath)
|
||||
{
|
||||
if (_config.WatchDirectory != newPath)
|
||||
{
|
||||
_config.WatchDirectory = newPath;
|
||||
_config.Save();
|
||||
}
|
||||
|
||||
if (_fsw is null)
|
||||
return;
|
||||
|
||||
_fsw.EnableRaisingEvents = false;
|
||||
if (!Directory.Exists(newPath) || newPath.Length is 0)
|
||||
{
|
||||
_fsw.Path = string.Empty;
|
||||
}
|
||||
else
|
||||
{
|
||||
_fsw.Path = newPath;
|
||||
_fsw.EnableRaisingEvents = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPath(object? sender, FileSystemEventArgs e)
|
||||
=> _pending.TryAdd(e.FullPath, 0);
|
||||
|
||||
private async Task ConsumerLoopAsync(CancellationToken token)
|
||||
{
|
||||
while (true)
|
||||
{
|
||||
var (path, _) = _pending.FirstOrDefault();
|
||||
if (path is null || _pausedConsumer)
|
||||
{
|
||||
await Task.Delay(500, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await ProcessOneAsync(path, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
Penumbra.Log.Debug("[FileWatcher] Canceled via Token.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Warning($"[FileWatcher] Error during Processing: {ex}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pending.TryRemove(path, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ProcessOneAsync(string path, CancellationToken token)
|
||||
{
|
||||
// Downloads often finish via rename; file may be locked briefly.
|
||||
// Wait until it exists and is readable; also require two stable size checks.
|
||||
const int maxTries = 40;
|
||||
long lastLen = -1;
|
||||
|
||||
for (var i = 0; i < maxTries && !token.IsCancellationRequested; i++)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
await Task.Delay(100, token);
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fi = new FileInfo(path);
|
||||
var len = fi.Length;
|
||||
if (len > 0 && len == lastLen)
|
||||
{
|
||||
if (_config.EnableAutomaticModImport)
|
||||
_modImportManager.AddUnpack(path);
|
||||
else
|
||||
_messageService.AddMessage(new InstallNotification(_modImportManager, path), false);
|
||||
return;
|
||||
}
|
||||
|
||||
lastLen = len;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
Penumbra.Log.Debug($"[FileWatcher] File is still being written to.");
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
Penumbra.Log.Debug($"[FileWatcher] File is locked.");
|
||||
}
|
||||
|
||||
await Task.Delay(150, token);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
EndConsumerTask();
|
||||
EndFileWatcher();
|
||||
}
|
||||
}
|
||||
39
Penumbra/Services/InstallNotification.cs
Normal file
39
Penumbra/Services/InstallNotification.cs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Interface.ImGuiNotification.EventArgs;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class InstallNotification(ModImportManager modImportManager, string filePath) : OtterGui.Classes.MessageService.IMessage
|
||||
{
|
||||
public string Message
|
||||
=> "A new mod has been found!";
|
||||
|
||||
public NotificationType NotificationType
|
||||
=> NotificationType.Info;
|
||||
|
||||
public uint NotificationDuration
|
||||
=> uint.MaxValue;
|
||||
|
||||
public string NotificationTitle { get; } = Path.GetFileNameWithoutExtension(filePath);
|
||||
|
||||
public string LogMessage
|
||||
=> $"A new mod has been found: {Path.GetFileName(filePath)}";
|
||||
|
||||
public void OnNotificationActions(INotificationDrawArgs args)
|
||||
{
|
||||
var region = ImGui.GetContentRegionAvail();
|
||||
var buttonSize = new Vector2((region.X - ImGui.GetStyle().ItemSpacing.X) / 2, 0);
|
||||
if (ImUtf8.ButtonEx("Install"u8, ""u8, buttonSize))
|
||||
{
|
||||
modImportManager.AddUnpack(filePath);
|
||||
args.Notification.DismissNow();
|
||||
}
|
||||
|
||||
ImGui.SameLine();
|
||||
if (ImUtf8.ButtonEx("Ignore"u8, ""u8, buttonSize))
|
||||
args.Notification.DismissNow();
|
||||
}
|
||||
}
|
||||
308
Penumbra/Services/PcpService.cs
Normal file
308
Penumbra/Services/PcpService.cs
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
using Dalamud.Game.ClientState.Objects.Types;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game.Object;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Actors;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.ResourceTree;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Groups;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Mods.SubMods;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Services;
|
||||
|
||||
public class PcpService : IApiService, IDisposable
|
||||
{
|
||||
public const string Extension = ".pcp";
|
||||
|
||||
private readonly Configuration _config;
|
||||
private readonly SaveService _files;
|
||||
private readonly ResourceTreeFactory _treeFactory;
|
||||
private readonly ObjectManager _objectManager;
|
||||
private readonly ActorManager _actors;
|
||||
private readonly FrameworkManager _framework;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly CollectionManager _collections;
|
||||
private readonly ModCreator _modCreator;
|
||||
private readonly ModExportManager _modExport;
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly SHA1 _sha1 = SHA1.Create();
|
||||
private readonly ModFileSystem _fileSystem;
|
||||
private readonly ModManager _mods;
|
||||
|
||||
public PcpService(Configuration config,
|
||||
SaveService files,
|
||||
ResourceTreeFactory treeFactory,
|
||||
ObjectManager objectManager,
|
||||
ActorManager actors,
|
||||
FrameworkManager framework,
|
||||
CollectionManager collections,
|
||||
CollectionResolver collectionResolver,
|
||||
ModCreator modCreator,
|
||||
ModExportManager modExport,
|
||||
CommunicatorService communicator,
|
||||
ModFileSystem fileSystem,
|
||||
ModManager mods)
|
||||
{
|
||||
_config = config;
|
||||
_files = files;
|
||||
_treeFactory = treeFactory;
|
||||
_objectManager = objectManager;
|
||||
_actors = actors;
|
||||
_framework = framework;
|
||||
_collectionResolver = collectionResolver;
|
||||
_collections = collections;
|
||||
_modCreator = modCreator;
|
||||
_modExport = modExport;
|
||||
_communicator = communicator;
|
||||
_fileSystem = fileSystem;
|
||||
_mods = mods;
|
||||
|
||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.PcpService);
|
||||
}
|
||||
|
||||
public void CleanPcpMods()
|
||||
{
|
||||
var mods = _mods.Where(m => m.ModTags.Contains("PCP")).ToList();
|
||||
Penumbra.Log.Information($"[PCPService] Deleting {mods.Count} mods containing the tag PCP.");
|
||||
foreach (var mod in mods)
|
||||
_mods.DeleteMod(mod);
|
||||
}
|
||||
|
||||
public void CleanPcpCollections()
|
||||
{
|
||||
var collections = _collections.Storage.Where(c => c.Identity.Name.StartsWith("PCP/")).ToList();
|
||||
Penumbra.Log.Information($"[PCPService] Deleting {collections.Count} collections starting with PCP/.");
|
||||
foreach (var collection in collections)
|
||||
_collections.Storage.RemoveCollection(collection);
|
||||
}
|
||||
|
||||
private void OnModPathChange(ModPathChangeType type, Mod mod, DirectoryInfo? oldDirectory, DirectoryInfo? newDirectory)
|
||||
{
|
||||
if (type is not ModPathChangeType.Added || _config.PcpSettings.DisableHandling || newDirectory is null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var file = Path.Combine(newDirectory.FullName, "character.json");
|
||||
if (!File.Exists(file))
|
||||
{
|
||||
// First version had collection.json, changed.
|
||||
var oldFile = Path.Combine(newDirectory.FullName, "collection.json");
|
||||
if (File.Exists(oldFile))
|
||||
{
|
||||
Penumbra.Log.Information("[PCPService] Renaming old PCP file from collection.json to character.json.");
|
||||
File.Move(oldFile, file, true);
|
||||
}
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
Penumbra.Log.Information($"[PCPService] Found a PCP file for {mod.Name}, applying.");
|
||||
var text = File.ReadAllText(file);
|
||||
var jObj = JObject.Parse(text);
|
||||
var collection = ModCollection.Empty;
|
||||
// Create collection.
|
||||
if (_config.PcpSettings.CreateCollection)
|
||||
{
|
||||
var identifier = _actors.FromJson(jObj["Actor"] as JObject);
|
||||
if (identifier.IsValid && jObj["Collection"]?.ToObject<string>() is { } collectionName)
|
||||
{
|
||||
var name = $"PCP/{collectionName}";
|
||||
if (_collections.Storage.AddCollection(name, null))
|
||||
{
|
||||
collection = _collections.Storage[^1];
|
||||
_collections.Editor.SetModState(collection, mod, true);
|
||||
|
||||
// Assign collection.
|
||||
if (_config.PcpSettings.AssignCollection)
|
||||
{
|
||||
var identifierGroup = _collections.Active.Individuals.GetGroup(identifier);
|
||||
_collections.Active.SetCollection(collection, CollectionType.Individual, identifierGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move to folder.
|
||||
if (_fileSystem.TryGetValue(mod, out var leaf))
|
||||
{
|
||||
try
|
||||
{
|
||||
var folder = _fileSystem.FindOrCreateAllFolders(_config.PcpSettings.FolderName);
|
||||
_fileSystem.Move(leaf, folder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignored.
|
||||
}
|
||||
}
|
||||
|
||||
// Invoke IPC.
|
||||
if (_config.PcpSettings.AllowIpc)
|
||||
_communicator.PcpParsing.Invoke(jObj, mod.Identifier, collection.Identity.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Error reading the character.json file from {mod.Identifier}:\n{ex}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChange);
|
||||
|
||||
public async Task<(bool, string)> CreatePcp(ObjectIndex objectIndex, string note = "", CancellationToken cancel = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
Penumbra.Log.Information($"[PCPService] Creating PCP file for game object {objectIndex.Index}.");
|
||||
var (identifier, tree, meta) = await _framework.Framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var (actor, identifier) = CheckActor(objectIndex);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
unsafe
|
||||
{
|
||||
var collection = _collectionResolver.IdentifyCollection((GameObject*)actor.Address, true);
|
||||
if (!collection.Valid || !collection.ModCollection.HasCache)
|
||||
throw new Exception($"Actor {identifier} has no mods applying, nothing to do.");
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
if (_treeFactory.FromCharacter(actor, 0) is not { } tree)
|
||||
throw new Exception($"Unable to fetch modded resources for {identifier}.");
|
||||
|
||||
var meta = new MetaDictionary(collection.ModCollection.MetaCache, actor.Address);
|
||||
return (identifier.CreatePermanent(), tree, meta);
|
||||
}
|
||||
});
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
var time = DateTime.Now;
|
||||
var modDirectory = CreateMod(identifier, note, time);
|
||||
await CreateDefaultMod(modDirectory, meta, tree, cancel);
|
||||
await CreateCollectionInfo(modDirectory, objectIndex, identifier, note, time, cancel);
|
||||
var file = ZipUp(modDirectory);
|
||||
return (true, file);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return (false, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static string ZipUp(DirectoryInfo directory)
|
||||
{
|
||||
var fileName = directory.FullName + Extension;
|
||||
ZipFile.CreateFromDirectory(directory.FullName, fileName, CompressionLevel.Optimal, false);
|
||||
directory.Delete(true);
|
||||
return fileName;
|
||||
}
|
||||
|
||||
private async Task CreateCollectionInfo(DirectoryInfo directory, ObjectIndex index, ActorIdentifier actor, string note, DateTime time,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var jObj = new JObject
|
||||
{
|
||||
["Version"] = 1,
|
||||
["Actor"] = actor.ToJson(),
|
||||
["Mod"] = directory.Name,
|
||||
["Collection"] = note.Length > 0 ? $"{actor.ToName()}: {note}" : actor.ToName(),
|
||||
["Time"] = time,
|
||||
["Note"] = note,
|
||||
};
|
||||
if (note.Length > 0)
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
if (_config.PcpSettings.AllowIpc)
|
||||
await _framework.Framework.RunOnFrameworkThread(() => _communicator.PcpCreation.Invoke(jObj, index.Index, directory.FullName));
|
||||
var filePath = Path.Combine(directory.FullName, "character.json");
|
||||
await using var file = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew);
|
||||
await using var stream = new StreamWriter(file);
|
||||
await using var json = new JsonTextWriter(stream);
|
||||
json.Formatting = Formatting.Indented;
|
||||
await jObj.WriteToAsync(json, cancel);
|
||||
}
|
||||
|
||||
private DirectoryInfo CreateMod(ActorIdentifier actor, string note, DateTime time)
|
||||
{
|
||||
var directory = _modExport.ExportDirectory;
|
||||
directory.Create();
|
||||
var actorName = actor.ToName();
|
||||
var authorName = _actors.GetCurrentPlayer().ToName();
|
||||
var suffix = note.Length > 0
|
||||
? note
|
||||
: time.ToString("yyyy-MM-ddTHH\\:mm", CultureInfo.InvariantCulture);
|
||||
var modName = $"{actorName} - {suffix}";
|
||||
var description = $"On-Screen Data for {actorName} as snapshotted on {time}.";
|
||||
return _modCreator.CreateEmptyMod(directory, modName, description, authorName, "PCP")
|
||||
?? throw new Exception($"Unable to create mod {modName} in {directory.FullName}.");
|
||||
}
|
||||
|
||||
private async Task CreateDefaultMod(DirectoryInfo modDirectory, MetaDictionary meta, ResourceTree tree,
|
||||
CancellationToken cancel = default)
|
||||
{
|
||||
var subDirectory = modDirectory.CreateSubdirectory("files");
|
||||
var subMod = new DefaultSubMod(null!)
|
||||
{
|
||||
Manipulations = meta,
|
||||
};
|
||||
|
||||
foreach (var node in tree.FlatNodes)
|
||||
{
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
var gamePath = node.GamePath;
|
||||
var fullPath = node.FullPath;
|
||||
if (fullPath.IsRooted)
|
||||
{
|
||||
var hash = await _sha1.ComputeHashAsync(File.OpenRead(fullPath.FullName), cancel).ConfigureAwait(false);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
var name = Convert.ToHexString(hash) + fullPath.Extension;
|
||||
var newFile = Path.Combine(subDirectory.FullName, name);
|
||||
if (!File.Exists(newFile))
|
||||
File.Copy(fullPath.FullName, newFile);
|
||||
subMod.Files.TryAdd(gamePath, new FullPath(newFile));
|
||||
}
|
||||
else if (gamePath.Path != fullPath.InternalName)
|
||||
{
|
||||
subMod.FileSwaps.TryAdd(gamePath, fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
|
||||
var saveGroup = new ModSaveGroup(modDirectory, subMod, _config.ReplaceNonAsciiOnImport);
|
||||
var filePath = _files.FileNames.OptionGroupFile(modDirectory.FullName, -1, string.Empty, _config.ReplaceNonAsciiOnImport);
|
||||
cancel.ThrowIfCancellationRequested();
|
||||
await using var fileStream = File.Open(filePath, File.Exists(filePath) ? FileMode.Truncate : FileMode.CreateNew);
|
||||
await using var writer = new StreamWriter(fileStream);
|
||||
saveGroup.Save(writer);
|
||||
}
|
||||
|
||||
private (ICharacter Actor, ActorIdentifier Identifier) CheckActor(ObjectIndex objectIndex)
|
||||
{
|
||||
var actor = _objectManager[objectIndex];
|
||||
if (!actor.Valid)
|
||||
throw new Exception($"No Actor at index {objectIndex} found.");
|
||||
|
||||
if (!actor.Identifier(_actors, out var identifier))
|
||||
throw new Exception($"Could not create valid identifier for actor at index {objectIndex}.");
|
||||
|
||||
if (!actor.IsCharacter)
|
||||
throw new Exception($"Actor {identifier} at index {objectIndex} is not a valid character.");
|
||||
|
||||
if (!actor.Model.Valid)
|
||||
throw new Exception($"Actor {identifier} at index {objectIndex} has no model.");
|
||||
|
||||
if (_objectManager.Objects.CreateObjectReference(actor.Address) is not ICharacter character)
|
||||
throw new Exception($"Actor {identifier} at index {objectIndex} could not be converted to ICharacter");
|
||||
|
||||
return (character, identifier);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.DataContainers;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Compression;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Mods.Editor;
|
||||
using Penumbra.Services;
|
||||
|
|
@ -80,7 +81,7 @@ public class FileEditor<T>(
|
|||
private Exception? _currentException;
|
||||
private bool _changed;
|
||||
|
||||
private string _defaultPath = string.Empty;
|
||||
private string _defaultPath = typeof(T) == typeof(ModEditWindow.PbdTab) ? GamePaths.Pbd.Path : string.Empty;
|
||||
private bool _inInput;
|
||||
private Utf8GamePath _defaultPathUtf8;
|
||||
private bool _isDefaultPathUtf8Valid;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Raii;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
@ -131,7 +131,7 @@ public sealed unsafe class MaterialTemplatePickers : IUiService
|
|||
if (texture == null)
|
||||
continue;
|
||||
var handle = _textureArraySlicer.GetImGuiHandle(texture, sliceIndex);
|
||||
if (handle == 0)
|
||||
if (handle.IsNull)
|
||||
continue;
|
||||
|
||||
var position = regionStart with { X = regionStart.X + (itemSize.X + itemSpacing) * j };
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using ImGuiNET;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
using Penumbra.GameData.Files;
|
||||
using OtterGui.Text;
|
||||
|
|
@ -338,10 +338,10 @@ public partial class MtrlTab
|
|||
var tmp = inputSqrt;
|
||||
if (ImUtf8.ColorEdit(label, ref tmp,
|
||||
ImGuiColorEditFlags.NoInputs
|
||||
| ImGuiColorEditFlags.DisplayRGB
|
||||
| ImGuiColorEditFlags.InputRGB
|
||||
| ImGuiColorEditFlags.DisplayRgb
|
||||
| ImGuiColorEditFlags.InputRgb
|
||||
| ImGuiColorEditFlags.NoTooltip
|
||||
| ImGuiColorEditFlags.HDR)
|
||||
| ImGuiColorEditFlags.Hdr)
|
||||
&& tmp != inputSqrt)
|
||||
{
|
||||
setter((HalfColor)PseudoSquareRgb(tmp));
|
||||
|
|
@ -373,10 +373,10 @@ public partial class MtrlTab
|
|||
var tmp = Vector4.Zero;
|
||||
ImUtf8.ColorEdit(label, ref tmp,
|
||||
ImGuiColorEditFlags.NoInputs
|
||||
| ImGuiColorEditFlags.DisplayRGB
|
||||
| ImGuiColorEditFlags.InputRGB
|
||||
| ImGuiColorEditFlags.DisplayRgb
|
||||
| ImGuiColorEditFlags.InputRgb
|
||||
| ImGuiColorEditFlags.NoTooltip
|
||||
| ImGuiColorEditFlags.HDR
|
||||
| ImGuiColorEditFlags.Hdr
|
||||
| ImGuiColorEditFlags.AlphaPreview);
|
||||
|
||||
if (letter.Length > 0 && ImGui.IsItemVisible())
|
||||
|
|
@ -594,7 +594,7 @@ public partial class MtrlTab
|
|||
internal static float PseudoSqrtRgb(float x)
|
||||
=> x < 0.0f ? -MathF.Sqrt(-x) : MathF.Sqrt(x);
|
||||
|
||||
internal static Vector3 PseudoSqrtRgb(Vector3 vec)
|
||||
public static Vector3 PseudoSqrtRgb(Vector3 vec)
|
||||
=> new(PseudoSqrtRgb(vec.X), PseudoSqrtRgb(vec.Y), PseudoSqrtRgb(vec.Z));
|
||||
|
||||
internal static Vector4 PseudoSqrtRgb(Vector4 vec)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility.Raii;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
|
|
@ -67,7 +67,7 @@ public partial class MtrlTab
|
|||
private static void DrawLegacyColorTableHeader(bool hasDyeTable)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImUtf8.TableHeader(default(ReadOnlySpan<byte>));
|
||||
ImUtf8.TableHeader(""u8);
|
||||
ImGui.TableNextColumn();
|
||||
ImUtf8.TableHeader("Row"u8);
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.GameData.Files.MaterialStructs;
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue