Compare commits

...

119 commits

Author SHA1 Message Date
Karou
ccb5b01290 Api version bump and remove redundant framework thread call
Some checks failed
.NET Build / build (push) Has been cancelled
2025-12-05 13:39:19 +01:00
Actions User
5dd74297c6 [CI] Updating repo.json for 1.5.1.8
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-28 22:10:17 +00:00
Karou
ce54aa5d25 Added IPC call to allow for redrawing only members of specified collections
Some checks failed
.NET Build / build (push) Has been cancelled
2025-11-03 15:15:40 +01:00
Actions User
c4b6e4e00b [CI] Updating repo.json for testing_1.5.1.7
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-23 21:50:20 +00:00
Ottermandias
912c183fc6 Improve file watcher. 2025-10-23 23:45:20 +02:00
Ottermandias
5bf901d0c4 Update actorobjectmanager when setting cutscene index.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-23 17:30:29 +02:00
Ottermandias
cbedc878b9 Slight cleanup and autoformat. 2025-10-22 21:56:16 +02:00
Ottermandias
c8cf560fc1 Merge branch 'refs/heads/StoiaCode/fileWatcher' 2025-10-22 21:48:42 +02:00
Stoia
f05cb52da2 Add Option to notify instead of auto install.
And General Fixes
2025-10-22 18:20:44 +02:00
Ottermandias
7ed81a9823 Update OtterGui.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-10-22 17:53:02 +02:00
Stoia
60aa23efcd
Merge branch 'xivdev:master' into fileWatcher 2025-10-22 14:28:08 +02:00
Ottermandias
ebbe957c95 Remove login screen log spam.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-11 20:13:51 +02:00
Actions User
300e0e6d84 [CI] Updating repo.json for 1.5.1.6
Some checks failed
.NET Build / build (push) Has been cancelled
2025-10-07 10:45:04 +00:00
Ottermandias
049baa4fe4 Again. 2025-10-07 12:42:54 +02:00
Ottermandias
0881dfde8a Update signatures. 2025-10-07 12:27:35 +02:00
Actions User
23c0506cb8 [CI] Updating repo.json for testing_1.5.1.5
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-28 10:43:01 +00:00
Ottermandias
699745413e Make priority an int. 2025-09-28 12:40:52 +02:00
Actions User
eb53f04c6b [CI] Updating repo.json for testing_1.5.1.4
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 12:03:35 +00:00
Ottermandias
c6b596169c Add default constructor. 2025-09-27 14:01:21 +02:00
Actions User
a0c3e820b0 [CI] Updating repo.json for testing_1.5.1.3
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-27 11:02:39 +00:00
Ottermandias
a59689ebfe CS API update and add http API routes. 2025-09-27 13:00:18 +02:00
Exter-N
e9f67a009b Lift "shaders known" restriction for saving materials
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-19 11:18:39 +02:00
Ottermandias
97c8d82b33 Prevent default-named collection from being renamed and always put it at the top of the selector.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-07 10:45:28 +02:00
Stoia
c3b00ff426 Integrate FileWatcher
HEAVY WIP
2025-09-06 14:22:18 +02:00
Actions User
6348c4a639 [CI] Updating repo.json for 1.5.1.2
Some checks failed
.NET Build / build (push) Has been cancelled
2025-09-02 14:25:55 +00:00
Ottermandias
5a6e06df3b git is stupid 2025-09-02 16:22:02 +02:00
Ottermandias
f5f6dd3246 Handle some TODOs. 2025-09-02 16:12:01 +02:00
Ottermandias
4e788f7c2b Update sig. 2025-09-02 11:51:59 +02:00
Ottermandias
ad1659caf6 Update libraries. 2025-09-02 11:29:58 +02:00
Ottermandias
18a6ce2a5f Merge branch 'refs/heads/Exter-N/cldapi'
Some checks are pending
.NET Build / build (push) Waiting to run
2025-09-01 15:59:26 +02:00
Ottermandias
e68e821b2a Merge branch 'master' into Exter-N/cldapi 2025-09-01 15:58:22 +02:00
Ottermandias
96764b34ca Merge branch 'refs/heads/Exter-N/restree-stuff' 2025-09-01 15:57:06 +02:00
Exter-N
2cf60b78cd Reject and warn about cloud-synced base directories 2025-08-31 06:42:45 +02:00
Exter-N
d59be1e660 Refine IsCloudSynced 2025-08-31 05:25:37 +02:00
Exter-N
5503bb32e0 CloudApi testing in Debug tab 2025-08-31 04:13:56 +02:00
Exter-N
f3ec4b2e08 Only display the file name and last dir for externals 2025-08-30 19:19:07 +02:00
Exter-N
b3379a9710 Stop redacting external paths 2025-08-30 16:55:20 +02:00
Exter-N
8c25ef4b47 Make the save button ResourceTreeViewer baseline 2025-08-30 16:53:12 +02:00
Ottermandias
912020cc3f Update for staging and wrong tooltip.
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-29 16:36:42 +02:00
Ottermandias
be8987a451 Merge branch 'master' of github.com:xivDev/Penumbra
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-28 18:52:29 +02:00
Ottermandias
f7cf5503bb Fix deleting PCP collections. 2025-08-28 18:52:06 +02:00
Ottermandias
a04a5a071c Add warning in file redirections if extension doesn't match. 2025-08-28 18:51:57 +02:00
Actions User
71e24c13c7 [CI] Updating repo.json for 1.5.1.0
Some checks failed
.NET Build / build (push) Has been cancelled
2025-08-25 08:39:42 +00:00
Ottermandias
c0120f81af 1.5.1.0 2025-08-25 10:37:38 +02:00
Ottermandias
da47c19aeb Woops, increment version. 2025-08-25 10:25:05 +02:00
Actions User
e16800f216 [CI] Updating repo.json for testing_1.5.0.10 2025-08-25 08:16:04 +00:00
Ottermandias
79a4fc5904 Fix wrong logging. 2025-08-25 10:13:48 +02:00
Ottermandias
bf90725dd2 Fix resolvecontext issue. 2025-08-25 10:13:39 +02:00
Ottermandias
a14347f73a Update temporary collection creation. 2025-08-25 10:13:31 +02:00
Actions User
1e07e43498 [CI] Updating repo.json for testing_1.5.0.9
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-24 13:51:43 +00:00
Ottermandias
f51f8a7bf8 Try to filter meta entries for relevance. 2025-08-24 15:24:57 +02:00
Exter-N
1fca78fa71 Add Kdb files to ResourceTree 2025-08-24 14:09:02 +02:00
Exter-N
c8b6325a87 Add game integrity message to On-Screen 2025-08-24 14:06:39 +02:00
Ottermandias
6079103505 Add collection PCP settings.
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-23 14:46:27 +02:00
Actions User
d302a17f1f [CI] Updating repo.json for testing_1.5.0.8
Some checks are pending
.NET Build / build (push) Waiting to run
2025-08-22 18:33:43 +00:00
Ottermandias
0d64384059 Add cleanup buttons to PCP, add option to turn off PCP IPC. 2025-08-22 20:31:40 +02:00
Ottermandias
10894d451a Add Pcp Events. 2025-08-22 18:08:22 +02:00
Actions User
fb34238530 [CI] Updating repo.json for testing_1.5.0.7 2025-08-22 13:51:50 +00:00
Ottermandias
8043e6fb6b Add option to disable PCP. 2025-08-22 15:49:15 +02:00
Ottermandias
e3b7f72893 Add initial PCP. 2025-08-22 15:44:33 +02:00
Ottermandias
b7f326e29c Fix bug with collection setting and empty collection. 2025-08-22 15:43:55 +02:00
Ottermandias
dad01e1af8 Update GameData. 2025-08-20 15:24:00 +02:00
Ottermandias
10b71930a1 Merge branch 'refs/heads/Exter-N/stockings-skin-slot' 2025-08-18 15:41:22 +02:00
Ottermandias
23257f94a4 Some cleanup and add option to disable skin material attribute scanning. 2025-08-18 15:41:10 +02:00
Ottermandias
83a36ed4cb Merge branch 'master' into Exter-N/stockings-skin-slot 2025-08-18 15:31:52 +02:00
Ottermandias
8304579d29 Add predefined tags to the multi mod selector. 2025-08-17 13:54:36 +02:00
Actions User
24cbc6c5e1 [CI] Updating repo.json for 1.5.0.6 2025-08-17 08:46:26 +00:00
Exter-N
41edc23820 Allow changing the skin mtrl suffix 2025-08-17 03:11:11 +02:00
Exter-N
aa920b5e9b Fix ImGui texture usage issue 2025-08-17 01:41:49 +02:00
Ottermandias
87ace28bcf Update OtterGui. 2025-08-16 11:56:24 +02:00
Ottermandias
5917f5fad1 Small fixes. 2025-08-13 17:42:45 +02:00
Actions User
f69c264317 [CI] Updating repo.json for 1.5.0.5 2025-08-13 14:53:11 +00:00
Ottermandias
a7246b9d98 Add PBD Post-Processor that appends EPBD data if the loaded PBD does not contain it. 2025-08-13 16:50:26 +02:00
Actions User
9aff388e21 [CI] Updating repo.json for 1.5.0.4 2025-08-12 12:53:33 +00:00
Ottermandias
091aff1b8a Merge branch 'master' of github.com:xivDev/Penumbra 2025-08-12 14:47:50 +02:00
Ottermandias
9f8185f67b Add new parameter to LoadWeapon hook. 2025-08-12 14:47:35 +02:00
Actions User
b112d75a27 [CI] Updating repo.json for 1.5.0.3 2025-08-12 10:31:13 +00:00
Ottermandias
7af81a6c18 Fix issue with removing default metadata. 2025-08-12 12:29:09 +02:00
Ottermandias
12a218bb2b Protect against empty requested paths. 2025-08-12 12:28:56 +02:00
Ottermandias
f6bac93db7 Update ChangedEquipData. 2025-08-11 19:58:24 +02:00
Actions User
155d3d49aa [CI] Updating repo.json for 1.5.0.2 2025-08-09 16:40:42 +00:00
Exter-N
9aae2210a2 Fix nullptr crashes 2025-08-09 14:57:15 +02:00
Actions User
3785a629ce [CI] Updating repo.json for 1.5.0.1 2025-08-09 11:03:24 +00:00
Ottermandias
02af52671f Need staging again ... 2025-08-09 13:00:40 +02:00
Ottermandias
391c9d727e Fix shifted timeline vfunc offset. 2025-08-09 12:51:39 +02:00
Ottermandias
ff2b2be953 Fix popups not working early. 2025-08-09 12:11:29 +02:00
Ottermandias
6242b30f93 Fix resizable child. 2025-08-09 11:58:35 +02:00
Exter-N
11cd08a9de ClientStructs-ify stuff 2025-08-09 10:29:29 +02:00
Ottermandias
46cfbcb115 Set Repo API level to 13 and remove stg from future releases. 2025-08-08 23:13:23 +02:00
Actions User
66543cc671 [CI] Updating repo.json for 1.5.0.0 2025-08-08 21:12:00 +00:00
Ottermandias
13283c9690 Fix dumb. 2025-08-08 23:08:26 +02:00
Ottermandias
bedfb22466 Use staging for release. 2025-08-08 23:04:50 +02:00
Ottermandias
13df8b2248 Update gamedata. 2025-08-08 23:02:22 +02:00
Ottermandias
93406e4d4e 1.5.0.0 2025-08-08 16:17:59 +02:00
Ridan Vandenbergh
8140d08557 Add vertex material types for usages of 2 colour attributes 2025-08-08 16:15:19 +02:00
Ridan Vandenbergh
2b36f39848 Fix basecolor texture in material export 2025-08-08 16:12:37 +02:00
Ottermandias
a69811800d Update GameData 2025-08-08 15:56:25 +02:00
Ottermandias
3f18ad50de Initial API13 / 7.3 update. 2025-08-08 00:45:24 +02:00
Ridan Vandenbergh
6689e326ee Material tab: disallow "Enable Transparency" for stockings shader 2025-08-02 00:38:24 +02:00
Passive
bdcab22a55 Cleanup methods to extension class 2025-08-02 00:16:55 +02:00
Passive
f5f4fe7259 Invalid tangent fix example 2025-08-02 00:16:55 +02:00
Sebastina
898963fea5 Allow focusing a specified mod via HTTP API under the mods tab. 2025-08-02 00:16:38 +02:00
Ottermandias
8527bfa29c Fix missing updates for OtterGui. 2025-08-02 00:13:35 +02:00
Ottermandias
baca3cdec2 Update Libs. 2025-08-02 00:08:09 +02:00
Ottermandias
dc93eba34c Add initial complex group things. 2025-08-02 00:06:25 +02:00
Ottermandias
012052daa0 Change behavior for directory names. 2025-08-02 00:06:03 +02:00
Ottermandias
a9546e31ee Update packages. 2025-08-02 00:05:27 +02:00
Actions User
a4a6283e7b [CI] Updating repo.json for testing_1.4.0.6 2025-07-14 15:12:06 +00:00
Ottermandias
00c02fd16e Fix tex file migration for small textures. 2025-07-14 17:09:07 +02:00
Ottermandias
140d150bb4 Fix character sound data. 2025-07-14 17:08:46 +02:00
Actions User
49a6d935f3 [CI] Updating repo.json for testing_1.4.0.5 2025-07-05 20:11:28 +00:00
Ottermandias
692beacc2e Merge remote-tracking branch 'Exter-N/human-skin-materials' 2025-07-05 22:04:48 +02:00
Ottermandias
a953febfba Add support for imc-toggle attributes to accessories, and fix up attributes when item swapping models. 2025-07-05 22:03:32 +02:00
Ottermandias
c0aa2e36ea Merge branch 'refs/heads/Exter-N/reslogger-tid' 2025-07-05 22:03:14 +02:00
Exter-N
278bf43b29 ClientStructs-ify ResourceTree stuff 2025-07-05 05:20:24 +02:00
Exter-N
a97d9e4953 Add Human skin material handling 2025-07-05 04:37:37 +02:00
Exter-N
30e3cd1f38 Add OS thread ID info to the Resource Logger 2025-07-04 19:41:31 +02:00
Ottermandias
62e9dc164d Add support button. 2025-06-26 14:49:28 +02:00
Actions User
9fc572ba0c [CI] Updating repo.json for testing_1.4.0.4 2025-06-15 21:47:41 +00:00
186 changed files with 3314 additions and 545 deletions

View file

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

@ -1 +1 @@
Subproject commit 78528f93ac253db0061d9a8244cfa0cee5c2f873
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770

@ -1 +1 @@
Subproject commit ff7b3b4014a97455f823380c78b8a7c5107f8e2f
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf

View file

@ -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

View file

@ -0,0 +1,7 @@
namespace Penumbra.Api.Api;
public static class IdentityChecker
{
public static bool Check(string identity)
=> true;
}

View file

@ -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)

View file

@ -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()
{

View file

@ -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
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
{
public void RedrawObject(int gameObjectIndex, RedrawType setting)
{
@ -28,6 +31,24 @@ 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;

View file

@ -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)

View file

@ -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)
{}
}
}
}

View file

@ -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),

View file

@ -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()

View file

@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Text;

View file

@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui;
using Dalamud.Plugin;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.IpcSubscribers;

View file

@ -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;

View file

@ -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();

View file

@ -1,5 +1,5 @@
using Dalamud.Plugin;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui.Services;
using Penumbra.Api.Enums;

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Text;
namespace Penumbra;

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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,

View 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,
}
}

View 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,
}
}

View file

@ -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);

View file

@ -2,5 +2,6 @@ namespace Penumbra;
public class DebugConfiguration
{
public static bool WriteImcBytesToLog = false;
public static bool WriteImcBytesToLog = false;
public static bool UseSkinMaterialProcessing = true;
}

View file

@ -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));
}
}
}

View file

@ -364,7 +364,7 @@ public class MeshExporter
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}");
}
@ -538,6 +590,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)
=> data switch

View file

@ -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));
}
}

View file

@ -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)

View 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);
}
}

View file

@ -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();

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using Penumbra.Import.Structs;

View file

@ -1,4 +1,4 @@
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui.Raii;
using OtterGui;
using SixLabors.ImageSharp.PixelFormats;

View file

@ -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.");

View file

@ -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)

View file

@ -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);

View 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);
}

View file

@ -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)
{

View file

@ -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);

View file

@ -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);
}

View file

@ -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);

View file

@ -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));

View file

@ -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));

View file

@ -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)
{

View file

@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
return false;
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
_objects.InvokeRequiredUpdates();
return true;
}

View file

@ -0,0 +1,7 @@
namespace Penumbra.Interop;
public static partial class ProcessThreadApi
{
[LibraryImport("kernel32.dll")]
public static partial uint GetCurrentThreadId();
}

View 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 [];
}
}
}

View 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 [];
}
}

View file

@ -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;

View file

@ -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)'/');

View file

@ -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)
{

View file

@ -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}";

View file

@ -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;

View file

@ -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()

View file

@ -34,6 +34,12 @@ internal static class StructExtensions
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)
{
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
@ -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;

View file

@ -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;
}
}

View file

@ -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)

View file

@ -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);

View file

@ -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();

View file

@ -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;

View 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;
}
}

View file

@ -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; }

View file

@ -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);

View file

@ -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));
}

View file

@ -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));

View file

@ -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)

View file

@ -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;
}

View file

@ -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()}";

View 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;
}
}

View 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;
}
}

View file

@ -27,6 +27,9 @@ public class DefaultSubMod(IMod mod) : IModDataContainer
public string GetName()
=> FullName;
public string GetDirectoryName()
=> GetName();
public string GetFullName()
=> FullName;

View file

@ -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();
}

View 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;
}

View file

@ -41,6 +41,9 @@ public abstract class OptionSubMod(IModGroup group) : IModOption, IModDataContai
public string GetName()
=> Name;
public string GetDirectoryName()
=> GetName();
public string GetFullName()
=> FullName;

View file

@ -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");

View file

@ -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>

View file

@ -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,

View file

@ -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();
}
}

View 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();
}
}

View 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();
}
}

View 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);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -1,5 +1,5 @@
using Dalamud.Interface.ImGuiNotification;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;

View file

@ -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 };

View file

@ -1,5 +1,5 @@
using Dalamud.Interface;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;

View file

@ -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)

View file

@ -1,5 +1,5 @@
using Dalamud.Interface;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Extensions;

View file

@ -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();

View file

@ -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;

View file

@ -1,6 +1,6 @@
using Dalamud.Interface;
using Dalamud.Interface.ImGuiNotification;
using ImGuiNET;
using Dalamud.Bindings.ImGui;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
@ -384,7 +384,7 @@ public partial class MtrlTab
var shpkFlags = (int)Mtrl.ShaderPackage.Flags;
ImGui.SetNextItemWidth(UiHelpers.Scale * 250.0f);
if (!ImGui.InputInt("Shader Flags", ref shpkFlags, 0, 0,
ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
flags: ImGuiInputTextFlags.CharsHexadecimal | (disabled ? ImGuiInputTextFlags.ReadOnly : ImGuiInputTextFlags.None)))
return false;
Mtrl.ShaderPackage.Flags = (uint)shpkFlags;

Some files were not shown because too many files have changed in this diff Show more