mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Compare commits
200 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb5b01290 | ||
|
|
5dd74297c6 | ||
|
|
ce54aa5d25 | ||
|
|
c4b6e4e00b | ||
|
|
912c183fc6 | ||
|
|
5bf901d0c4 | ||
|
|
cbedc878b9 | ||
|
|
c8cf560fc1 | ||
|
|
f05cb52da2 | ||
|
|
7ed81a9823 | ||
|
|
60aa23efcd | ||
|
|
ebbe957c95 | ||
|
|
300e0e6d84 | ||
|
|
049baa4fe4 | ||
|
|
0881dfde8a | ||
|
|
23c0506cb8 | ||
|
|
699745413e | ||
|
|
eb53f04c6b | ||
|
|
c6b596169c | ||
|
|
a0c3e820b0 | ||
|
|
a59689ebfe | ||
|
|
e9f67a009b | ||
|
|
97c8d82b33 | ||
|
|
c3b00ff426 | ||
|
|
6348c4a639 | ||
|
|
5a6e06df3b | ||
|
|
f5f6dd3246 | ||
|
|
4e788f7c2b | ||
|
|
ad1659caf6 | ||
|
|
18a6ce2a5f | ||
|
|
e68e821b2a | ||
|
|
96764b34ca | ||
|
|
2cf60b78cd | ||
|
|
d59be1e660 | ||
|
|
5503bb32e0 | ||
|
|
f3ec4b2e08 | ||
|
|
b3379a9710 | ||
|
|
8c25ef4b47 | ||
|
|
912020cc3f | ||
|
|
be8987a451 | ||
|
|
f7cf5503bb | ||
|
|
a04a5a071c | ||
|
|
71e24c13c7 | ||
|
|
c0120f81af | ||
|
|
da47c19aeb | ||
|
|
e16800f216 | ||
|
|
79a4fc5904 | ||
|
|
bf90725dd2 | ||
|
|
a14347f73a | ||
|
|
1e07e43498 | ||
|
|
f51f8a7bf8 | ||
|
|
1fca78fa71 | ||
|
|
c8b6325a87 | ||
|
|
6079103505 | ||
|
|
d302a17f1f | ||
|
|
0d64384059 | ||
|
|
10894d451a | ||
|
|
fb34238530 | ||
|
|
8043e6fb6b | ||
|
|
e3b7f72893 | ||
|
|
b7f326e29c | ||
|
|
dad01e1af8 | ||
|
|
10b71930a1 | ||
|
|
23257f94a4 | ||
|
|
83a36ed4cb | ||
|
|
8304579d29 | ||
|
|
24cbc6c5e1 | ||
|
|
41edc23820 | ||
|
|
aa920b5e9b | ||
|
|
87ace28bcf | ||
|
|
5917f5fad1 | ||
|
|
f69c264317 | ||
|
|
a7246b9d98 | ||
|
|
9aff388e21 | ||
|
|
091aff1b8a | ||
|
|
9f8185f67b | ||
|
|
b112d75a27 | ||
|
|
7af81a6c18 | ||
|
|
12a218bb2b | ||
|
|
f6bac93db7 | ||
|
|
155d3d49aa | ||
|
|
9aae2210a2 | ||
|
|
3785a629ce | ||
|
|
02af52671f | ||
|
|
391c9d727e | ||
|
|
ff2b2be953 | ||
|
|
6242b30f93 | ||
|
|
11cd08a9de | ||
|
|
46cfbcb115 | ||
|
|
66543cc671 | ||
|
|
13283c9690 | ||
|
|
bedfb22466 | ||
|
|
13df8b2248 | ||
|
|
93406e4d4e | ||
|
|
8140d08557 | ||
|
|
2b36f39848 | ||
|
|
a69811800d | ||
|
|
3f18ad50de | ||
|
|
6689e326ee | ||
|
|
bdcab22a55 | ||
|
|
f5f4fe7259 | ||
|
|
898963fea5 | ||
|
|
8527bfa29c | ||
|
|
baca3cdec2 | ||
|
|
dc93eba34c | ||
|
|
012052daa0 | ||
|
|
a9546e31ee | ||
|
|
a4a6283e7b | ||
|
|
00c02fd16e | ||
|
|
140d150bb4 | ||
|
|
49a6d935f3 | ||
|
|
692beacc2e | ||
|
|
a953febfba | ||
|
|
c0aa2e36ea | ||
|
|
278bf43b29 | ||
|
|
a97d9e4953 | ||
|
|
30e3cd1f38 | ||
|
|
62e9dc164d | ||
|
|
9fc572ba0c | ||
|
|
3c20b541ce | ||
|
|
1961b03d37 | ||
|
|
1f4ec984b3 | ||
|
|
4981b0348f | ||
|
|
a8c05fc6ee | ||
|
|
3d05662384 | ||
|
|
973814b31b | ||
|
|
a16fd85a7e | ||
|
|
4c0e6d2a67 | ||
|
|
535694e9c8 | ||
|
|
318a41fe52 | ||
|
|
98203e4e8a | ||
|
|
6cba63ac98 | ||
|
|
b48c4f440a | ||
|
|
75f4e66dbf | ||
|
|
74bd1cf911 | ||
|
|
ff2a9f95c4 | ||
|
|
9921c3332e | ||
|
|
f2927290f5 | ||
|
|
1551d9b6f3 | ||
|
|
5e985f4a84 | ||
|
|
2c115eda94 | ||
|
|
ebe45c6a47 | ||
|
|
82fc334be7 | ||
|
|
cd56163b1b | ||
|
|
ccc2c1fd4c | ||
|
|
08c9124858 | ||
|
|
1bdbfe22c1 | ||
|
|
9e7c304556 | ||
|
|
bc4f88aee9 | ||
|
|
400d7d0bea | ||
|
|
ac4c75d3c3 | ||
|
|
507b0a5aee | ||
|
|
f5db888bbd | ||
|
|
d7dee39fab | ||
|
|
3412786282 | ||
|
|
861cbc7759 | ||
|
|
fefa3852f7 | ||
|
|
68b68d6ce7 | ||
|
|
47b5895404 | ||
|
|
e18e4bb0e1 | ||
|
|
6e4e28fa00 | ||
|
|
e326e3d809 | ||
|
|
fbc4c2d054 | ||
|
|
3078c467d0 | ||
|
|
52927ff06b | ||
|
|
08e8b9d2a4 | ||
|
|
f1448ed947 | ||
|
|
c0dcfdd835 | ||
|
|
70295b7a6b | ||
|
|
480942339f | ||
|
|
6ad0b4299a | ||
|
|
0adec35848 | ||
|
|
0fe4a3671a | ||
|
|
363d115be8 | ||
|
|
7595827d29 | ||
|
|
117724b0ae | ||
|
|
a5d221dc13 | ||
|
|
cbebfe5e99 | ||
|
|
0c768979d4 | ||
|
|
53ef42adfa | ||
|
|
0954f50912 | ||
|
|
5d5fc673b1 | ||
|
|
2bd0c89588 | ||
|
|
f03a139e0e | ||
|
|
f9b5a626cf | ||
|
|
dc336569ff | ||
|
|
0ec6a17ac7 | ||
|
|
129156a1c1 | ||
|
|
33ada1d994 | ||
|
|
0afcae4504 | ||
|
|
93e60471de | ||
|
|
5437ab477f | ||
|
|
3b54485127 | ||
|
|
c3b2443ab5 | ||
|
|
2fdafc5c85 | ||
|
|
09c2264de4 | ||
|
|
c3be151d40 | ||
|
|
abb47751c8 | ||
|
|
1d517103b3 | ||
|
|
fe5d1bc36e |
263 changed files with 7959 additions and 1363 deletions
|
|
@ -3576,6 +3576,18 @@ resharper_xaml_xaml_xamarin_forms_data_type_and_binding_context_type_mismatched_
|
|||
resharper_xaml_x_key_attribute_disallowed_highlighting=error
|
||||
resharper_xml_doc_comment_syntax_problem_highlighting=warning
|
||||
resharper_xunit_xunit_test_with_console_output_highlighting=warning
|
||||
csharp_style_prefer_implicitly_typed_lambda_expression = true:suggestion
|
||||
csharp_style_expression_bodied_methods = true:silent
|
||||
csharp_style_prefer_tuple_swap = true:suggestion
|
||||
csharp_style_prefer_unbound_generic_type_in_nameof = true:suggestion
|
||||
csharp_style_prefer_utf8_string_literals = true:suggestion
|
||||
csharp_style_inlined_variable_declaration = true:suggestion
|
||||
csharp_style_expression_bodied_constructors = true:silent
|
||||
csharp_style_expression_bodied_operators = true:silent
|
||||
csharp_style_deconstructed_variable_declaration = true:suggestion
|
||||
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
|
||||
csharp_style_unused_value_expression_statement_preference = discard_variable:silent
|
||||
csharp_style_expression_bodied_properties = true:silent
|
||||
|
||||
[*.{cshtml,htm,html,proto,razor}]
|
||||
indent_style=tab
|
||||
|
|
|
|||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
|||
run: dotnet restore
|
||||
- name: Download Dalamud
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/latest.zip -OutFile latest.zip
|
||||
Invoke-WebRequest -Uri https://goatcorp.github.io/dalamud-distrib/stg/latest.zip -OutFile latest.zip
|
||||
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||
- name: Build
|
||||
run: |
|
||||
|
|
|
|||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
|||
Subproject commit 3396ee176fa72ad2dfb2de3294f7125ebce4dae5
|
||||
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit bd56d82816b8366e19dddfb2dc7fd7f167e264ee
|
||||
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Dalamud.NET.Sdk/12.0.2">
|
||||
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
Subproject commit b6b91f846096d15276b728ba2078f27b95317d15
|
||||
Subproject commit d889f9ef918514a46049725052d378b441915b00
|
||||
|
|
@ -1 +1 @@
|
|||
Subproject commit 2896c0561f60827f97408650d52a15c38f4d9d10
|
||||
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793
|
||||
|
|
@ -42,6 +42,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\group_single.json = schemas\structs\group_single.json
|
||||
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
||||
schemas\structs\meta_atch.json = schemas\structs\meta_atch.json
|
||||
schemas\structs\meta_atr.json = schemas\structs\meta_atr.json
|
||||
schemas\structs\meta_enums.json = schemas\structs\meta_enums.json
|
||||
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
||||
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
||||
|
|
@ -50,6 +51,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "structs", "structs", "{B03F
|
|||
schemas\structs\meta_gmp.json = schemas\structs\meta_gmp.json
|
||||
schemas\structs\meta_imc.json = schemas\structs\meta_imc.json
|
||||
schemas\structs\meta_rsp.json = schemas\structs\meta_rsp.json
|
||||
schemas\structs\meta_shp.json = schemas\structs\meta_shp.json
|
||||
schemas\structs\option.json = schemas\structs\option.json
|
||||
EndProjectSection
|
||||
EndProject
|
||||
|
|
|
|||
|
|
@ -14,16 +14,18 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
|
|||
{
|
||||
private readonly CommunicatorService _communicator;
|
||||
private readonly CollectionResolver _collectionResolver;
|
||||
private readonly DrawObjectState _drawObjectState;
|
||||
private readonly CutsceneService _cutsceneService;
|
||||
private readonly ResourceLoader _resourceLoader;
|
||||
|
||||
public unsafe GameStateApi(CommunicatorService communicator, CollectionResolver collectionResolver, CutsceneService cutsceneService,
|
||||
ResourceLoader resourceLoader)
|
||||
ResourceLoader resourceLoader, DrawObjectState drawObjectState)
|
||||
{
|
||||
_communicator = communicator;
|
||||
_collectionResolver = collectionResolver;
|
||||
_cutsceneService = cutsceneService;
|
||||
_resourceLoader = resourceLoader;
|
||||
_drawObjectState = drawObjectState;
|
||||
_resourceLoader.ResourceLoaded += OnResourceLoaded;
|
||||
_resourceLoader.PapRequested += OnPapRequested;
|
||||
_communicator.CreatedCharacterBase.Subscribe(OnCreatedCharacterBase, Communication.CreatedCharacterBase.Priority.Api);
|
||||
|
|
@ -67,6 +69,30 @@ public class GameStateApi : IPenumbraApiGameState, IApiService, IDisposable
|
|||
public int GetCutsceneParentIndex(int actorIdx)
|
||||
=> _cutsceneService.GetParentIndex(actorIdx);
|
||||
|
||||
public Func<int, int> GetCutsceneParentIndexFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<CutsceneService>(_cutsceneService);
|
||||
return idx =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying cutscene state storage of this IPC container was disposed.");
|
||||
|
||||
return c.GetParentIndex(idx);
|
||||
};
|
||||
}
|
||||
|
||||
public Func<nint, nint> GetGameObjectFromDrawObjectFunc()
|
||||
{
|
||||
var weakRef = new WeakReference<DrawObjectState>(_drawObjectState);
|
||||
return model =>
|
||||
{
|
||||
if (!weakRef.TryGetTarget(out var c))
|
||||
throw new ObjectDisposedException("The underlying draw object state storage of this IPC container was disposed.");
|
||||
|
||||
return c.TryGetValue(model, out var data) ? data.Item1.Address : nint.Zero;
|
||||
};
|
||||
}
|
||||
|
||||
public PenumbraApiEc SetCutsceneParentIndex(int copyIdx, int newParentIdx)
|
||||
=> _cutsceneService.SetParentIndex(copyIdx, newParentIdx)
|
||||
? PenumbraApiEc.Success
|
||||
|
|
|
|||
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
7
Penumbra/Api/Api/IdentityChecker.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Api.Api;
|
||||
|
||||
public static class IdentityChecker
|
||||
{
|
||||
public static bool Check(string identity)
|
||||
=> true;
|
||||
}
|
||||
|
|
@ -66,6 +66,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
MetaDictionary.SerializeTo(array, cache.Rsp.Select(kvp => new KeyValuePair<RspIdentifier, RspEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Gmp.Select(kvp => new KeyValuePair<GmpIdentifier, GmpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atch.Select(kvp => new KeyValuePair<AtchIdentifier, AtchEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Shp.Select(kvp => new KeyValuePair<ShpIdentifier, ShpEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
MetaDictionary.SerializeTo(array, cache.Atr.Select(kvp => new KeyValuePair<AtrIdentifier, AtrEntry>(kvp.Key, kvp.Value.Entry)));
|
||||
}
|
||||
|
||||
return Functions.ToCompressedBase64(array, 0);
|
||||
|
|
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch);
|
||||
WriteCache(zipStream, cache.Shp);
|
||||
WriteCache(zipStream, cache.Atr);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -140,6 +144,86 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
}
|
||||
|
||||
public const uint ImcKey = ((uint)'I' << 24) | ((uint)'M' << 16) | ((uint)'C' << 8);
|
||||
public const uint EqpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'P' << 8);
|
||||
public const uint EqdpKey = ((uint)'E' << 24) | ((uint)'Q' << 16) | ((uint)'D' << 8) | 'P';
|
||||
public const uint EstKey = ((uint)'E' << 24) | ((uint)'S' << 16) | ((uint)'T' << 8);
|
||||
public const uint RspKey = ((uint)'R' << 24) | ((uint)'S' << 16) | ((uint)'P' << 8);
|
||||
public const uint GmpKey = ((uint)'G' << 24) | ((uint)'M' << 16) | ((uint)'P' << 8);
|
||||
public const uint GeqpKey = ((uint)'G' << 24) | ((uint)'E' << 16) | ((uint)'Q' << 8) | 'P';
|
||||
public const uint AtchKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'C' << 8) | 'H';
|
||||
public const uint ShpKey = ((uint)'S' << 24) | ((uint)'H' << 16) | ((uint)'P' << 8);
|
||||
public const uint AtrKey = ((uint)'A' << 24) | ((uint)'T' << 16) | ((uint)'R' << 8);
|
||||
|
||||
private static unsafe string CompressMetaManipulationsV2(ModCollection? collection)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
ms.Capacity = 1024;
|
||||
using (var zipStream = new GZipStream(ms, CompressionMode.Compress, true))
|
||||
{
|
||||
zipStream.Write((byte)2);
|
||||
zipStream.Write("META0002"u8);
|
||||
if (collection?.MetaCache is { } cache)
|
||||
{
|
||||
WriteCache(zipStream, cache.Imc, ImcKey);
|
||||
WriteCache(zipStream, cache.Eqp, EqpKey);
|
||||
WriteCache(zipStream, cache.Eqdp, EqdpKey);
|
||||
WriteCache(zipStream, cache.Est, EstKey);
|
||||
WriteCache(zipStream, cache.Rsp, RspKey);
|
||||
WriteCache(zipStream, cache.Gmp, GmpKey);
|
||||
cache.GlobalEqp.EnterReadLock();
|
||||
|
||||
try
|
||||
{
|
||||
if (cache.GlobalEqp.Count > 0)
|
||||
{
|
||||
zipStream.Write(GeqpKey);
|
||||
zipStream.Write(cache.GlobalEqp.Count);
|
||||
foreach (var (globalEqp, _) in cache.GlobalEqp)
|
||||
zipStream.Write(new ReadOnlySpan<byte>(&globalEqp, sizeof(GlobalEqpManipulation)));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
cache.GlobalEqp.ExitReadLock();
|
||||
}
|
||||
|
||||
WriteCache(zipStream, cache.Atch, AtchKey);
|
||||
WriteCache(zipStream, cache.Shp, ShpKey);
|
||||
WriteCache(zipStream, cache.Atr, AtrKey);
|
||||
}
|
||||
}
|
||||
|
||||
ms.Flush();
|
||||
ms.Position = 0;
|
||||
var data = ms.GetBuffer().AsSpan(0, (int)ms.Length);
|
||||
return Convert.ToBase64String(data);
|
||||
|
||||
void WriteCache<TKey, TValue>(Stream stream, MetaCacheBase<TKey, TValue> metaCache, uint label)
|
||||
where TKey : unmanaged, IMetaIdentifier
|
||||
where TValue : unmanaged
|
||||
{
|
||||
metaCache.EnterReadLock();
|
||||
try
|
||||
{
|
||||
if (metaCache.Count <= 0)
|
||||
return;
|
||||
|
||||
stream.Write(label);
|
||||
stream.Write(metaCache.Count);
|
||||
foreach (var (identifier, (_, value)) in metaCache)
|
||||
{
|
||||
stream.Write(identifier);
|
||||
stream.Write(value);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
metaCache.ExitReadLock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||
/// The empty string is treated as an empty set.
|
||||
|
|
@ -170,6 +254,7 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
{
|
||||
case 0: return ConvertManipsV0(data, out manips);
|
||||
case 1: return ConvertManipsV1(data, out manips);
|
||||
case 2: return ConvertManipsV2(data, out manips);
|
||||
default:
|
||||
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
||||
manips = null;
|
||||
|
|
@ -185,6 +270,131 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
}
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV2(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0002"u8))
|
||||
{
|
||||
Penumbra.Log.Debug("Invalid manipulations of version 2, does not start with valid prefix.");
|
||||
manips = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
manips = new MetaDictionary();
|
||||
var r = new SpanBinaryReader(data[8..]);
|
||||
while (r.Remaining > 4)
|
||||
{
|
||||
var prefix = r.ReadUInt32();
|
||||
var count = r.Remaining > 4 ? r.ReadInt32() : 0;
|
||||
if (count is 0)
|
||||
continue;
|
||||
|
||||
switch (prefix)
|
||||
{
|
||||
case ImcKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ImcIdentifier>();
|
||||
var value = r.Read<ImcEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqpIdentifier>();
|
||||
var value = r.Read<EqpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EqdpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EqdpIdentifier>();
|
||||
var value = r.Read<EqdpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case EstKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<EstIdentifier>();
|
||||
var value = r.Read<EstEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case RspKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<RspIdentifier>();
|
||||
var value = r.Read<RspEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GmpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GmpIdentifier>();
|
||||
var value = r.Read<GmpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case GeqpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<GlobalEqpManipulation>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtchKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtchIdentifier>();
|
||||
var value = r.Read<AtchEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case ShpKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
case AtrKey:
|
||||
for (var i = 0; i < count; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||
{
|
||||
if (!data.StartsWith("META0001"u8))
|
||||
|
|
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
|||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
// Shp and Atr was added later
|
||||
if (r.Position < r.Count)
|
||||
{
|
||||
var shpCount = r.ReadInt32();
|
||||
for (var i = 0; i < shpCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<ShpIdentifier>();
|
||||
var value = r.Read<ShpEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
|
||||
var atrCount = r.ReadInt32();
|
||||
for (var i = 0; i < atrCount; ++i)
|
||||
{
|
||||
var identifier = r.Read<AtrIdentifier>();
|
||||
var value = r.Read<AtrEntry>();
|
||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Api.Helpers;
|
||||
|
|
|
|||
|
|
@ -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,10 +108,22 @@ 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)
|
||||
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
|
||||
|
||||
var fullPath = leaf.FullName();
|
||||
|
|
@ -127,7 +138,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
|||
return PenumbraApiEc.InvalidArgument;
|
||||
|
||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|
||||
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||
return PenumbraApiEc.ModMissing;
|
||||
|
||||
try
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class PenumbraApi(
|
|||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||
{
|
||||
public const int BreakingVersion = 5;
|
||||
public const int FeatureVersion = 8;
|
||||
public const int FeatureVersion = 13;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,39 +1,38 @@
|
|||
using System.Collections.Frozen;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class PluginStateApi : IPenumbraApiPluginState, IApiService
|
||||
public class PluginStateApi(Configuration config, CommunicatorService communicator) : IPenumbraApiPluginState, IApiService
|
||||
{
|
||||
private readonly Configuration _config;
|
||||
private readonly CommunicatorService _communicator;
|
||||
|
||||
public PluginStateApi(Configuration config, CommunicatorService communicator)
|
||||
{
|
||||
_config = config;
|
||||
_communicator = communicator;
|
||||
}
|
||||
|
||||
public string GetModDirectory()
|
||||
=> _config.ModDirectory;
|
||||
=> config.ModDirectory;
|
||||
|
||||
public string GetConfiguration()
|
||||
=> JsonConvert.SerializeObject(_config, Formatting.Indented);
|
||||
=> JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||
|
||||
public event Action<string, bool>? ModDirectoryChanged
|
||||
{
|
||||
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public bool GetEnabledState()
|
||||
=> _config.EnableMods;
|
||||
=> config.EnableMods;
|
||||
|
||||
public event Action<bool>? EnabledChange
|
||||
{
|
||||
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||
remove => _communicator.EnabledChanged.Unsubscribe(value!);
|
||||
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||
remove => communicator.EnabledChanged.Unsubscribe(value!);
|
||||
}
|
||||
|
||||
public FrozenSet<string> SupportedFeatures
|
||||
=> FeatureChecker.SupportedFeatures.ToFrozenSet();
|
||||
|
||||
public string[] CheckSupportedFeatures(IEnumerable<string> requiredFeatures)
|
||||
=> requiredFeatures.Where(f => !FeatureChecker.Supported(f)).ToArray();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,57 @@
|
|||
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) : IPenumbraApiRedraw, IApiService
|
||||
namespace Penumbra.Api.Api;
|
||||
|
||||
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
|
||||
{
|
||||
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||
=> redrawService.RedrawObject(gameObjectIndex, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObjectIndex, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(string name, RedrawType setting)
|
||||
=> redrawService.RedrawObject(name, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(name, setting));
|
||||
}
|
||||
|
||||
public void RedrawObject(IGameObject? gameObject, RedrawType setting)
|
||||
=> redrawService.RedrawObject(gameObject, setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawObject(gameObject, setting));
|
||||
}
|
||||
|
||||
public void RedrawAll(RedrawType setting)
|
||||
=> redrawService.RedrawAll(setting);
|
||||
{
|
||||
framework.RunOnFrameworkThread(() => redrawService.RedrawAll(setting));
|
||||
}
|
||||
|
||||
public void RedrawCollectionMembers(Guid collectionId, RedrawType setting)
|
||||
{
|
||||
|
||||
if (!collections.Storage.ById(collectionId, out var collection))
|
||||
collection = ModCollection.Empty;
|
||||
framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
foreach (var actor in objects.Objects)
|
||||
{
|
||||
helpers.AssociatedCollection(actor.ObjectIndex, out var modCollection);
|
||||
if (collection == modCollection)
|
||||
{
|
||||
redrawService.RedrawObject(actor.ObjectIndex, setting);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||
{
|
||||
add => redrawService.GameObjectRedrawn += value;
|
||||
remove => redrawService.GameObjectRedrawn -= value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,8 +20,16 @@ public class TemporaryApi(
|
|||
ApiHelpers apiHelpers,
|
||||
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
||||
{
|
||||
public Guid CreateTemporaryCollection(string name)
|
||||
=> tempCollections.CreateTemporaryCollection(name);
|
||||
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
|
||||
{
|
||||
if (!IdentityChecker.Check(identity))
|
||||
return (PenumbraApiEc.InvalidCredentials, Guid.Empty);
|
||||
|
||||
var collection = tempCollections.CreateTemporaryCollection(name);
|
||||
if (collection == Guid.Empty)
|
||||
return (PenumbraApiEc.UnknownError, collection);
|
||||
return (PenumbraApiEc.Success, collection);
|
||||
}
|
||||
|
||||
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
|
||||
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using EmbedIO.WebApi;
|
|||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods.Settings;
|
||||
|
||||
namespace Penumbra.Api;
|
||||
|
||||
|
|
@ -13,12 +14,15 @@ public class HttpApi : IDisposable, IApiService
|
|||
private partial class Controller : WebApiController
|
||||
{
|
||||
// @formatter:off
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
||||
[Route( HttpVerbs.Post, "/focusmod" )] public partial Task FocusMod();
|
||||
[Route( HttpVerbs.Post, "/setmodsettings")] public partial Task SetModSettings();
|
||||
// @formatter:on
|
||||
}
|
||||
|
||||
|
|
@ -64,6 +68,12 @@ public class HttpApi : IDisposable, IApiService
|
|||
|
||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
||||
{
|
||||
public partial string GetModDirectory()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetModDirectory)} triggered.");
|
||||
return api.PluginState.GetModDirectory();
|
||||
}
|
||||
|
||||
public partial object? GetMods()
|
||||
{
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
||||
|
|
@ -116,6 +126,38 @@ public class HttpApi : IDisposable, IApiService
|
|||
api.Ui.OpenMainWindow(TabType.Mods, string.Empty, string.Empty);
|
||||
}
|
||||
|
||||
public async partial Task FocusMod()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<ModFocusData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(FocusMod)} triggered.");
|
||||
if (data.Path.Length != 0)
|
||||
api.Ui.OpenMainWindow(TabType.Mods, data.Path, data.Name);
|
||||
}
|
||||
|
||||
public async partial Task SetModSettings()
|
||||
{
|
||||
var data = await HttpContext.GetRequestDataAsync<SetModSettingsData>().ConfigureAwait(false);
|
||||
Penumbra.Log.Debug($"[HTTP] {nameof(SetModSettings)} triggered.");
|
||||
await framework.RunOnFrameworkThread(() =>
|
||||
{
|
||||
var collection = data.CollectionId ?? api.Collection.GetCollection(ApiCollectionType.Current)!.Value.Id;
|
||||
if (data.Inherit.HasValue)
|
||||
{
|
||||
api.ModSettings.TryInheritMod(collection, data.ModPath, data.ModName, data.Inherit.Value);
|
||||
if (data.Inherit.Value)
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.State.HasValue)
|
||||
api.ModSettings.TrySetMod(collection, data.ModPath, data.ModName, data.State.Value);
|
||||
if (data.Priority.HasValue)
|
||||
api.ModSettings.TrySetModPriority(collection, data.ModPath, data.ModName, data.Priority.Value);
|
||||
foreach (var (group, settings) in data.Settings ?? [])
|
||||
api.ModSettings.TrySetModSettings(collection, data.ModPath, data.ModName, group, settings);
|
||||
}
|
||||
).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private record ModReloadData(string Path, string Name)
|
||||
{
|
||||
public ModReloadData()
|
||||
|
|
@ -123,6 +165,13 @@ public class HttpApi : IDisposable, IApiService
|
|||
{ }
|
||||
}
|
||||
|
||||
private record ModFocusData(string Path, string Name)
|
||||
{
|
||||
public ModFocusData()
|
||||
: this(string.Empty, string.Empty)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record ModInstallData(string Path)
|
||||
{
|
||||
public ModInstallData()
|
||||
|
|
@ -136,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
|
|||
: this(string.Empty, RedrawType.Redraw, -1)
|
||||
{ }
|
||||
}
|
||||
|
||||
private record SetModSettingsData(
|
||||
Guid? CollectionId,
|
||||
string ModPath,
|
||||
string ModName,
|
||||
bool? Inherit,
|
||||
bool? State,
|
||||
int? Priority,
|
||||
Dictionary<string, List<string>>? Settings)
|
||||
{
|
||||
public SetModSettingsData()
|
||||
: this(null, string.Empty, string.Empty, null, null, null, null)
|
||||
{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,6 +40,8 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.CreatingCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.CreatedCharacterBase.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GameObjectResourcePathResolved.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetCutsceneParentIndexFunc.Provider(pi, api.GameState),
|
||||
IpcSubscribers.GetGameObjectFromDrawObjectFunc.Provider(pi, api.GameState),
|
||||
|
||||
IpcSubscribers.GetPlayerMetaManipulations.Provider(pi, api.Meta),
|
||||
IpcSubscribers.GetMetaManipulations.Provider(pi, api.Meta),
|
||||
|
|
@ -52,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),
|
||||
|
|
@ -78,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
|
|||
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.EnabledChange.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.SupportedFeatures.Provider(pi, api.PluginState),
|
||||
IpcSubscribers.CheckSupportedFeatures.Provider(pi, api.PluginState),
|
||||
|
||||
IpcSubscribers.RedrawObject.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
||||
|
||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
|||
}).ToArray();
|
||||
ImGui.OpenPopup("Changed Item List");
|
||||
}
|
||||
IpcTester.DrawIntro(RedrawCollectionMembers.Label, "Redraw Collection Members");
|
||||
if (ImGui.Button("Redraw##ObjectCollection"))
|
||||
new RedrawCollectionMembers(pi).Invoke(collectionList[0].Id, RedrawType.Redraw);
|
||||
|
||||
}
|
||||
|
||||
private void DrawChangedItemPopup()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Interface.Utility;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
using Penumbra.Api.Helpers;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
||||
|
|
@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester;
|
|||
|
||||
public class PluginStateIpcTester : IUiService, IDisposable
|
||||
{
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
private readonly IDalamudPluginInterface _pi;
|
||||
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
|
||||
public readonly EventSubscriber Initialized;
|
||||
public readonly EventSubscriber Disposed;
|
||||
|
|
@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
private readonly List<DateTimeOffset> _initializedList = [];
|
||||
private readonly List<DateTimeOffset> _disposedList = [];
|
||||
|
||||
private string _requiredFeatureString = string.Empty;
|
||||
private string[] _requiredFeatures = [];
|
||||
|
||||
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
|
||||
private bool? _lastEnabledValue;
|
||||
|
||||
|
|
@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
EnabledChange.Dispose();
|
||||
}
|
||||
|
||||
|
||||
public void Draw()
|
||||
{
|
||||
using var _ = ImRaii.TreeNode("Plugin State");
|
||||
if (!_)
|
||||
return;
|
||||
|
||||
if (ImUtf8.InputText("Required Features"u8, ref _requiredFeatureString))
|
||||
_requiredFeatures = _requiredFeatureString.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
|
||||
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||
if (!table)
|
||||
return;
|
||||
|
|
@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
|||
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
|
||||
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
|
||||
|
||||
IpcTester.DrawIntro(SupportedFeatures.Label, "Supported Features");
|
||||
ImUtf8.Text(string.Join(", ", new SupportedFeatures(_pi).Invoke()));
|
||||
|
||||
IpcTester.DrawIntro(CheckSupportedFeatures.Label, "Missing Features");
|
||||
ImUtf8.Text(string.Join(", ", new CheckSupportedFeatures(_pi).Invoke(_requiredFeatures)));
|
||||
|
||||
DrawConfigPopup();
|
||||
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
|
||||
if (ImGui.Button("Get"))
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.IpcSubscribers;
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
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;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
using Dalamud.Interface;
|
||||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Text;
|
||||
|
|
@ -37,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;
|
||||
|
|
@ -47,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);
|
||||
|
|
@ -72,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;
|
||||
|
|
@ -281,7 +284,7 @@ public class TemporaryIpcTester(
|
|||
foreach (var mod in list)
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Name);
|
||||
ImGui.TextUnformatted(mod.Name.Text);
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.TextUnformatted(mod.Priority.ToString());
|
||||
ImGui.TableNextColumn();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Plugin;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Text;
|
||||
|
||||
namespace Penumbra;
|
||||
|
|
|
|||
65
Penumbra/Collections/Cache/AtrCache.cs
Normal file
65
Penumbra/Collections/Cache/AtrCache.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class AtrCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<AtrIdentifier, AtrEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeDisabled(in ShapeAttributeString attribute, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> DisabledCount > 0 && _atrData.TryGetValue(attribute, out var value) && value.CheckEntry(slot, id, genderRace) is false;
|
||||
|
||||
public int EnabledCount { get; private set; }
|
||||
public int DisabledCount { get; private set; }
|
||||
|
||||
|
||||
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> Data
|
||||
=> _atrData;
|
||||
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _atrData = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_atrData.Clear();
|
||||
DisabledCount = 0;
|
||||
EnabledCount = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Reset();
|
||||
|
||||
protected override void ApplyModInternal(AtrIdentifier identifier, AtrEntry entry)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
{
|
||||
value = [];
|
||||
_atrData.Add(identifier.Attribute, value);
|
||||
}
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
|
||||
{
|
||||
if (entry.Value)
|
||||
++EnabledCount;
|
||||
else
|
||||
++DisabledCount;
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(AtrIdentifier identifier)
|
||||
{
|
||||
if (!_atrData.TryGetValue(identifier.Attribute, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
|
||||
{
|
||||
if (which)
|
||||
--EnabledCount;
|
||||
else
|
||||
--DisabledCount;
|
||||
if (value.IsEmpty)
|
||||
_atrData.Remove(identifier.Attribute);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -8,6 +7,7 @@ using Penumbra.Mods.Editor;
|
|||
using Penumbra.String.Classes;
|
||||
using Penumbra.Util;
|
||||
using Penumbra.GameData.Data;
|
||||
using OtterGui.Extensions;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
|
|
@ -245,6 +245,10 @@ public sealed class CollectionCache : IDisposable
|
|||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atch)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var (identifier, entry) in files.Manipulations.Atr)
|
||||
AddManipulation(mod, identifier, entry);
|
||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||
AddManipulation(mod, identifier, null!);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
|
||||
private bool _doNotHideVieraHats;
|
||||
private bool _doNotHideHrothgarHats;
|
||||
private bool _hideAuRaHorns;
|
||||
private bool _hideVieraEars;
|
||||
private bool _hideMiqoteEars;
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
|
|
@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
_doNotHideRingR.Clear();
|
||||
_doNotHideHrothgarHats = false;
|
||||
_doNotHideVieraHats = false;
|
||||
_hideAuRaHorns = false;
|
||||
_hideVieraEars = false;
|
||||
_hideMiqoteEars = false;
|
||||
}
|
||||
|
||||
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
|
||||
|
|
@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
if (_doNotHideHrothgarHats)
|
||||
original |= EqpEntry.HeadShowHrothgarHat;
|
||||
|
||||
if (_hideAuRaHorns)
|
||||
original &= ~EqpEntry.HeadShowEarAuRa;
|
||||
|
||||
if (_hideVieraEars)
|
||||
original &= ~EqpEntry.HeadShowEarViera;
|
||||
|
||||
if (_hideMiqoteEars)
|
||||
original &= ~EqpEntry.HeadShowEarMiqote;
|
||||
|
||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe | EqpEntry.HeadShowEarringsLalaElezen | EqpEntry.HeadShowEarringsMiqoHrothViera | EqpEntry.HeadShowEarringsAura;
|
||||
original |= EqpEntry.HeadShowEarringsHyurRoe
|
||||
| EqpEntry.HeadShowEarringsLalaElezen
|
||||
| EqpEntry.HeadShowEarringsMiqoHrothViera
|
||||
| EqpEntry.HeadShowEarringsAura;
|
||||
|
||||
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||
|
|
@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
|
||||
if (_doNotHideRingL.Contains(armor[9].Set))
|
||||
original |= EqpEntry.HandShowRingL;
|
||||
|
||||
return original;
|
||||
}
|
||||
|
||||
|
|
@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
|
||||
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
|
||||
GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
|
||||
GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
|
||||
GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
|
|
@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
|||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
|
||||
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
|
||||
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
|
||||
GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
|
||||
GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
|
||||
GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
|
||||
_ => false,
|
||||
};
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -16,11 +16,13 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
public readonly RspCache Rsp = new(manager, collection);
|
||||
public readonly ImcCache Imc = new(manager, collection);
|
||||
public readonly AtchCache Atch = new(manager, collection);
|
||||
public readonly ShpCache Shp = new(manager, collection);
|
||||
public readonly AtrCache Atr = new(manager, collection);
|
||||
public readonly GlobalEqpCache GlobalEqp = new();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public int Count
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + GlobalEqp.Count;
|
||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.Count + Atr.Count + GlobalEqp.Count;
|
||||
|
||||
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
|
||||
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
||||
|
|
@ -30,6 +32,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
.Concat(Rsp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Imc.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Shp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(Atr.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
|
||||
|
||||
public void Reset()
|
||||
|
|
@ -41,6 +45,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Reset();
|
||||
Imc.Reset();
|
||||
Atch.Reset();
|
||||
Shp.Reset();
|
||||
Atr.Reset();
|
||||
GlobalEqp.Clear();
|
||||
}
|
||||
|
||||
|
|
@ -57,6 +63,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
Rsp.Dispose();
|
||||
Imc.Dispose();
|
||||
Atch.Dispose();
|
||||
Shp.Dispose();
|
||||
Atr.Dispose();
|
||||
}
|
||||
|
||||
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||
|
|
@ -71,6 +79,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
RspIdentifier i => Rsp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
ShpIdentifier i => Shp.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
AtrIdentifier i => Atr.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -92,6 +102,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i => Imc.RevertMod(i, out mod),
|
||||
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
||||
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
||||
ShpIdentifier i => Shp.RevertMod(i, out mod),
|
||||
AtrIdentifier i => Atr.RevertMod(i, out mod),
|
||||
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
|
||||
_ => (mod = null) != null,
|
||||
};
|
||||
|
|
@ -108,6 +120,8 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
|||
ImcIdentifier i when entry is ImcEntry e => Imc.ApplyMod(mod, i, e),
|
||||
RspIdentifier i when entry is RspEntry e => Rsp.ApplyMod(mod, i, e),
|
||||
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
|
||||
ShpIdentifier i when entry is ShpEntry e => Shp.ApplyMod(mod, i, e),
|
||||
AtrIdentifier i when entry is AtrEntry e => Atr.ApplyMod(mod, i, e),
|
||||
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
|
||||
_ => false,
|
||||
};
|
||||
|
|
|
|||
181
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal file
181
Penumbra/Collections/Cache/ShapeAttributeHashSet.cs
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
using System.Collections.Frozen;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class ShapeAttributeHashSet : Dictionary<(HumanSlot Slot, PrimaryId Id), ulong>
|
||||
{
|
||||
public static readonly IReadOnlyList<GenderRace> GenderRaceValues =
|
||||
[
|
||||
GenderRace.Unknown, GenderRace.MidlanderMale, GenderRace.MidlanderFemale, GenderRace.HighlanderMale, GenderRace.HighlanderFemale,
|
||||
GenderRace.ElezenMale, GenderRace.ElezenFemale, GenderRace.MiqoteMale, GenderRace.MiqoteFemale, GenderRace.RoegadynMale,
|
||||
GenderRace.RoegadynFemale, GenderRace.LalafellMale, GenderRace.LalafellFemale, GenderRace.AuRaMale, GenderRace.AuRaFemale,
|
||||
GenderRace.HrothgarMale, GenderRace.HrothgarFemale, GenderRace.VieraMale, GenderRace.VieraFemale,
|
||||
];
|
||||
|
||||
public static readonly FrozenDictionary<GenderRace, int> GenderRaceIndices =
|
||||
GenderRaceValues.WithIndex().ToFrozenDictionary(p => p.Value, p => p.Index);
|
||||
|
||||
private readonly BitArray _allIds = new(2 * (ShapeAttributeManager.ModelSlotSize + 1) * GenderRaceValues.Count);
|
||||
|
||||
public bool? this[HumanSlot slot]
|
||||
=> AllCheck(ToIndex(slot, 0));
|
||||
|
||||
public bool? this[GenderRace genderRace]
|
||||
=> ToIndex(HumanSlot.Unknown, genderRace, out var index) ? AllCheck(index) : null;
|
||||
|
||||
public bool? this[HumanSlot slot, GenderRace genderRace]
|
||||
=> ToIndex(slot, genderRace, out var index) ? AllCheck(index) : null;
|
||||
|
||||
public bool? All
|
||||
=> Convert(_allIds[2 * AllIndex], _allIds[2 * AllIndex + 1]);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private bool? AllCheck(int idx)
|
||||
=> Convert(_allIds[idx], _allIds[idx + 1]);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static int ToIndex(HumanSlot slot, int genderRaceIndex)
|
||||
=> 2 * (slot is HumanSlot.Unknown ? genderRaceIndex + AllIndex : genderRaceIndex + (int)slot * GenderRaceValues.Count);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveOptimization)]
|
||||
public bool? CheckEntry(HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
{
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
|
||||
return null;
|
||||
|
||||
// Check for specific ID.
|
||||
if (TryGetValue((slot, id), out var flags))
|
||||
{
|
||||
// Check completely specified entry.
|
||||
if (Convert(flags, 2 * index) is { } specified)
|
||||
return specified;
|
||||
|
||||
// Check any gender / race.
|
||||
if (Convert(flags, 0) is { } anyGr)
|
||||
return anyGr;
|
||||
}
|
||||
|
||||
// Check for specified gender / race and slot, but no ID.
|
||||
if (AllCheck(ToIndex(slot, index)) is { } noIdButGr)
|
||||
return noIdButGr;
|
||||
|
||||
// Check for specified gender / race but no slot or ID.
|
||||
if (AllCheck(ToIndex(HumanSlot.Unknown, index)) is { } noSlotButGr)
|
||||
return noSlotButGr;
|
||||
|
||||
// Check for specified slot but no gender / race or ID.
|
||||
if (AllCheck(ToIndex(slot, 0)) is { } noGrButSlot)
|
||||
return noGrButSlot;
|
||||
|
||||
return All;
|
||||
}
|
||||
|
||||
public bool TrySet(HumanSlot slot, PrimaryId? id, GenderRace genderRace, bool? value, out bool which)
|
||||
{
|
||||
which = false;
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out var index))
|
||||
return false;
|
||||
|
||||
if (!id.HasValue)
|
||||
{
|
||||
var slotIndex = ToIndex(slot, index);
|
||||
var ret = false;
|
||||
if (value is true)
|
||||
{
|
||||
if (!_allIds[slotIndex])
|
||||
ret = true;
|
||||
_allIds[slotIndex] = true;
|
||||
_allIds[slotIndex + 1] = false;
|
||||
}
|
||||
else if (value is false)
|
||||
{
|
||||
if (!_allIds[slotIndex + 1])
|
||||
ret = true;
|
||||
_allIds[slotIndex] = false;
|
||||
_allIds[slotIndex + 1] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_allIds[slotIndex])
|
||||
{
|
||||
which = true;
|
||||
ret = true;
|
||||
}
|
||||
else if (_allIds[slotIndex + 1])
|
||||
{
|
||||
which = false;
|
||||
ret = true;
|
||||
}
|
||||
|
||||
_allIds[slotIndex] = false;
|
||||
_allIds[slotIndex + 1] = false;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
if (TryGetValue((slot, id.Value), out var flags))
|
||||
{
|
||||
index *= 2;
|
||||
var newFlags = value switch
|
||||
{
|
||||
true => (flags | (1ul << index)) & ~(1ul << (index + 1)),
|
||||
false => (flags & ~(1ul << index)) | (1ul << (index + 1)),
|
||||
_ => flags & ~(1ul << index) & ~(1ul << (index + 1)),
|
||||
};
|
||||
if (newFlags == flags)
|
||||
return false;
|
||||
|
||||
this[(slot, id.Value)] = newFlags;
|
||||
which = (flags & (1ul << index)) is not 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (value is null)
|
||||
return false;
|
||||
|
||||
this[(slot, id.Value)] = 1ul << (2 * index + (value.Value ? 0 : 1));
|
||||
return true;
|
||||
}
|
||||
|
||||
public new void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
_allIds.SetAll(false);
|
||||
}
|
||||
|
||||
public bool IsEmpty
|
||||
=> !_allIds.HasAnySet() && Count is 0;
|
||||
|
||||
private static readonly int AllIndex = ShapeAttributeManager.ModelSlotSize * GenderRaceValues.Count;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool ToIndex(HumanSlot slot, GenderRace genderRace, out int index)
|
||||
{
|
||||
if (!GenderRaceIndices.TryGetValue(genderRace, out index))
|
||||
return false;
|
||||
|
||||
index = ToIndex(slot, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool? Convert(bool trueValue, bool falseValue)
|
||||
=> trueValue ? true : falseValue ? false : null;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private static bool? Convert(ulong mask, int idx)
|
||||
{
|
||||
mask >>= idx;
|
||||
return (mask & 3) switch
|
||||
{
|
||||
1 => true,
|
||||
2 => false,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
106
Penumbra/Collections/Cache/ShpCache.cs
Normal file
106
Penumbra/Collections/Cache/ShpCache.cs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Collections.Cache;
|
||||
|
||||
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
|
||||
{
|
||||
public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true;
|
||||
|
||||
public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||
=> DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false;
|
||||
|
||||
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> State(ShapeConnectorCondition connector)
|
||||
=> connector switch
|
||||
{
|
||||
ShapeConnectorCondition.None => _shpData,
|
||||
ShapeConnectorCondition.Wrists => _wristConnectors,
|
||||
ShapeConnectorCondition.Waist => _waistConnectors,
|
||||
ShapeConnectorCondition.Ankles => _ankleConnectors,
|
||||
_ => [],
|
||||
};
|
||||
|
||||
public int EnabledCount { get; private set; }
|
||||
public int DisabledCount { get; private set; }
|
||||
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _wristConnectors = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _waistConnectors = [];
|
||||
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _ankleConnectors = [];
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Clear();
|
||||
_shpData.Clear();
|
||||
_wristConnectors.Clear();
|
||||
_waistConnectors.Clear();
|
||||
_ankleConnectors.Clear();
|
||||
EnabledCount = 0;
|
||||
DisabledCount = 0;
|
||||
}
|
||||
|
||||
protected override void Dispose(bool _)
|
||||
=> Reset();
|
||||
|
||||
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
||||
{
|
||||
switch (identifier.ConnectorCondition)
|
||||
{
|
||||
case ShapeConnectorCondition.None: Func(_shpData); break;
|
||||
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
|
||||
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
|
||||
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||
{
|
||||
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||
{
|
||||
value = [];
|
||||
dict.Add(identifier.Shape, value);
|
||||
}
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
|
||||
{
|
||||
if (entry.Value)
|
||||
++EnabledCount;
|
||||
else
|
||||
++DisabledCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void RevertModInternal(ShpIdentifier identifier)
|
||||
{
|
||||
switch (identifier.ConnectorCondition)
|
||||
{
|
||||
case ShapeConnectorCondition.None: Func(_shpData); break;
|
||||
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
|
||||
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
|
||||
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||
{
|
||||
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||
return;
|
||||
|
||||
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
|
||||
{
|
||||
if (which)
|
||||
--EnabledCount;
|
||||
else
|
||||
--DisabledCount;
|
||||
if (value.IsEmpty)
|
||||
dict.Remove(identifier.Shape);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
|
|||
return;
|
||||
|
||||
var collection = _resolver.PlayerCollection();
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
if (collection.Identity.Id == Guid.Empty)
|
||||
{
|
||||
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||
_collections.SetCollection(collection, CollectionType.Current);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData.Actors;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.Mods.Manager;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api;
|
||||
using Penumbra.Communication;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
||||
namespace Penumbra.Collections;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Game.Command;
|
||||
using Dalamud.Game.Text.SeStringHandling;
|
||||
using Dalamud.Plugin.Services;
|
||||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Api;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api;
|
|||
using Penumbra.Api.Api;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.Manager;
|
||||
using Penumbra.Services;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
|
|
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
|
|||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="PcpService.OnModPathChange"/>
|
||||
PcpService = int.MinValue,
|
||||
|
||||
/// <seealso cref="ModsApi.OnModPathChange"/>
|
||||
ApiMods = int.MinValue,
|
||||
ApiMods = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
||||
ApiModSettings = int.MinValue,
|
||||
ApiModSettings = int.MinValue + 1,
|
||||
|
||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||
EphemeralConfig = -500,
|
||||
|
|
|
|||
21
Penumbra/Communication/PcpCreation.cs
Normal file
21
Penumbra/Communication/PcpCreation.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the character.json file for a .pcp file is written.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the JObject that gets written to file. </item>
|
||||
/// <item>Parameter is the object index of the game object this is written for. </item>
|
||||
/// <item>Parameter is the full path to the directory being set up for the PCP creation. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PcpCreation() : EventWrapper<JObject, ushort, string, PcpCreation.Priority>(nameof(PcpCreation))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.Api.ModsApi"/>
|
||||
ModsApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
21
Penumbra/Communication/PcpParsing.cs
Normal file
21
Penumbra/Communication/PcpParsing.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
using OtterGui.Classes;
|
||||
|
||||
namespace Penumbra.Communication;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered when the character.json file for a .pcp file is parsed and applied.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is parsed JObject that contains the data. </item>
|
||||
/// <item>Parameter is the identifier of the created mod. </item>
|
||||
/// <item>Parameter is the GUID of the created collection. </item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
public sealed class PcpParsing() : EventWrapper<JObject, string, Guid, PcpParsing.Priority>(nameof(PcpParsing))
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="Api.Api.ModsApi"/>
|
||||
ModsApi = int.MinValue,
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
using Dalamud.Configuration;
|
||||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Filesystem;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Widgets;
|
||||
|
|
@ -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,9 +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 RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||
public bool EnableDirectoryWatch { get; set; } = false;
|
||||
public bool EnableAutomaticModImport { get; set; } = false;
|
||||
public bool EnableCustomShapes { get; set; } = true;
|
||||
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);
|
||||
|
||||
|
|
@ -84,9 +98,6 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
[JsonProperty(Order = int.MaxValue)]
|
||||
public ISortMode<Mod> SortMode = ISortMode<Mod>.FoldersFirst;
|
||||
|
||||
public bool ScaleModSelector { get; set; } = false;
|
||||
public float ModSelectorAbsoluteSize { get; set; } = Constants.DefaultAbsoluteSize;
|
||||
public int ModSelectorScaledSize { get; set; } = Constants.DefaultScaledSize;
|
||||
public bool OpenFoldersByDefault { get; set; } = false;
|
||||
public int SingleGroupRadioMax { get; set; } = 2;
|
||||
public string DefaultImportFolder { get; set; } = string.Empty;
|
||||
|
|
@ -94,6 +105,7 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
|||
public string QuickMoveFolder2 { get; set; } = string.Empty;
|
||||
public string QuickMoveFolder3 { get; set; } = string.Empty;
|
||||
public DoubleModifier DeleteModModifier { get; set; } = new(ModifierHotkey.Control, ModifierHotkey.Shift);
|
||||
public DoubleModifier IncognitoModifier { get; set; } = new(ModifierHotkey.Control);
|
||||
public bool PrintSuccessfulCommandsToChat { get; set; } = true;
|
||||
public bool AutoDeduplicateOnImport { get; set; } = true;
|
||||
public bool AutoReduplicateUiOnImport { get; set; } = true;
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ namespace Penumbra;
|
|||
|
||||
public class DebugConfiguration
|
||||
{
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool WriteImcBytesToLog = false;
|
||||
public static bool UseSkinMaterialProcessing = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Dalamud.Interface.ImGuiNotification;
|
||||
using Newtonsoft.Json;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.FileSystem.Selector;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
|
|
@ -23,6 +24,10 @@ public class EphemeralConfig : ISavable, IDisposable, IService
|
|||
[JsonIgnore]
|
||||
private readonly ModPathChanged _modPathChanged;
|
||||
|
||||
public float CurrentModSelectorWidth { get; set; } = 200f;
|
||||
public float ModSelectorMinimumScale { get; set; } = 0.1f;
|
||||
public float ModSelectorMaximumScale { get; set; } = 0.5f;
|
||||
|
||||
public int Version { get; set; } = Configuration.Constants.CurrentVersion;
|
||||
public int LastSeenVersion { get; set; } = PenumbraChangelog.LastChangelogVersion;
|
||||
public bool DebugSeparateWindow { get; set; } = false;
|
||||
|
|
@ -41,6 +46,7 @@ public class EphemeralConfig : ISavable, IDisposable, IService
|
|||
public string LastModPath { get; set; } = string.Empty;
|
||||
public bool AdvancedEditingOpen { get; set; } = false;
|
||||
public bool ForceRedrawOnFileChange { get; set; } = false;
|
||||
public bool IncognitoMode { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Load the current configuration.
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using System.Collections.Immutable;
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Lumina.Extensions;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Geometry;
|
||||
|
|
@ -360,11 +360,11 @@ public class MeshExporter
|
|||
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
|
||||
// TODO: While this assumption is safe, it would be sensible to actually check.
|
||||
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
|
||||
|
||||
|
||||
return new VertexPositionNormalTangent(
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
|
||||
bitangent
|
||||
bitangent.SanitizeTangent()
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -390,23 +390,30 @@ public class MeshExporter
|
|||
}
|
||||
}
|
||||
|
||||
usages.TryGetValue(MdlFile.VertexUsage.Color, out var colours);
|
||||
var nColors = colours?.Count ?? 0;
|
||||
|
||||
var materialUsages = (
|
||||
uvCount,
|
||||
usages.ContainsKey(MdlFile.VertexUsage.Color)
|
||||
nColors
|
||||
);
|
||||
|
||||
return materialUsages switch
|
||||
{
|
||||
(3, true) => typeof(VertexTexture3ColorFfxiv),
|
||||
(3, false) => typeof(VertexTexture3),
|
||||
(2, true) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, false) => typeof(VertexTexture2),
|
||||
(1, true) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, false) => typeof(VertexTexture1),
|
||||
(0, true) => typeof(VertexColorFfxiv),
|
||||
(0, false) => typeof(VertexEmpty),
|
||||
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
|
||||
(3, 1) => typeof(VertexTexture3ColorFfxiv),
|
||||
(3, 0) => typeof(VertexTexture3),
|
||||
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
|
||||
(2, 1) => typeof(VertexTexture2ColorFfxiv),
|
||||
(2, 0) => typeof(VertexTexture2),
|
||||
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
|
||||
(1, 1) => typeof(VertexTexture1ColorFfxiv),
|
||||
(1, 0) => typeof(VertexTexture1),
|
||||
(0, 2) => typeof(VertexColor2Ffxiv),
|
||||
(0, 1) => typeof(VertexColorFfxiv),
|
||||
(0, 0) => typeof(VertexEmpty),
|
||||
|
||||
_ => throw _notifier.Exception($"Unhandled UV count of {uvCount} encountered."),
|
||||
_ => throw _notifier.Exception($"Unhandled UV/color count of {uvCount}/{nColors} encountered."),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -419,6 +426,12 @@ public class MeshExporter
|
|||
if (_materialType == typeof(VertexColorFfxiv))
|
||||
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
|
||||
|
||||
if (_materialType == typeof(VertexColor2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexColor2Ffxiv(ToVector4(color0), ToVector4(color1));
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture1))
|
||||
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
|
||||
|
||||
|
|
@ -428,6 +441,16 @@ public class MeshExporter
|
|||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
|
||||
if (_materialType == typeof(VertexTexture1Color2Ffxiv))
|
||||
{
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
return new VertexTexture1Color2Ffxiv(
|
||||
ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
// XIV packs two UVs into a single vec4 attribute.
|
||||
|
||||
if (_materialType == typeof(VertexTexture2))
|
||||
|
|
@ -448,6 +471,20 @@ public class MeshExporter
|
|||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture2Color2Ffxiv))
|
||||
{
|
||||
var uv = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.UV));
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture2Color2Ffxiv(
|
||||
new Vector2(uv.X, uv.Y),
|
||||
new Vector2(uv.Z, uv.W),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3))
|
||||
{
|
||||
// Not 100% sure about this
|
||||
|
|
@ -472,6 +509,21 @@ public class MeshExporter
|
|||
);
|
||||
}
|
||||
|
||||
if (_materialType == typeof(VertexTexture3Color2Ffxiv))
|
||||
{
|
||||
var uv0 = ToVector4(attributes[MdlFile.VertexUsage.UV][0]);
|
||||
var uv1 = ToVector4(attributes[MdlFile.VertexUsage.UV][1]);
|
||||
var (color0, color1) = GetBothSafe(attributes, MdlFile.VertexUsage.Color);
|
||||
|
||||
return new VertexTexture3Color2Ffxiv(
|
||||
new Vector2(uv0.X, uv0.Y),
|
||||
new Vector2(uv0.Z, uv0.W),
|
||||
new Vector2(uv1.X, uv1.Y),
|
||||
ToVector4(color0),
|
||||
ToVector4(color1)
|
||||
);
|
||||
}
|
||||
|
||||
throw _notifier.Exception($"Unknown material type {_skinningType}");
|
||||
}
|
||||
|
||||
|
|
@ -537,6 +589,17 @@ public class MeshExporter
|
|||
|
||||
return list[0];
|
||||
}
|
||||
|
||||
/// <summary> Check that the list has length 2 for any case where this is expected and return both entries. </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
private (T First, T Second) GetBothSafe<T>(IReadOnlyDictionary<MdlFile.VertexUsage, List<T>> attributes, MdlFile.VertexUsage usage)
|
||||
{
|
||||
var list = attributes[usage];
|
||||
if (list.Count != 2)
|
||||
throw _notifier.Exception($"{list.Count} usage indices encountered for {usage}, but expected 2.");
|
||||
|
||||
return (list[0], list[1]);
|
||||
}
|
||||
|
||||
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||
private static Vector2 ToVector2(object data)
|
||||
|
|
|
|||
|
|
@ -84,6 +84,103 @@ public struct VertexColorFfxiv(Vector4 ffxivColor) : IVertexCustom
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexColor2Ffxiv(Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 0;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{ }
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, Vector2.Zero, Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{ }
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
|
|
@ -172,6 +269,118 @@ public struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) :
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture1Color2Ffxiv(Vector2 texCoord0, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 1;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), Vector2.Zero);
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex >= 1)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
|
|
@ -266,6 +475,124 @@ public struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
|
|||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture2Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor0, Vector4 ffxivColor1) : IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 2;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex >= 2)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
|
||||
: IVertexCustom
|
||||
{
|
||||
|
|
@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
|
|||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||
}
|
||||
}
|
||||
|
||||
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
|
||||
: IVertexCustom
|
||||
{
|
||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||
{
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||
}
|
||||
|
||||
public Vector2 TexCoord0 = texCoord0;
|
||||
public Vector2 TexCoord1 = texCoord1;
|
||||
public Vector2 TexCoord2 = texCoord2;
|
||||
public Vector4 FfxivColor0 = ffxivColor0;
|
||||
public Vector4 FfxivColor1 = ffxivColor1;
|
||||
|
||||
public int MaxColors
|
||||
=> 0;
|
||||
|
||||
public int MaxTextCoords
|
||||
=> 3;
|
||||
|
||||
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||
|
||||
public IEnumerable<string> CustomAttributes
|
||||
=> CustomNames;
|
||||
|
||||
public void Add(in VertexMaterialDelta delta)
|
||||
{
|
||||
TexCoord0 += delta.TexCoord0Delta;
|
||||
TexCoord1 += delta.TexCoord1Delta;
|
||||
TexCoord2 += delta.TexCoord2Delta;
|
||||
}
|
||||
|
||||
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||
|
||||
public Vector2 GetTexCoord(int index)
|
||||
=> index switch
|
||||
{
|
||||
0 => TexCoord0,
|
||||
1 => TexCoord1,
|
||||
2 => TexCoord2,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||
};
|
||||
|
||||
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||
{
|
||||
if (setIndex == 0)
|
||||
TexCoord0 = coord;
|
||||
if (setIndex == 1)
|
||||
TexCoord1 = coord;
|
||||
if (setIndex == 2)
|
||||
TexCoord2 = coord;
|
||||
if (setIndex >= 3)
|
||||
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||
}
|
||||
|
||||
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0":
|
||||
value = FfxivColor0;
|
||||
return true;
|
||||
|
||||
case "_FFXIV_COLOR_1":
|
||||
value = FfxivColor1;
|
||||
return true;
|
||||
|
||||
default:
|
||||
value = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void SetCustomAttribute(string attributeName, object value)
|
||||
{
|
||||
switch (attributeName)
|
||||
{
|
||||
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||
FfxivColor0 = valueVector4;
|
||||
break;
|
||||
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||
FfxivColor1 = valueVector4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public Vector4 GetColor(int index)
|
||||
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||
|
||||
public void SetColor(int setIndex, Vector4 color)
|
||||
{ }
|
||||
|
||||
public void Validate()
|
||||
{
|
||||
var components = new[]
|
||||
{
|
||||
FfxivColor0.X,
|
||||
FfxivColor0.Y,
|
||||
FfxivColor0.Z,
|
||||
FfxivColor0.W,
|
||||
};
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||
components =
|
||||
[
|
||||
FfxivColor1.X,
|
||||
FfxivColor1.Y,
|
||||
FfxivColor1.Z,
|
||||
FfxivColor1.W,
|
||||
];
|
||||
if (components.Any(component => component is < 0 or > 1))
|
||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Files.ModelStructs;
|
||||
using SharpGLTF.Schema2;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
using System.Text.Json;
|
||||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using SharpGLTF.Schema2;
|
||||
|
||||
namespace Penumbra.Import.Models.Import;
|
||||
|
|
|
|||
|
|
@ -319,7 +319,7 @@ public class VertexAttribute
|
|||
|
||||
var normals = normalAccessor.AsVector3Array();
|
||||
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
||||
? accessor.AsVector4Array()
|
||||
? accessor.AsVector4Array().ToArray()
|
||||
: CalculateTangents(accessors, indices, normals, notifier);
|
||||
|
||||
if (tangents == null)
|
||||
|
|
|
|||
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
namespace Penumbra.Import.Models;
|
||||
|
||||
public static class ModelExtensions
|
||||
{
|
||||
// https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158
|
||||
private const float UnitLengthThresholdVec3 = 0.00674f;
|
||||
private const float UnitLengthThresholdVec4 = 0.00769f;
|
||||
|
||||
internal static bool _IsFinite(this float value)
|
||||
{
|
||||
return float.IsFinite(value);
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this Vector2 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite();
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this Vector3 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite();
|
||||
}
|
||||
|
||||
internal static bool _IsFinite(this in Vector4 v)
|
||||
{
|
||||
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
|
||||
}
|
||||
|
||||
internal static Boolean IsNormalized(this Vector3 normal)
|
||||
{
|
||||
if (!normal._IsFinite()) return false;
|
||||
|
||||
return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3;
|
||||
}
|
||||
|
||||
internal static void ValidateNormal(this Vector3 normal, string msg)
|
||||
{
|
||||
if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
|
||||
|
||||
if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length.");
|
||||
}
|
||||
|
||||
internal static void ValidateTangent(this Vector4 tangent, string msg)
|
||||
{
|
||||
if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg);
|
||||
|
||||
new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg);
|
||||
}
|
||||
|
||||
internal static Vector3 SanitizeNormal(this Vector3 normal)
|
||||
{
|
||||
if (normal == Vector3.Zero) return Vector3.UnitX;
|
||||
return normal.IsNormalized() ? normal : Vector3.Normalize(normal);
|
||||
}
|
||||
|
||||
internal static bool IsValidTangent(this Vector4 tangent)
|
||||
{
|
||||
if (tangent.W != 1 && tangent.W != -1) return false;
|
||||
|
||||
return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized();
|
||||
}
|
||||
|
||||
internal static Vector4 SanitizeTangent(this Vector4 tangent)
|
||||
{
|
||||
var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal();
|
||||
var s = float.IsNaN(tangent.W) ? 1 : tangent.W;
|
||||
return new Vector4(n, s > 0 ? 1 : -1);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using Lumina.Data.Parsing;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using OtterGui.Tasks;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using System.Xml;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Import.Models.Export;
|
||||
|
||||
namespace Penumbra.Import.Models;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
|||
using OtterGui.Filesystem;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Services;
|
||||
using SharpCompress.Archives;
|
||||
using SharpCompress.Archives.Rar;
|
||||
using SharpCompress.Archives.SevenZip;
|
||||
|
|
@ -146,6 +147,9 @@ public partial class TexToolsImporter
|
|||
case ".mtrl":
|
||||
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
case ".tex":
|
||||
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
default:
|
||||
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
|
||||
break;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using Penumbra.Import.Structs;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Newtonsoft.Json;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Mods;
|
||||
|
|
@ -259,6 +259,7 @@ public partial class TexToolsImporter
|
|||
{
|
||||
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
|
||||
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
|
||||
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
|
||||
_ => data.Data,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Lumina.Extensions;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Import.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Import;
|
||||
|
|
@ -19,9 +18,7 @@ public partial class TexToolsMeta
|
|||
|
||||
var identifier = new EqpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot);
|
||||
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data) & mask;
|
||||
var def = ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId) & mask;
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
|
||||
// Deserialize and check Eqdp Entries and add them to the list if they are non-default.
|
||||
|
|
@ -41,11 +38,9 @@ public partial class TexToolsMeta
|
|||
continue;
|
||||
|
||||
var identifier = new EqdpIdentifier(metaFileInfo.PrimaryId, metaFileInfo.EquipSlot, gr);
|
||||
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
|
||||
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
|
||||
var def = ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId) & mask;
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
var mask = Eqdp.Mask(metaFileInfo.EquipSlot);
|
||||
var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2) & mask;
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -55,10 +50,9 @@ public partial class TexToolsMeta
|
|||
if (data == null)
|
||||
return;
|
||||
|
||||
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
|
||||
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
|
||||
if (_keepDefault || value != def)
|
||||
MetaManipulations.TryAdd(new GmpIdentifier(metaFileInfo.PrimaryId), value);
|
||||
var value = GmpEntry.FromTexToolsMeta(data.AsSpan(0, 5));
|
||||
var identifier = new GmpIdentifier(metaFileInfo.PrimaryId);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
|
||||
// Deserialize and check Est Entries and add them to the list if they are non-default.
|
||||
|
|
@ -86,9 +80,7 @@ public partial class TexToolsMeta
|
|||
continue;
|
||||
|
||||
var identifier = new EstIdentifier(id, type, gr);
|
||||
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
|
||||
if (_keepDefault || def != value)
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -108,15 +100,10 @@ public partial class TexToolsMeta
|
|||
{
|
||||
var identifier = new ImcIdentifier(metaFileInfo.PrimaryId, 0, metaFileInfo.PrimaryType, metaFileInfo.SecondaryId,
|
||||
metaFileInfo.EquipSlot, metaFileInfo.SecondaryType);
|
||||
var file = new ImcFile(_metaFileManager, identifier);
|
||||
var partIdx = ImcFile.PartIndex(identifier.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
|
||||
foreach (var value in values)
|
||||
{
|
||||
identifier = identifier with { Variant = (Variant)i };
|
||||
var def = file.GetEntry(partIdx, (Variant)i);
|
||||
if (_keepDefault || def != value && identifier.Validate())
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
|
||||
MetaManipulations.TryAdd(identifier, value);
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Meta;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Import;
|
||||
|
|
@ -8,7 +7,7 @@ namespace Penumbra.Import;
|
|||
public partial class TexToolsMeta
|
||||
{
|
||||
// Parse a single rgsp file.
|
||||
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data, bool keepDefault)
|
||||
public static TexToolsMeta FromRgspFile(MetaFileManager manager, string filePath, byte[] data)
|
||||
{
|
||||
if (data.Length != 45 && data.Length != 42)
|
||||
{
|
||||
|
|
@ -70,9 +69,7 @@ public partial class TexToolsMeta
|
|||
void Add(RspAttribute attribute, float value)
|
||||
{
|
||||
var identifier = new RspIdentifier(subRace, attribute);
|
||||
var def = CmpFile.GetDefault(manager, subRace, attribute);
|
||||
if (keepDefault || value != def.Value)
|
||||
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
|
||||
ret.MetaManipulations.TryAdd(identifier, new RspEntry(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,15 +23,11 @@ public partial class TexToolsMeta
|
|||
// The info class determines the files or table locations the changes need to apply to from the filename.
|
||||
public readonly uint Version;
|
||||
public readonly string FilePath;
|
||||
public readonly MetaDictionary MetaManipulations = new();
|
||||
private readonly bool _keepDefault;
|
||||
public readonly MetaDictionary MetaManipulations = new();
|
||||
|
||||
private readonly MetaFileManager _metaFileManager;
|
||||
|
||||
public TexToolsMeta(MetaFileManager metaFileManager, GamePathParser parser, byte[] data, bool keepDefault)
|
||||
public TexToolsMeta(GamePathParser parser, byte[] data)
|
||||
{
|
||||
_metaFileManager = metaFileManager;
|
||||
_keepDefault = keepDefault;
|
||||
try
|
||||
{
|
||||
using var reader = new BinaryReader(new MemoryStream(data));
|
||||
|
|
@ -79,7 +75,6 @@ public partial class TexToolsMeta
|
|||
|
||||
private TexToolsMeta(MetaFileManager metaFileManager, string filePath, uint version)
|
||||
{
|
||||
_metaFileManager = metaFileManager;
|
||||
FilePath = filePath;
|
||||
Version = version;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
using ImGuiNET;
|
||||
using Dalamud.Bindings.ImGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
|
|
|
|||
|
|
@ -61,6 +61,76 @@ public static class TexFileParser
|
|||
return 13;
|
||||
}
|
||||
|
||||
public static unsafe void FixMipOffsets(long size, ref TexFile.TexHeader header, out long newSize)
|
||||
{
|
||||
var width = (uint)header.Width;
|
||||
var height = (uint)header.Height;
|
||||
var format = header.Format.ToDXGI();
|
||||
var bits = format.BitsPerPixel();
|
||||
var totalSize = 80u;
|
||||
size -= totalSize;
|
||||
var minSize = format.IsCompressed() ? 4u : 1u;
|
||||
for (var i = 0; i < 13; ++i)
|
||||
{
|
||||
var requiredSize = (uint)((long)width * height * bits / 8);
|
||||
if (requiredSize > size)
|
||||
{
|
||||
newSize = totalSize;
|
||||
if (header.MipCount != i)
|
||||
{
|
||||
Penumbra.Log.Debug(
|
||||
$"-- Mip Map Count in TEX header was {header.MipCount}, but file only contains data for {i} Mip Maps, fixed.");
|
||||
FixLodOffsets(ref header, i);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (header.OffsetToSurface[i] != totalSize)
|
||||
{
|
||||
Penumbra.Log.Debug(
|
||||
$"-- Mip Map Offset {i + 1} in TEX header was {header.OffsetToSurface[i]} but should be {totalSize}, fixed.");
|
||||
header.OffsetToSurface[i] = totalSize;
|
||||
}
|
||||
|
||||
if (width == minSize && height == minSize)
|
||||
{
|
||||
++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.");
|
||||
FixLodOffsets(ref header, i);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
totalSize += requiredSize;
|
||||
size -= requiredSize;
|
||||
width = Math.Max(width / 2, minSize);
|
||||
height = Math.Max(height / 2, minSize);
|
||||
}
|
||||
|
||||
newSize = totalSize;
|
||||
if (header.MipCount != 13)
|
||||
{
|
||||
Penumbra.Log.Debug($"-- Mip Map Count in TEX header was {header.MipCount}, but maximum is 13, fixed.");
|
||||
FixLodOffsets(ref header, 13);
|
||||
}
|
||||
|
||||
void FixLodOffsets(ref TexFile.TexHeader header, int index)
|
||||
{
|
||||
header.MipCount = index;
|
||||
if (header.LodOffset[2] >= header.MipCount)
|
||||
header.LodOffset[2] = (byte)(header.MipCount - 1);
|
||||
if (header.LodOffset[1] >= header.MipCount)
|
||||
header.LodOffset[1] = header.MipCount > 2 ? (byte)(header.MipCount - 2) : (byte)(header.MipCount - 1);
|
||||
for (++index; index < 13; ++index)
|
||||
header.OffsetToSurface[index] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe void CopyData(ScratchImage image, BinaryReader r)
|
||||
{
|
||||
fixed (byte* ptr = image.Pixels)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
using Dalamud.Bindings.ImGui;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Data.Files;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
|
|
@ -20,7 +20,7 @@ public static class TextureDrawer
|
|||
{
|
||||
size = texture.TextureWrap.Size.Contain(size);
|
||||
|
||||
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
|
||||
ImGui.Image(texture.TextureWrap.Handle, size);
|
||||
DrawData(texture);
|
||||
}
|
||||
else if (texture.LoadError != null)
|
||||
|
|
|
|||
|
|
@ -406,7 +406,7 @@ public sealed class TextureManager(IDataManager gameData, Logger logger, ITextur
|
|||
// See https://github.com/microsoft/DirectXTex/wiki/Compress#parameters for the format condition.
|
||||
if (format is DXGIFormat.BC6HUF16 or DXGIFormat.BC6HSF16 or DXGIFormat.BC7UNorm or DXGIFormat.BC7UNormSRGB)
|
||||
{
|
||||
var device = uiBuilder.Device;
|
||||
var device = new Device(uiBuilder.DeviceHandle);
|
||||
var dxgiDevice = device.QueryInterface<DxgiDevice>();
|
||||
|
||||
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);
|
||||
|
|
|
|||
47
Penumbra/Interop/CloudApi.cs
Normal file
47
Penumbra/Interop/CloudApi.cs
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
namespace Penumbra.Interop;
|
||||
|
||||
public static unsafe partial class CloudApi
|
||||
{
|
||||
private const int CfSyncRootInfoBasic = 0;
|
||||
|
||||
/// <summary> Determines whether a file or directory is cloud-synced using OneDrive or other providers that use the Cloud API. </summary>
|
||||
/// <remarks> Can be expensive. Callers should cache the result when relevant. </remarks>
|
||||
public static bool IsCloudSynced(string path)
|
||||
{
|
||||
var buffer = stackalloc long[1];
|
||||
int hr;
|
||||
uint length;
|
||||
try
|
||||
{
|
||||
hr = CfGetSyncRootInfoByPath(path, CfSyncRootInfoBasic, buffer, sizeof(long), out length);
|
||||
}
|
||||
catch (DllNotFoundException)
|
||||
{
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw DllNotFoundException");
|
||||
return false;
|
||||
}
|
||||
catch (EntryPointNotFoundException)
|
||||
{
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} threw EntryPointNotFoundException");
|
||||
return false;
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned HRESULT 0x{hr:X8}");
|
||||
if (hr < 0)
|
||||
return false;
|
||||
|
||||
if (length != sizeof(long))
|
||||
{
|
||||
Penumbra.Log.Debug($"Expected {nameof(CfGetSyncRootInfoByPath)} to return {sizeof(long)} bytes, got {length} bytes");
|
||||
return false;
|
||||
}
|
||||
|
||||
Penumbra.Log.Debug($"{nameof(CfGetSyncRootInfoByPath)} returned {{ SyncRootFileId = 0x{*buffer:X16} }}");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
[LibraryImport("cldapi.dll", StringMarshalling = StringMarshalling.Utf16)]
|
||||
private static partial int CfGetSyncRootInfoByPath(string filePath, int infoClass, void* infoBuffer, uint infoBufferLength,
|
||||
out uint returnedLength);
|
||||
}
|
||||
|
|
@ -59,9 +59,6 @@ public class GameState : IService
|
|||
|
||||
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
||||
|
||||
public ResolveData SoundData
|
||||
=> _animationLoadData.Value;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||
public ResolveData SetSoundData(ResolveData data)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public unsafe class AtchCallerHook1 : FastHook<AtchCallerHook1.Delegate>, IDispo
|
|||
|
||||
private void Detour(DrawObjectData* data, uint slot, nint unk, Model playerModel)
|
||||
{
|
||||
var collection = _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true);
|
||||
var collection = playerModel.Valid ? _collectionResolver.IdentifyCollection(playerModel.AsDrawObject, true) : _collectionResolver.DefaultCollection;
|
||||
_metaState.AtchCollection.Push(collection);
|
||||
Task.Result.Original(data, slot, unk, playerModel);
|
||||
_metaState.AtchCollection.Pop();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ using Dalamud.Hooking;
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.UI.AdvancedWindow;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Objects;
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
|
|||
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterBaseDestructor"/>
|
||||
DrawObjectState = 0,
|
||||
|
||||
/// <seealso cref="ModEditWindow.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
|
||||
/// <seealso cref="UI.AdvancedWindow.Materials.MtrlTab.UnbindFromDrawObjectMaterialInstances"/>
|
||||
MtrlTab = -1000,
|
||||
}
|
||||
|
||||
|
|
@ -42,7 +41,7 @@ public sealed unsafe class CharacterBaseDestructor : EventWrapperPtr<CharacterBa
|
|||
|
||||
private nint Detour(CharacterBase* characterBase)
|
||||
{
|
||||
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
|
||||
Penumbra.Log.Excessive($"[{Name}] Triggered with 0x{(nint)characterBase:X}.");
|
||||
Invoke(characterBase);
|
||||
return _task.Result.Original(characterBase);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
|
|||
|
||||
/// <seealso cref="PathResolving.IdentifiedCollectionCache"/>
|
||||
IdentifiedCollectionCache = 0,
|
||||
|
||||
/// <seealso cref="PathResolving.DrawObjectState.OnCharacterDestructor"/>
|
||||
DrawObjectState = 0,
|
||||
}
|
||||
|
||||
public CharacterDestructor(HookManager hooks)
|
||||
|
|
@ -42,7 +45,7 @@ public sealed unsafe class CharacterDestructor : EventWrapperPtr<Character, Char
|
|||
|
||||
private void Detour(Character* character)
|
||||
{
|
||||
Penumbra.Log.Verbose($"[{Name}] Triggered with 0x{(nint)character:X}.");
|
||||
Penumbra.Log.Excessive($"[{Name}] Triggered with 0x{(nint)character:X}.");
|
||||
Invoke(character);
|
||||
_task.Result.Original(character);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
85
Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs
Normal file
85
Penumbra/Interop/Hooks/PostProcessing/AttributeHook.cs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
using Dalamud.Hooking;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using OtterGui.Classes;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Meta;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.PostProcessing;
|
||||
|
||||
/// <summary>
|
||||
/// Triggered whenever a model recomputes its attribute masks.
|
||||
/// <list type="number">
|
||||
/// <item>Parameter is the game object that recomputed its attributes. </item>
|
||||
/// <item>Parameter is the draw object on which the recomputation was called. </item>
|
||||
/// <item>Parameter is the collection associated with the game object. </item>
|
||||
/// <item>Parameter is the slot that was recomputed. If this is Unknown, it is a general new update call. </item>
|
||||
/// </list> </summary>
|
||||
public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollection, AttributeHook.Priority>, IHookService
|
||||
{
|
||||
public enum Priority
|
||||
{
|
||||
/// <seealso cref="ShapeAttributeManager.OnAttributeComputed"/>
|
||||
ShapeAttributeManager = 0,
|
||||
}
|
||||
|
||||
private readonly CollectionResolver _resolver;
|
||||
private readonly Configuration _config;
|
||||
|
||||
public AttributeHook(HookManager hooks, Configuration config, CollectionResolver resolver)
|
||||
: base("Update Model Attributes")
|
||||
{
|
||||
_config = config;
|
||||
_resolver = resolver;
|
||||
_task = hooks.CreateHook<Delegate>(Name, Sigs.UpdateAttributes, Detour, config.EnableCustomShapes);
|
||||
}
|
||||
|
||||
private readonly Task<Hook<Delegate>> _task;
|
||||
|
||||
public nint Address
|
||||
=> _task.Result.Address;
|
||||
|
||||
public void Enable()
|
||||
=> SetState(true);
|
||||
|
||||
public void Disable()
|
||||
=> SetState(false);
|
||||
|
||||
public void SetState(bool enabled)
|
||||
{
|
||||
if (_config.EnableCustomShapes == enabled)
|
||||
return;
|
||||
|
||||
_config.EnableCustomShapes = enabled;
|
||||
_config.Save();
|
||||
if (enabled)
|
||||
_task.Result.Enable();
|
||||
else
|
||||
_task.Result.Disable();
|
||||
}
|
||||
|
||||
|
||||
public Task Awaiter
|
||||
=> _task;
|
||||
|
||||
public bool Finished
|
||||
=> _task.IsCompletedSuccessfully;
|
||||
|
||||
private delegate void Delegate(Human* human);
|
||||
|
||||
private void Detour(Human* human)
|
||||
{
|
||||
_task.Result.Original(human);
|
||||
var resolveData = _resolver.IdentifyCollection((DrawObject*)human, true);
|
||||
var identifiedActor = resolveData.AssociatedGameObject;
|
||||
var identifiedCollection = resolveData.ModCollection;
|
||||
Penumbra.Log.Excessive($"[{Name}] Invoked on 0x{(ulong)human:X} (0x{identifiedActor:X}).");
|
||||
Invoke(identifiedActor, human, identifiedCollection);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
=> _task.Result.Dispose();
|
||||
}
|
||||
|
|
@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi
|
|||
if (!_framework.IsInFrameworkUpdateThread)
|
||||
Penumbra.Log.Warning(
|
||||
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
|
||||
|
||||
|
||||
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
|
||||
try
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using System.Text.Unicode;
|
||||
using Dalamud.Hooking;
|
||||
using Iced.Intel;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using Penumbra.String.Classes;
|
||||
using Swan;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using Dalamud.Hooking;
|
||||
using Dalamud.Interface.Windowing;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Dalamud.Utility.Signatures;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
||||
|
|
@ -85,7 +86,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8);
|
||||
|
||||
private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
||||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8, uint unk9);
|
||||
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, byte isUnknown, nint unk8,
|
||||
uint unk9);
|
||||
|
||||
[Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))]
|
||||
private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!;
|
||||
|
|
@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
unk9);
|
||||
}
|
||||
|
||||
var original = gamePath;
|
||||
if (gamePath.IsEmpty)
|
||||
{
|
||||
Penumbra.Log.Error($"[ResourceService] Empty resource path requested with category {*categoryId}, type {*resourceType}, hash {*resourceHash}.");
|
||||
return null;
|
||||
}
|
||||
|
||||
var original = gamePath;
|
||||
ResourceHandle* returnValue = null;
|
||||
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync,
|
||||
ref returnValue);
|
||||
if (returnValue != null)
|
||||
return returnValue;
|
||||
|
||||
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8, unk9);
|
||||
return GetOriginalResource(isSync, *categoryId, *resourceType, *resourceHash, gamePath.Path, original, pGetResParams, isUnk, unk8,
|
||||
unk9);
|
||||
}
|
||||
|
||||
/// <summary> Call the original GetResource function. </summary>
|
||||
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path, Utf8GamePath original,
|
||||
public ResourceHandle* GetOriginalResource(bool sync, ResourceCategory categoryId, ResourceType type, int hash, CiByteString path,
|
||||
Utf8GamePath original,
|
||||
GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0)
|
||||
{
|
||||
var previous = _currentGetResourcePath.Value;
|
||||
|
|
@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
resourceParameters, unk8, unk9)
|
||||
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
|
||||
resourceParameters, unk, unk8, unk9);
|
||||
} finally
|
||||
}
|
||||
finally
|
||||
{
|
||||
_currentGetResourcePath.Value = previous;
|
||||
}
|
||||
|
|
@ -163,7 +174,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
/// <param name="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
|
||||
/// <param name="previousState">The previous state of the resource.</param>
|
||||
/// <param name="returnValue">The return value to use.</param>
|
||||
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal, (byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
|
||||
public delegate void ResourceStateUpdatedDelegate(ResourceHandle* handle, Utf8GamePath syncOriginal,
|
||||
(byte UnkState, LoadState LoadState) previousState, ref uint returnValue);
|
||||
|
||||
/// <summary>
|
||||
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
|
||||
|
|
@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
|||
private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread)
|
||||
{
|
||||
var previousState = (handle->UnkState, handle->LoadState);
|
||||
var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty;
|
||||
var syncOriginal = _currentGetResourcePath.IsValueCreated ? _currentGetResourcePath.Value : Utf8GamePath.Empty;
|
||||
ResourceStateUpdating?.Invoke(handle, syncOriginal);
|
||||
var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread);
|
||||
ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret);
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using Penumbra.Collections;
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.PathResolving;
|
||||
using Penumbra.Interop.Processing;
|
||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
||||
|
||||
namespace Penumbra.Interop.Hooks.Resources;
|
||||
|
|
@ -35,6 +36,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
|
||||
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveSkinMtrlPathHook;
|
||||
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
|
||||
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
|
||||
|
|
@ -52,22 +54,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
{
|
||||
_parent = parent;
|
||||
// @formatter:off
|
||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
||||
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
|
||||
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
|
||||
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
|
||||
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
|
||||
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
|
||||
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
|
||||
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
|
||||
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
|
||||
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
|
||||
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
|
||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
||||
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
||||
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
||||
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
|
||||
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
|
||||
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
|
||||
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
|
||||
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
|
||||
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
|
||||
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
|
||||
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
|
||||
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
|
||||
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
|
||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
||||
_resolveSkinMtrlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl);
|
||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
||||
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
||||
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
||||
|
||||
|
||||
// @formatter:on
|
||||
|
|
@ -83,6 +86,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Enable();
|
||||
_resolveMdlPathHook.Enable();
|
||||
_resolveMtrlPathHook.Enable();
|
||||
_resolveSkinMtrlPathHook.Enable();
|
||||
_resolvePapPathHook.Enable();
|
||||
_resolveKdbPathHook.Enable();
|
||||
_resolvePhybPathHook.Enable();
|
||||
|
|
@ -103,6 +107,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Disable();
|
||||
_resolveMdlPathHook.Disable();
|
||||
_resolveMtrlPathHook.Disable();
|
||||
_resolveSkinMtrlPathHook.Disable();
|
||||
_resolvePapPathHook.Disable();
|
||||
_resolveKdbPathHook.Disable();
|
||||
_resolvePhybPathHook.Disable();
|
||||
|
|
@ -123,6 +128,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
_resolveMPapPathHook.Dispose();
|
||||
_resolveMdlPathHook.Dispose();
|
||||
_resolveMtrlPathHook.Dispose();
|
||||
_resolveSkinMtrlPathHook.Dispose();
|
||||
_resolvePapPathHook.Dispose();
|
||||
_resolveKdbPathHook.Dispose();
|
||||
_resolvePhybPathHook.Dispose();
|
||||
|
|
@ -153,6 +159,15 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName)
|
||||
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, mtrlFileName));
|
||||
|
||||
private nint ResolveSkinMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex)
|
||||
{
|
||||
var finalPathBuffer = _resolveSkinMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex);
|
||||
if (DebugConfiguration.UseSkinMaterialProcessing && finalPathBuffer != nint.Zero && finalPathBuffer == pathBuffer)
|
||||
SkinMtrlPathEarlyProcessing.Process(new Span<byte>((void*)pathBuffer, (int)pathBufferSize), (CharacterBase*)drawObject, slotIndex);
|
||||
|
||||
return ResolvePath(drawObject, finalPathBuffer);
|
||||
}
|
||||
|
||||
private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
|
||||
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
|
||||
|
||||
|
|
@ -246,28 +261,6 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
|||
return ret;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
private struct ChangedEquipData
|
||||
{
|
||||
[FieldOffset(0)]
|
||||
public PrimaryId Model;
|
||||
|
||||
[FieldOffset(2)]
|
||||
public Variant Variant;
|
||||
|
||||
[FieldOffset(8)]
|
||||
public PrimaryId BonusModel;
|
||||
|
||||
[FieldOffset(10)]
|
||||
public Variant BonusVariant;
|
||||
|
||||
[FieldOffset(20)]
|
||||
public ushort VfxId;
|
||||
|
||||
[FieldOffset(22)]
|
||||
public GenderRace GenderRace;
|
||||
}
|
||||
|
||||
private nint ResolveVfxHuman(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint unkOutParam)
|
||||
{
|
||||
switch (slotIndex)
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Services;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.Collections.Manager;
|
||||
|
|
@ -89,12 +89,19 @@ public sealed unsafe class CollectionResolver(
|
|||
/// <summary> Identify the correct collection for a draw object. </summary>
|
||||
public ResolveData IdentifyCollection(DrawObject* drawObject, bool useCache)
|
||||
{
|
||||
var obj = (GameObject*)(drawObjectState.TryGetValue((nint)drawObject, out var gameObject)
|
||||
if (drawObject is null)
|
||||
return DefaultCollection;
|
||||
|
||||
Actor obj = drawObjectState.TryGetValue(drawObject, out var gameObject)
|
||||
? gameObject.Item1
|
||||
: drawObjectState.LastGameObject);
|
||||
return IdentifyCollection(obj, useCache);
|
||||
: drawObjectState.LastGameObject;
|
||||
return IdentifyCollection(obj.AsObject, useCache);
|
||||
}
|
||||
|
||||
/// <summary> Get the default collection. </summary>
|
||||
public ResolveData DefaultCollection
|
||||
=> collectionManager.Active.Default.ToResolveData();
|
||||
|
||||
/// <summary> Return whether the given ModelChara id refers to a human-type model. </summary>
|
||||
public bool IsModelHuman(uint modelCharaId)
|
||||
=> humanModels.IsHuman(modelCharaId);
|
||||
|
|
@ -130,7 +137,7 @@ public sealed unsafe class CollectionResolver(
|
|||
{
|
||||
var item = charaEntry.Value;
|
||||
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
|
||||
Penumbra.Log.Verbose(
|
||||
Penumbra.Log.Excessive(
|
||||
$"Identified {identifier.Incognito(null)} in cutscene for actor {idx + 200} at 0x{(ulong)gameObject:X} of race {(gameObject->IsCharacter() ? ((Character*)gameObject)->DrawData.CustomizeData.Race.ToString() : "Unknown")}.");
|
||||
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
|||
return false;
|
||||
|
||||
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
||||
_objects.InvokeRequiredUpdates();
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,39 +9,42 @@ using Penumbra.Interop.Hooks.Objects;
|
|||
|
||||
namespace Penumbra.Interop.PathResolving;
|
||||
|
||||
public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (nint, bool)>, IService
|
||||
public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<Model, (Actor, ObjectIndex, bool)>, IService
|
||||
{
|
||||
private readonly ObjectManager _objects;
|
||||
private readonly CreateCharacterBase _createCharacterBase;
|
||||
private readonly WeaponReload _weaponReload;
|
||||
private readonly CharacterBaseDestructor _characterBaseDestructor;
|
||||
private readonly CharacterDestructor _characterDestructor;
|
||||
private readonly GameState _gameState;
|
||||
|
||||
private readonly Dictionary<nint, (nint GameObject, bool IsChild)> _drawObjectToGameObject = [];
|
||||
private readonly Dictionary<Model, (Actor GameObject, ObjectIndex Index, bool IsChild)> _drawObjectToGameObject = [];
|
||||
|
||||
public nint LastGameObject
|
||||
=> _gameState.LastGameObject;
|
||||
|
||||
public unsafe DrawObjectState(ObjectManager objects, CreateCharacterBase createCharacterBase, WeaponReload weaponReload,
|
||||
CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework)
|
||||
CharacterBaseDestructor characterBaseDestructor, GameState gameState, IFramework framework, CharacterDestructor characterDestructor)
|
||||
{
|
||||
_objects = objects;
|
||||
_createCharacterBase = createCharacterBase;
|
||||
_weaponReload = weaponReload;
|
||||
_characterBaseDestructor = characterBaseDestructor;
|
||||
_gameState = gameState;
|
||||
_characterDestructor = characterDestructor;
|
||||
framework.RunOnFrameworkThread(InitializeDrawObjects);
|
||||
|
||||
_weaponReload.Subscribe(OnWeaponReloading, WeaponReload.Priority.DrawObjectState);
|
||||
_weaponReload.Subscribe(OnWeaponReloaded, WeaponReload.PostEvent.Priority.DrawObjectState);
|
||||
_createCharacterBase.Subscribe(OnCharacterBaseCreated, CreateCharacterBase.PostEvent.Priority.DrawObjectState);
|
||||
_characterBaseDestructor.Subscribe(OnCharacterBaseDestructor, CharacterBaseDestructor.Priority.DrawObjectState);
|
||||
_characterDestructor.Subscribe(OnCharacterDestructor, CharacterDestructor.Priority.DrawObjectState);
|
||||
}
|
||||
|
||||
public bool ContainsKey(nint key)
|
||||
public bool ContainsKey(Model key)
|
||||
=> _drawObjectToGameObject.ContainsKey(key);
|
||||
|
||||
public IEnumerator<KeyValuePair<nint, (nint, bool)>> GetEnumerator()
|
||||
public IEnumerator<KeyValuePair<Model, (Actor, ObjectIndex, bool)>> GetEnumerator()
|
||||
=> _drawObjectToGameObject.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
|
|
@ -50,16 +53,28 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (ni
|
|||
public int Count
|
||||
=> _drawObjectToGameObject.Count;
|
||||
|
||||
public bool TryGetValue(nint drawObject, out (nint, bool) gameObject)
|
||||
=> _drawObjectToGameObject.TryGetValue(drawObject, out gameObject);
|
||||
public bool TryGetValue(Model drawObject, out (Actor, ObjectIndex, bool) gameObject)
|
||||
{
|
||||
if (!_drawObjectToGameObject.TryGetValue(drawObject, out gameObject))
|
||||
return false;
|
||||
|
||||
public (nint, bool) this[nint key]
|
||||
var currentObject = _objects[gameObject.Item2];
|
||||
if (currentObject != gameObject.Item1)
|
||||
{
|
||||
Penumbra.Log.Warning($"[DrawObjectState] Stored association {drawObject} -> {gameObject.Item1} has index {gameObject.Item2}, which resolves to {currentObject}.");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public (Actor, ObjectIndex, bool) this[Model key]
|
||||
=> _drawObjectToGameObject[key];
|
||||
|
||||
public IEnumerable<nint> Keys
|
||||
public IEnumerable<Model> Keys
|
||||
=> _drawObjectToGameObject.Keys;
|
||||
|
||||
public IEnumerable<(nint, bool)> Values
|
||||
public IEnumerable<(Actor, ObjectIndex, bool)> Values
|
||||
=> _drawObjectToGameObject.Values;
|
||||
|
||||
public unsafe void Dispose()
|
||||
|
|
@ -68,6 +83,37 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (ni
|
|||
_weaponReload.Unsubscribe(OnWeaponReloaded);
|
||||
_createCharacterBase.Unsubscribe(OnCharacterBaseCreated);
|
||||
_characterBaseDestructor.Unsubscribe(OnCharacterBaseDestructor);
|
||||
_characterDestructor.Unsubscribe(OnCharacterDestructor);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Seems like sometimes the draw object of a game object is destroyed in frames after the original game object is already destroyed.
|
||||
/// So protect against outdated game object pointers in the dictionary.
|
||||
/// </remarks>
|
||||
private unsafe void OnCharacterDestructor(Character* a)
|
||||
{
|
||||
if (a is null)
|
||||
return;
|
||||
|
||||
var character = (nint)a;
|
||||
var delete = stackalloc nint[5];
|
||||
var current = 0;
|
||||
foreach (var (drawObject, (gameObject, _, _)) in _drawObjectToGameObject)
|
||||
{
|
||||
if (gameObject != character)
|
||||
continue;
|
||||
|
||||
delete[current++] = drawObject;
|
||||
if (current is 4)
|
||||
break;
|
||||
}
|
||||
|
||||
for (var ptr = delete; *ptr != nint.Zero; ++ptr)
|
||||
{
|
||||
_drawObjectToGameObject.Remove(*ptr, out var pair);
|
||||
Penumbra.Log.Excessive(
|
||||
$"[DrawObjectState] Removed draw object 0x{*ptr:X} -> 0x{(nint)a:X} (actual: 0x{pair.GameObject.Address:X}, {pair.IsChild}).");
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe void OnWeaponReloading(DrawDataContainer* _, Character* character, CharacterWeapon* _2)
|
||||
|
|
@ -85,9 +131,9 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (ni
|
|||
private unsafe void OnCharacterBaseCreated(ModelCharaId modelCharaId, CustomizeArray* customize, CharacterArmor* equipment,
|
||||
CharacterBase* drawObject)
|
||||
{
|
||||
var gameObject = LastGameObject;
|
||||
if (gameObject != nint.Zero)
|
||||
_drawObjectToGameObject[(nint)drawObject] = (gameObject, false);
|
||||
Actor gameObject = LastGameObject;
|
||||
if (gameObject.Valid)
|
||||
_drawObjectToGameObject[(nint)drawObject] = (gameObject, gameObject.Index, false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -103,12 +149,12 @@ public sealed class DrawObjectState : IDisposable, IReadOnlyDictionary<nint, (ni
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe void IterateDrawObjectTree(Object* drawObject, nint gameObject, bool isChild, bool iterate)
|
||||
private unsafe void IterateDrawObjectTree(Object* drawObject, Actor gameObject, bool isChild, bool iterate)
|
||||
{
|
||||
if (drawObject == null)
|
||||
return;
|
||||
|
||||
_drawObjectToGameObject[(nint)drawObject] = (gameObject, isChild);
|
||||
_drawObjectToGameObject[drawObject] = (gameObject, gameObject.Index, isChild);
|
||||
IterateDrawObjectTree(drawObject->ChildObject, gameObject, true, true);
|
||||
if (!iterate)
|
||||
return;
|
||||
|
|
|
|||
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
namespace Penumbra.Interop;
|
||||
|
||||
public static partial class ProcessThreadApi
|
||||
{
|
||||
[LibraryImport("kernel32.dll")]
|
||||
public static partial uint GetCurrentThreadId();
|
||||
}
|
||||
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
using Dalamud.Game;
|
||||
using Dalamud.Plugin.Services;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.String;
|
||||
|
||||
namespace Penumbra.Interop.Processing;
|
||||
|
||||
public sealed class PbdFilePostProcessor : IFilePostProcessor
|
||||
{
|
||||
private readonly IFileAllocator _allocator;
|
||||
private byte[] _epbdData;
|
||||
private unsafe delegate* unmanaged<ResourceHandle*, void> _loadEpbdData;
|
||||
|
||||
public ResourceType Type
|
||||
=> ResourceType.Pbd;
|
||||
|
||||
public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner)
|
||||
{
|
||||
_allocator = allocator;
|
||||
_epbdData = SetEpbdData(dataManager);
|
||||
_loadEpbdData = (delegate* unmanaged<ResourceHandle*, void>)scanner.ScanText(Sigs.LoadEpbdData);
|
||||
}
|
||||
|
||||
public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan<byte> additionalData)
|
||||
{
|
||||
if (_epbdData.Length is 0)
|
||||
return;
|
||||
|
||||
if (resource->LoadState is not LoadState.Success)
|
||||
{
|
||||
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState}).");
|
||||
return;
|
||||
}
|
||||
|
||||
var (data, length) = resource->GetData();
|
||||
if (length is 0 || data == nint.Zero)
|
||||
{
|
||||
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data.");
|
||||
return;
|
||||
}
|
||||
|
||||
var span = new ReadOnlySpan<byte>((void*)data, (int)resource->FileSize);
|
||||
var reader = new PackReader(span);
|
||||
if (reader.HasData)
|
||||
{
|
||||
Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data.");
|
||||
return;
|
||||
}
|
||||
|
||||
var newData = AppendData(span);
|
||||
fixed (byte* ptr = newData)
|
||||
{
|
||||
// Set the appended data and the actual file size, then re-load the EPBD data via game function call.
|
||||
if (resource->SetData((nint)ptr, newData.Length))
|
||||
{
|
||||
resource->FileSize = (uint)newData.Length;
|
||||
resource->CsHandle.FileSize2 = (uint)newData.Length;
|
||||
resource->CsHandle.FileSize3 = (uint)newData.Length;
|
||||
_loadEpbdData(resource);
|
||||
// Free original data.
|
||||
_allocator.Release((void*)data, length);
|
||||
Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data.");
|
||||
}
|
||||
else
|
||||
{
|
||||
Penumbra.Log.Warning(
|
||||
$"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary> Combine the given data with the default PBD data using the game's file allocator. </summary>
|
||||
private unsafe ReadOnlySpan<byte> AppendData(ReadOnlySpan<byte> data)
|
||||
{
|
||||
// offset has to be set, otherwise not called.
|
||||
var newLength = data.Length + _epbdData.Length;
|
||||
var memory = _allocator.Allocate(newLength);
|
||||
var span = new Span<byte>(memory, newLength);
|
||||
data.CopyTo(span);
|
||||
_epbdData.CopyTo(span[data.Length..]);
|
||||
return span;
|
||||
}
|
||||
|
||||
/// <summary> Fetch the default EPBD data from the .pbd file of the game's installation. </summary>
|
||||
private static byte[] SetEpbdData(IDataManager dataManager)
|
||||
{
|
||||
try
|
||||
{
|
||||
var file = dataManager.GetFile(GamePaths.Pbd.Path);
|
||||
if (file is null || file.Data.Length is 0)
|
||||
{
|
||||
Penumbra.Log.Warning("Default PBD file has no data.");
|
||||
return [];
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> span = file.Data;
|
||||
var reader = new PackReader(span);
|
||||
if (!reader.HasData)
|
||||
{
|
||||
Penumbra.Log.Warning("Default PBD file has no EPBD section.");
|
||||
return [];
|
||||
}
|
||||
|
||||
var offset = span.Length - (int)reader.PackLength;
|
||||
var ret = span[offset..];
|
||||
Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}.");
|
||||
return ret.ToArray();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,7 +49,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager,
|
|||
return null;
|
||||
}
|
||||
|
||||
private static SanityCheckResult SanityCheck(string path)
|
||||
internal static SanityCheckResult SanityCheck(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
|
@ -79,7 +79,7 @@ public sealed class ShpkPathPreProcessor(ResourceManagerService resourceManager,
|
|||
_ => string.Empty,
|
||||
};
|
||||
|
||||
private enum SanityCheckResult
|
||||
internal enum SanityCheckResult
|
||||
{
|
||||
Success,
|
||||
IoError,
|
||||
|
|
|
|||
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
|
||||
namespace Penumbra.Interop.Processing;
|
||||
|
||||
public static unsafe class SkinMtrlPathEarlyProcessing
|
||||
{
|
||||
public static void Process(Span<byte> path, CharacterBase* character, uint slotIndex)
|
||||
{
|
||||
var end = path.IndexOf(MaterialExtension());
|
||||
if (end < 0)
|
||||
return;
|
||||
|
||||
var suffixPos = path[..end].LastIndexOf((byte)'_');
|
||||
if (suffixPos < 0)
|
||||
return;
|
||||
|
||||
var handle = GetModelResourceHandle(character, slotIndex);
|
||||
if (handle == null)
|
||||
return;
|
||||
|
||||
var skinSuffix = GetSkinSuffix(handle);
|
||||
if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7)
|
||||
return;
|
||||
|
||||
++suffixPos;
|
||||
skinSuffix.CopyTo(path[suffixPos..]);
|
||||
suffixPos += skinSuffix.Length;
|
||||
MaterialExtension().CopyTo(path[suffixPos..]);
|
||||
return;
|
||||
|
||||
static ReadOnlySpan<byte> MaterialExtension()
|
||||
=> ".mtrl\0"u8;
|
||||
}
|
||||
|
||||
private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex)
|
||||
{
|
||||
if (character is null)
|
||||
return null;
|
||||
|
||||
if (character->PerSlotStagingArea is not null)
|
||||
{
|
||||
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
|
||||
if (handle != null)
|
||||
return handle;
|
||||
}
|
||||
|
||||
var model = character->Models[slotIndex];
|
||||
return model is null ? null : model->ModelResourceHandle;
|
||||
}
|
||||
|
||||
private static ReadOnlySpan<byte> GetSkinSuffix(ModelResourceHandle* handle)
|
||||
{
|
||||
foreach (var (attribute, _) in handle->Attributes)
|
||||
{
|
||||
var attributeSpan = attribute.AsSpan();
|
||||
if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_')
|
||||
return attributeSpan[12..];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
|
@ -338,6 +338,34 @@ internal partial record ResolveContext
|
|||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private Utf8GamePath ResolveKineDriverModulePath(uint partialSkeletonIndex)
|
||||
{
|
||||
// Correctness and Safety:
|
||||
// Resolving a KineDriver module path through the game's code can use EST metadata for human skeletons.
|
||||
// Additionally, it can dereference null pointers for human equipment skeletons.
|
||||
return ModelType switch
|
||||
{
|
||||
ModelType.Human => ResolveHumanKineDriverModulePath(partialSkeletonIndex),
|
||||
_ => ResolveKineDriverModulePathNative(partialSkeletonIndex),
|
||||
};
|
||||
}
|
||||
|
||||
private Utf8GamePath ResolveHumanKineDriverModulePath(uint partialSkeletonIndex)
|
||||
{
|
||||
var (raceCode, slot, set) = ResolveHumanSkeletonData(partialSkeletonIndex);
|
||||
if (set.Id is 0)
|
||||
return Utf8GamePath.Empty;
|
||||
|
||||
var path = GamePaths.Kdb.Customization(raceCode, slot, set);
|
||||
return Utf8GamePath.FromString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private unsafe Utf8GamePath ResolveKineDriverModulePathNative(uint partialSkeletonIndex)
|
||||
{
|
||||
var path = CharacterBase->ResolveKdbPathAsByteString(partialSkeletonIndex);
|
||||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
||||
}
|
||||
|
||||
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
|
||||
{
|
||||
var animation = ResolveImcData(imc).MaterialAnimationId;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
|||
using FFXIVClientStructs.FFXIV.Client.Graphics.Render;
|
||||
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||
using FFXIVClientStructs.Interop;
|
||||
using OtterGui;
|
||||
using OtterGui.Extensions;
|
||||
using OtterGui.Text.HelperObjects;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
|
|
@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext(
|
|||
if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path))
|
||||
return null;
|
||||
|
||||
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
|
||||
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path);
|
||||
}
|
||||
|
||||
[SkipLocalsInit]
|
||||
|
|
@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext(
|
|||
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
|
||||
}
|
||||
|
||||
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle, ResourceHandle* mpapHandle)
|
||||
public ResourceNode? CreateNodeFromModel(Model* mdl, ResourceHandle* imc, TextureResourceHandle* decalHandle,
|
||||
MaterialResourceHandle* skinMtrlHandle, ResourceHandle* mpapHandle)
|
||||
{
|
||||
if (mdl is null || mdl->ModelResourceHandle is null)
|
||||
return null;
|
||||
|
|
@ -218,6 +219,12 @@ internal unsafe partial record ResolveContext(
|
|||
}
|
||||
}
|
||||
|
||||
if (skinMtrlHandle is not null
|
||||
&& Utf8GamePath.FromByteString(CharacterBase->ResolveSkinMtrlPathAsByteString(SlotIndex), out var skinMtrlPath)
|
||||
&& CreateNodeFromMaterial(skinMtrlHandle->Material, skinMtrlPath) is
|
||||
{ } skinMaaterialNode)
|
||||
node.Children.Add(skinMaaterialNode);
|
||||
|
||||
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
|
||||
node.Children.Add(decalNode);
|
||||
|
||||
|
|
@ -238,7 +245,7 @@ internal unsafe partial record ResolveContext(
|
|||
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||
return cached;
|
||||
|
||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, &resource->ResourceHandle, path, false);
|
||||
var node = CreateNode(ResourceType.Mtrl, (nint)mtrl, (ResourceHandle*)resource, path, false);
|
||||
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value));
|
||||
if (shpkNode is not null)
|
||||
{
|
||||
|
|
@ -364,7 +371,8 @@ internal unsafe partial record ResolveContext(
|
|||
return node;
|
||||
}
|
||||
|
||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, uint partialSkeletonIndex)
|
||||
public ResourceNode? CreateNodeFromPartialSkeleton(PartialSkeleton* sklb, ResourceHandle* phybHandle, ResourceHandle* kdbHandle,
|
||||
uint partialSkeletonIndex)
|
||||
{
|
||||
if (sklb is null || sklb->SkeletonResourceHandle is null)
|
||||
return null;
|
||||
|
|
@ -379,6 +387,8 @@ internal unsafe partial record ResolveContext(
|
|||
node.Children.Add(skpNode);
|
||||
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
||||
node.Children.Add(phybNode);
|
||||
if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode)
|
||||
node.Children.Add(kdbNode);
|
||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||
|
||||
return node;
|
||||
|
|
@ -420,6 +430,24 @@ internal unsafe partial record ResolveContext(
|
|||
return node;
|
||||
}
|
||||
|
||||
private ResourceNode? CreateNodeFromKdb(ResourceHandle* kdbHandle, uint partialSkeletonIndex)
|
||||
{
|
||||
if (kdbHandle is null)
|
||||
return null;
|
||||
|
||||
var path = ResolveKineDriverModulePath(partialSkeletonIndex);
|
||||
|
||||
if (Global.Nodes.TryGetValue((path, (nint)kdbHandle), out var cached))
|
||||
return cached;
|
||||
|
||||
var node = CreateNode(ResourceType.Kdb, 0, kdbHandle, path, false);
|
||||
if (Global.WithUiData)
|
||||
node.FallbackName = "KineDriver Module";
|
||||
Global.Nodes.Add((path, (nint)kdbHandle), node);
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||
{
|
||||
var path = gamePath.Path.Split((byte)'/');
|
||||
|
|
|
|||
|
|
@ -45,7 +45,9 @@ public class ResourceNode : ICloneable
|
|||
|
||||
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
|
||||
public bool Protected
|
||||
=> ForceProtected || Internal || Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Pbd;
|
||||
=> ForceProtected
|
||||
|| Internal
|
||||
|| Type is ResourceType.Shpk or ResourceType.Sklb or ResourceType.Skp or ResourceType.Phyb or ResourceType.Kdb or ResourceType.Pbd;
|
||||
|
||||
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,12 +70,16 @@ public class ResourceTree(
|
|||
|
||||
var genericContext = globalContext.CreateContext(model);
|
||||
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
|
||||
var mpapArrayPtr = model->MaterialAnimationPacks;
|
||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
|
||||
var skinMtrlArray = modelType switch
|
||||
{
|
||||
ModelType.Human => ((Human*) model)->SlotSkinMaterials,
|
||||
_ => [],
|
||||
};
|
||||
var decalArray = modelType switch
|
||||
{
|
||||
ModelType.Human => human->SlotDecalsSpan,
|
||||
ModelType.Human => human->SlotDecals,
|
||||
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
|
||||
ModelType.Weapon => [((Weapon*)model)->Decal],
|
||||
ModelType.Monster => [((Monster*)model)->Decal],
|
||||
|
|
@ -108,7 +112,8 @@ public class ResourceTree(
|
|||
|
||||
var mdl = model->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
|
||||
i < mpapArray.Length ? mpapArray[(int)i].Value : null) is { } mdlNode)
|
||||
i < skinMtrlArray.Length ? skinMtrlArray[(int)i].Value : null, i < mpapArray.Length ? mpapArray[(int)i].Value : null) is
|
||||
{ } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Model #{i}";
|
||||
|
|
@ -116,9 +121,8 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
|
||||
AddSkeleton(Nodes, genericContext, model);
|
||||
AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton);
|
||||
|
||||
AddWeapons(globalContext, model);
|
||||
|
||||
|
|
@ -149,8 +153,7 @@ public class ResourceTree(
|
|||
|
||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
|
||||
var mpapArrayPtr = subObject->MaterialAnimationPacks;
|
||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
|
||||
|
||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||
|
|
@ -166,7 +169,8 @@ public class ResourceTree(
|
|||
}
|
||||
|
||||
var mdl = subObject->Models[i];
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, i < mpapArray.Length ? mpapArray[i].Value : null) is { } mdlNode)
|
||||
if (slotContext.CreateNodeFromModel(mdl, imc, weapon->Decal, null, i < mpapArray.Length ? mpapArray[i].Value : null) is
|
||||
{ } mdlNode)
|
||||
{
|
||||
if (globalContext.WithUiData)
|
||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||
|
|
@ -174,10 +178,8 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule,
|
||||
$"Weapon #{weaponIndex}, ");
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940),
|
||||
AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, ");
|
||||
AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton,
|
||||
$"Weapon #{weaponIndex}, ");
|
||||
|
||||
++weaponIndex;
|
||||
|
|
@ -239,8 +241,11 @@ public class ResourceTree(
|
|||
}
|
||||
}
|
||||
|
||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, CharacterBase* model, string prefix = "")
|
||||
=> AddSkeleton(nodes, context, model->EID, model->Skeleton, model->BonePhysicsModule, model->BoneKineDriverModule, prefix);
|
||||
|
||||
private unsafe void AddSkeleton(List<ResourceNode> nodes, ResolveContext context, void* eid, Skeleton* skeleton, BonePhysicsModule* physics,
|
||||
string prefix = "")
|
||||
BoneKineDriverModule* kineDriver, string prefix = "")
|
||||
{
|
||||
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||
if (eidNode != null)
|
||||
|
|
@ -255,9 +260,9 @@ public class ResourceTree(
|
|||
|
||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||
{
|
||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
||||
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
|
||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
|
||||
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
|
||||
var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
|
||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
|
||||
{
|
||||
if (context.Global.WithUiData)
|
||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ using OtterGui.Services;
|
|||
using Penumbra.Api;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Communication;
|
||||
using Penumbra.GameData;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Interop;
|
||||
using Penumbra.Interop.Structs;
|
||||
|
|
@ -354,21 +353,14 @@ public sealed unsafe partial class RedrawService : IDisposable
|
|||
{
|
||||
switch (settings)
|
||||
{
|
||||
case RedrawType.Redraw:
|
||||
ReloadActor(actor);
|
||||
break;
|
||||
case RedrawType.AfterGPose:
|
||||
ReloadActorAfterGPose(actor);
|
||||
break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null);
|
||||
case RedrawType.Redraw: ReloadActor(actor); break;
|
||||
case RedrawType.AfterGPose: ReloadActorAfterGPose(actor); break;
|
||||
default: throw new ArgumentOutOfRangeException(nameof(settings), settings, null);
|
||||
}
|
||||
}
|
||||
|
||||
private IGameObject? GetLocalPlayer()
|
||||
{
|
||||
var gPosePlayer = _objects.GetDalamudObject(GPosePlayerIdx);
|
||||
return gPosePlayer ?? _objects.GetDalamudObject(0);
|
||||
}
|
||||
=> InGPose ? _objects.GetDalamudObject(GPosePlayerIdx) ?? _objects.GetDalamudObject(0) : _objects.GetDalamudObject(0);
|
||||
|
||||
public bool GetName(string lowerName, out IGameObject? actor)
|
||||
{
|
||||
|
|
@ -429,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;
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue