mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-14 12:44:19 +01:00
Compare commits
166 commits
testing_1.
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ccb5b01290 | ||
|
|
5dd74297c6 | ||
|
|
ce54aa5d25 | ||
|
|
c4b6e4e00b | ||
|
|
912c183fc6 | ||
|
|
5bf901d0c4 | ||
|
|
cbedc878b9 | ||
|
|
c8cf560fc1 | ||
|
|
f05cb52da2 | ||
|
|
7ed81a9823 | ||
|
|
60aa23efcd | ||
|
|
ebbe957c95 | ||
|
|
300e0e6d84 | ||
|
|
049baa4fe4 | ||
|
|
0881dfde8a | ||
|
|
23c0506cb8 | ||
|
|
699745413e | ||
|
|
eb53f04c6b | ||
|
|
c6b596169c | ||
|
|
a0c3e820b0 | ||
|
|
a59689ebfe | ||
|
|
e9f67a009b | ||
|
|
97c8d82b33 | ||
|
|
c3b00ff426 | ||
|
|
6348c4a639 | ||
|
|
5a6e06df3b | ||
|
|
f5f6dd3246 | ||
|
|
4e788f7c2b | ||
|
|
ad1659caf6 | ||
|
|
18a6ce2a5f | ||
|
|
e68e821b2a | ||
|
|
96764b34ca | ||
|
|
2cf60b78cd | ||
|
|
d59be1e660 | ||
|
|
5503bb32e0 | ||
|
|
f3ec4b2e08 | ||
|
|
b3379a9710 | ||
|
|
8c25ef4b47 | ||
|
|
912020cc3f | ||
|
|
be8987a451 | ||
|
|
f7cf5503bb | ||
|
|
a04a5a071c | ||
|
|
71e24c13c7 | ||
|
|
c0120f81af | ||
|
|
da47c19aeb | ||
|
|
e16800f216 | ||
|
|
79a4fc5904 | ||
|
|
bf90725dd2 | ||
|
|
a14347f73a | ||
|
|
1e07e43498 | ||
|
|
f51f8a7bf8 | ||
|
|
1fca78fa71 | ||
|
|
c8b6325a87 | ||
|
|
6079103505 | ||
|
|
d302a17f1f | ||
|
|
0d64384059 | ||
|
|
10894d451a | ||
|
|
fb34238530 | ||
|
|
8043e6fb6b | ||
|
|
e3b7f72893 | ||
|
|
b7f326e29c | ||
|
|
dad01e1af8 | ||
|
|
10b71930a1 | ||
|
|
23257f94a4 | ||
|
|
83a36ed4cb | ||
|
|
8304579d29 | ||
|
|
24cbc6c5e1 | ||
|
|
41edc23820 | ||
|
|
aa920b5e9b | ||
|
|
87ace28bcf | ||
|
|
5917f5fad1 | ||
|
|
f69c264317 | ||
|
|
a7246b9d98 | ||
|
|
9aff388e21 | ||
|
|
091aff1b8a | ||
|
|
9f8185f67b | ||
|
|
b112d75a27 | ||
|
|
7af81a6c18 | ||
|
|
12a218bb2b | ||
|
|
f6bac93db7 | ||
|
|
155d3d49aa | ||
|
|
9aae2210a2 | ||
|
|
3785a629ce | ||
|
|
02af52671f | ||
|
|
391c9d727e | ||
|
|
ff2b2be953 | ||
|
|
6242b30f93 | ||
|
|
11cd08a9de | ||
|
|
46cfbcb115 | ||
|
|
66543cc671 | ||
|
|
13283c9690 | ||
|
|
bedfb22466 | ||
|
|
13df8b2248 | ||
|
|
93406e4d4e | ||
|
|
8140d08557 | ||
|
|
2b36f39848 | ||
|
|
a69811800d | ||
|
|
3f18ad50de | ||
|
|
6689e326ee | ||
|
|
bdcab22a55 | ||
|
|
f5f4fe7259 | ||
|
|
898963fea5 | ||
|
|
8527bfa29c | ||
|
|
baca3cdec2 | ||
|
|
dc93eba34c | ||
|
|
012052daa0 | ||
|
|
a9546e31ee | ||
|
|
a4a6283e7b | ||
|
|
00c02fd16e | ||
|
|
140d150bb4 | ||
|
|
49a6d935f3 | ||
|
|
692beacc2e | ||
|
|
a953febfba | ||
|
|
c0aa2e36ea | ||
|
|
278bf43b29 | ||
|
|
a97d9e4953 | ||
|
|
30e3cd1f38 | ||
|
|
62e9dc164d | ||
|
|
9fc572ba0c | ||
|
|
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 |
220 changed files with 6054 additions and 1373 deletions
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -20,7 +20,7 @@ jobs:
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
- name: Download Dalamud
|
- name: Download Dalamud
|
||||||
run: |
|
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"
|
Expand-Archive -Force latest.zip "$env:AppData\XIVLauncher\addon\Hooks\dev"
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
2
OtterGui
2
OtterGui
|
|
@ -1 +1 @@
|
||||||
Subproject commit f130c928928cb0d48d3c807b7df5874c2460fe98
|
Subproject commit a63f6735cf4bed4f7502a022a10378607082b770
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1465203967d08519c6716292bc5e5094c7fbcacc
|
Subproject commit 3d6cee1a11922ccd426f36060fd026bc1a698adf
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
<Project Sdk="Dalamud.NET.Sdk/12.0.2">
|
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Exe</OutputType>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 8e57c2e12570bb1795efb9e5c6e38617aa8dd5e3
|
Subproject commit d889f9ef918514a46049725052d378b441915b00
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit 0e5dcd1a5687ec5f8fa2ef2526b94b9a0ea1b5b5
|
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\group_single.json = schemas\structs\group_single.json
|
||||||
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
schemas\structs\manipulation.json = schemas\structs\manipulation.json
|
||||||
schemas\structs\meta_atch.json = schemas\structs\meta_atch.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_enums.json = schemas\structs\meta_enums.json
|
||||||
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
schemas\structs\meta_eqdp.json = schemas\structs\meta_eqdp.json
|
||||||
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
schemas\structs\meta_eqp.json = schemas\structs\meta_eqp.json
|
||||||
|
|
|
||||||
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.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.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.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);
|
return Functions.ToCompressedBase64(array, 0);
|
||||||
|
|
@ -111,6 +113,8 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteCache(zipStream, cache.Atch);
|
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>
|
/// <summary>
|
||||||
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
/// Convert manipulations from a transmitted base64 string to actual manipulations.
|
||||||
/// The empty string is treated as an empty set.
|
/// 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 0: return ConvertManipsV0(data, out manips);
|
||||||
case 1: return ConvertManipsV1(data, out manips);
|
case 1: return ConvertManipsV1(data, out manips);
|
||||||
|
case 2: return ConvertManipsV2(data, out manips);
|
||||||
default:
|
default:
|
||||||
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
Penumbra.Log.Debug($"Invalid version for manipulations: {version}.");
|
||||||
manips = null;
|
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)
|
private static bool ConvertManipsV1(ReadOnlySpan<byte> data, [NotNullWhen(true)] out MetaDictionary? manips)
|
||||||
{
|
{
|
||||||
if (!data.StartsWith("META0001"u8))
|
if (!data.StartsWith("META0001"u8))
|
||||||
|
|
@ -269,6 +479,28 @@ public class MetaApi(IFramework framework, CollectionResolver collectionResolver
|
||||||
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
if (!identifier.Validate() || !manips.TryAdd(identifier, value))
|
||||||
return false;
|
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;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Compression;
|
using OtterGui.Compression;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
|
@ -33,12 +34,8 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ModPathChangeType.Deleted when oldDirectory != null:
|
case ModPathChangeType.Deleted when oldDirectory != null: ModDeleted?.Invoke(oldDirectory.Name); break;
|
||||||
ModDeleted?.Invoke(oldDirectory.Name);
|
case ModPathChangeType.Added when newDirectory != null: ModAdded?.Invoke(newDirectory.Name); break;
|
||||||
break;
|
|
||||||
case ModPathChangeType.Added when newDirectory != null:
|
|
||||||
ModAdded?.Invoke(newDirectory.Name);
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
case ModPathChangeType.Moved when newDirectory != null && oldDirectory != null:
|
||||||
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
ModMoved?.Invoke(oldDirectory.Name, newDirectory.Name);
|
||||||
break;
|
break;
|
||||||
|
|
@ -46,7 +43,9 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
=> _communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
{
|
||||||
|
_communicator.ModPathChanged.Unsubscribe(OnModPathChanged);
|
||||||
|
}
|
||||||
|
|
||||||
public Dictionary<string, string> GetModList()
|
public Dictionary<string, string> GetModList()
|
||||||
=> _modManager.ToDictionary(m => m.ModPath.Name, m => m.Name.Text);
|
=> _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>? ModAdded;
|
||||||
public event Action<string, string>? ModMoved;
|
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)
|
public (PenumbraApiEc, string, bool, bool) GetModPath(string modDirectory, string modName)
|
||||||
{
|
{
|
||||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
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);
|
return (PenumbraApiEc.ModMissing, string.Empty, false, false);
|
||||||
|
|
||||||
var fullPath = leaf.FullName();
|
var fullPath = leaf.FullName();
|
||||||
|
|
@ -127,7 +138,7 @@ public class ModsApi : IPenumbraApiMods, IApiService, IDisposable
|
||||||
return PenumbraApiEc.InvalidArgument;
|
return PenumbraApiEc.InvalidArgument;
|
||||||
|
|
||||||
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
if (!_modManager.TryGetMod(modDirectory, modName, out var mod)
|
||||||
|| !_modFileSystem.FindLeaf(mod, out var leaf))
|
|| !_modFileSystem.TryGetValue(mod, out var leaf))
|
||||||
return PenumbraApiEc.ModMissing;
|
return PenumbraApiEc.ModMissing;
|
||||||
|
|
||||||
try
|
try
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ public class PenumbraApi(
|
||||||
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
UiApi ui) : IDisposable, IApiService, IPenumbraApi
|
||||||
{
|
{
|
||||||
public const int BreakingVersion = 5;
|
public const int BreakingVersion = 5;
|
||||||
public const int FeatureVersion = 9;
|
public const int FeatureVersion = 13;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,38 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
using Penumbra.Mods;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Api.Api;
|
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()
|
public string GetModDirectory()
|
||||||
=> _config.ModDirectory;
|
=> config.ModDirectory;
|
||||||
|
|
||||||
public string GetConfiguration()
|
public string GetConfiguration()
|
||||||
=> JsonConvert.SerializeObject(_config, Formatting.Indented);
|
=> JsonConvert.SerializeObject(config, Formatting.Indented);
|
||||||
|
|
||||||
public event Action<string, bool>? ModDirectoryChanged
|
public event Action<string, bool>? ModDirectoryChanged
|
||||||
{
|
{
|
||||||
add => _communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
add => communicator.ModDirectoryChanged.Subscribe(value!, Communication.ModDirectoryChanged.Priority.Api);
|
||||||
remove => _communicator.ModDirectoryChanged.Unsubscribe(value!);
|
remove => communicator.ModDirectoryChanged.Unsubscribe(value!);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool GetEnabledState()
|
public bool GetEnabledState()
|
||||||
=> _config.EnableMods;
|
=> config.EnableMods;
|
||||||
|
|
||||||
public event Action<bool>? EnabledChange
|
public event Action<bool>? EnabledChange
|
||||||
{
|
{
|
||||||
add => _communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
add => communicator.EnabledChanged.Subscribe(value!, EnabledChanged.Priority.Api);
|
||||||
remove => _communicator.EnabledChanged.Unsubscribe(value!);
|
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();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,14 @@ using Dalamud.Game.ClientState.Objects.Types;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Collections.Manager;
|
||||||
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.Interop.Services;
|
using Penumbra.Interop.Services;
|
||||||
|
|
||||||
namespace Penumbra.Api.Api;
|
namespace Penumbra.Api.Api;
|
||||||
|
|
||||||
public class RedrawApi(RedrawService redrawService, IFramework framework) : IPenumbraApiRedraw, IApiService
|
public class RedrawApi(RedrawService redrawService, IFramework framework, CollectionManager collections, ObjectManager objects, ApiHelpers helpers) : IPenumbraApiRedraw, IApiService
|
||||||
{
|
{
|
||||||
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
public void RedrawObject(int gameObjectIndex, RedrawType setting)
|
||||||
{
|
{
|
||||||
|
|
@ -28,9 +31,27 @@ public class RedrawApi(RedrawService redrawService, IFramework framework) : IPen
|
||||||
framework.RunOnFrameworkThread(() => 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
|
public event GameObjectRedrawnDelegate? GameObjectRedrawn
|
||||||
{
|
{
|
||||||
add => redrawService.GameObjectRedrawn += value;
|
add => redrawService.GameObjectRedrawn += value;
|
||||||
remove => redrawService.GameObjectRedrawn -= value;
|
remove => redrawService.GameObjectRedrawn -= value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,8 +20,16 @@ public class TemporaryApi(
|
||||||
ApiHelpers apiHelpers,
|
ApiHelpers apiHelpers,
|
||||||
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
ModManager modManager) : IPenumbraApiTemporary, IApiService
|
||||||
{
|
{
|
||||||
public Guid CreateTemporaryCollection(string name)
|
public (PenumbraApiEc, Guid) CreateTemporaryCollection(string identity, string name)
|
||||||
=> tempCollections.CreateTemporaryCollection(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)
|
public PenumbraApiEc DeleteTemporaryCollection(Guid collectionId)
|
||||||
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
=> tempCollections.RemoveTemporaryCollection(collectionId)
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using EmbedIO.WebApi;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.Mods.Settings;
|
||||||
|
|
||||||
namespace Penumbra.Api;
|
namespace Penumbra.Api;
|
||||||
|
|
||||||
|
|
@ -13,12 +14,15 @@ public class HttpApi : IDisposable, IApiService
|
||||||
private partial class Controller : WebApiController
|
private partial class Controller : WebApiController
|
||||||
{
|
{
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
[Route( HttpVerbs.Get, "/moddirectory" )] public partial string GetModDirectory();
|
||||||
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
[Route( HttpVerbs.Get, "/mods" )] public partial object? GetMods();
|
||||||
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
[Route( HttpVerbs.Post, "/redraw" )] public partial Task Redraw();
|
||||||
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
[Route( HttpVerbs.Post, "/redrawAll" )] public partial Task RedrawAll();
|
||||||
[Route( HttpVerbs.Post, "/installmod" )] public partial Task InstallMod();
|
[Route( HttpVerbs.Post, "/reloadmod" )] public partial Task ReloadMod();
|
||||||
[Route( HttpVerbs.Post, "/openwindow" )] public partial void OpenWindow();
|
[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
|
// @formatter:on
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +68,12 @@ public class HttpApi : IDisposable, IApiService
|
||||||
|
|
||||||
private partial class Controller(IPenumbraApi api, IFramework framework)
|
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()
|
public partial object? GetMods()
|
||||||
{
|
{
|
||||||
Penumbra.Log.Debug($"[HTTP] {nameof(GetMods)} triggered.");
|
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);
|
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)
|
private record ModReloadData(string Path, string Name)
|
||||||
{
|
{
|
||||||
public ModReloadData()
|
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)
|
private record ModInstallData(string Path)
|
||||||
{
|
{
|
||||||
public ModInstallData()
|
public ModInstallData()
|
||||||
|
|
@ -136,5 +185,19 @@ public class HttpApi : IDisposable, IApiService
|
||||||
: this(string.Empty, RedrawType.Redraw, -1)
|
: this(string.Empty, RedrawType.Redraw, -1)
|
||||||
{ }
|
{ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private record SetModSettingsData(
|
||||||
|
Guid? CollectionId,
|
||||||
|
string ModPath,
|
||||||
|
string ModName,
|
||||||
|
bool? Inherit,
|
||||||
|
bool? State,
|
||||||
|
int? Priority,
|
||||||
|
Dictionary<string, List<string>>? Settings)
|
||||||
|
{
|
||||||
|
public SetModSettingsData()
|
||||||
|
: this(null, string.Empty, string.Empty, null, null, null, null)
|
||||||
|
{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ public sealed class IpcProviders : IDisposable, IApiService
|
||||||
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
|
IpcSubscribers.ModDeleted.Provider(pi, api.Mods),
|
||||||
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
|
IpcSubscribers.ModAdded.Provider(pi, api.Mods),
|
||||||
IpcSubscribers.ModMoved.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.GetModPath.Provider(pi, api.Mods),
|
||||||
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
|
IpcSubscribers.SetModPath.Provider(pi, api.Mods),
|
||||||
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
|
IpcSubscribers.GetChangedItems.Provider(pi, api.Mods),
|
||||||
|
|
@ -80,10 +82,13 @@ public sealed class IpcProviders : IDisposable, IApiService
|
||||||
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
|
IpcSubscribers.ModDirectoryChanged.Provider(pi, api.PluginState),
|
||||||
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
|
IpcSubscribers.GetEnabledState.Provider(pi, api.PluginState),
|
||||||
IpcSubscribers.EnabledChange.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.RedrawObject.Provider(pi, api.Redraw),
|
||||||
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
IpcSubscribers.RedrawAll.Provider(pi, api.Redraw),
|
||||||
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
IpcSubscribers.GameObjectRedrawn.Provider(pi, api.Redraw),
|
||||||
|
IpcSubscribers.RedrawCollectionMembers.Provider(pi, api.Redraw),
|
||||||
|
|
||||||
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
IpcSubscribers.ResolveDefaultPath.Provider(pi, api.Resolve),
|
||||||
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
IpcSubscribers.ResolveInterfacePath.Provider(pi, api.Resolve),
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
|
@ -121,6 +121,10 @@ public class CollectionsIpcTester(IDalamudPluginInterface pi) : IUiService
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
ImGui.OpenPopup("Changed Item List");
|
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()
|
private void DrawChangedItemPopup()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
using FFXIVClientStructs.FFXIV.Client.System.Framework;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
|
using OtterGui.Text;
|
||||||
using Penumbra.Api.Helpers;
|
using Penumbra.Api.Helpers;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
||||||
|
|
@ -12,7 +13,7 @@ namespace Penumbra.Api.IpcTester;
|
||||||
|
|
||||||
public class PluginStateIpcTester : IUiService, IDisposable
|
public class PluginStateIpcTester : IUiService, IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDalamudPluginInterface _pi;
|
private readonly IDalamudPluginInterface _pi;
|
||||||
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
|
public readonly EventSubscriber<string, bool> ModDirectoryChanged;
|
||||||
public readonly EventSubscriber Initialized;
|
public readonly EventSubscriber Initialized;
|
||||||
public readonly EventSubscriber Disposed;
|
public readonly EventSubscriber Disposed;
|
||||||
|
|
@ -26,6 +27,9 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
||||||
private readonly List<DateTimeOffset> _initializedList = [];
|
private readonly List<DateTimeOffset> _initializedList = [];
|
||||||
private readonly List<DateTimeOffset> _disposedList = [];
|
private readonly List<DateTimeOffset> _disposedList = [];
|
||||||
|
|
||||||
|
private string _requiredFeatureString = string.Empty;
|
||||||
|
private string[] _requiredFeatures = [];
|
||||||
|
|
||||||
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
|
private DateTimeOffset _lastEnabledChange = DateTimeOffset.UnixEpoch;
|
||||||
private bool? _lastEnabledValue;
|
private bool? _lastEnabledValue;
|
||||||
|
|
||||||
|
|
@ -48,12 +52,15 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
||||||
EnabledChange.Dispose();
|
EnabledChange.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Draw()
|
public void Draw()
|
||||||
{
|
{
|
||||||
using var _ = ImRaii.TreeNode("Plugin State");
|
using var _ = ImRaii.TreeNode("Plugin State");
|
||||||
if (!_)
|
if (!_)
|
||||||
return;
|
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);
|
using var table = ImRaii.Table(string.Empty, 3, ImGuiTableFlags.SizingFixedFit);
|
||||||
if (!table)
|
if (!table)
|
||||||
return;
|
return;
|
||||||
|
|
@ -71,6 +78,12 @@ public class PluginStateIpcTester : IUiService, IDisposable
|
||||||
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
|
IpcTester.DrawIntro(IpcSubscribers.EnabledChange.Label, "Last Change");
|
||||||
ImGui.TextUnformatted(_lastEnabledValue is { } v ? $"{_lastEnabledChange} (to {v})" : "Never");
|
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();
|
DrawConfigPopup();
|
||||||
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
|
IpcTester.DrawIntro(GetConfiguration.Label, "Configuration");
|
||||||
if (ImGui.Button("Get"))
|
if (ImGui.Button("Get"))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.IpcSubscribers;
|
using Penumbra.Api.IpcSubscribers;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Game.ClientState.Objects.Enums;
|
using Dalamud.Game.ClientState.Objects.Enums;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Extensions;
|
using OtterGui.Extensions;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Extensions;
|
using OtterGui.Extensions;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
|
@ -38,6 +38,7 @@ public class TemporaryIpcTester(
|
||||||
private string _tempGamePath = "test/game/path.mtrl";
|
private string _tempGamePath = "test/game/path.mtrl";
|
||||||
private string _tempFilePath = "test/success.mtrl";
|
private string _tempFilePath = "test/success.mtrl";
|
||||||
private string _tempManipulation = string.Empty;
|
private string _tempManipulation = string.Empty;
|
||||||
|
private string _identity = string.Empty;
|
||||||
private PenumbraApiEc _lastTempError;
|
private PenumbraApiEc _lastTempError;
|
||||||
private int _tempActorIndex;
|
private int _tempActorIndex;
|
||||||
private bool _forceOverwrite;
|
private bool _forceOverwrite;
|
||||||
|
|
@ -48,6 +49,7 @@ public class TemporaryIpcTester(
|
||||||
if (!_)
|
if (!_)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
ImGui.InputTextWithHint("##identity", "Identity...", ref _identity, 128);
|
||||||
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
ImGui.InputTextWithHint("##tempCollection", "Collection Name...", ref _tempCollectionName, 128);
|
||||||
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
ImGuiUtil.GuidInput("##guid", "Collection GUID...", string.Empty, ref _tempGuid, ref _tempCollectionGuidName);
|
||||||
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
ImGui.InputInt("##tempActorIndex", ref _tempActorIndex, 0, 0);
|
||||||
|
|
@ -73,7 +75,7 @@ public class TemporaryIpcTester(
|
||||||
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
|
IpcTester.DrawIntro(CreateTemporaryCollection.Label, "Create Temporary Collection");
|
||||||
if (ImGui.Button("Create##Collection"))
|
if (ImGui.Button("Create##Collection"))
|
||||||
{
|
{
|
||||||
LastCreatedCollectionId = new CreateTemporaryCollection(pi).Invoke(_tempCollectionName);
|
_lastTempError = new CreateTemporaryCollection(pi).Invoke(_identity, _tempCollectionName, out LastCreatedCollectionId);
|
||||||
if (_tempGuid == null)
|
if (_tempGuid == null)
|
||||||
{
|
{
|
||||||
_tempGuid = LastCreatedCollectionId;
|
_tempGuid = LastCreatedCollectionId;
|
||||||
|
|
@ -282,7 +284,7 @@ public class TemporaryIpcTester(
|
||||||
foreach (var mod in list)
|
foreach (var mod in list)
|
||||||
{
|
{
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(mod.Name);
|
ImGui.TextUnformatted(mod.Name.Text);
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
ImGui.TextUnformatted(mod.Priority.ToString());
|
ImGui.TextUnformatted(mod.Priority.ToString());
|
||||||
ImGui.TableNextColumn();
|
ImGui.TableNextColumn();
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Text;
|
using OtterGui.Text;
|
||||||
|
|
||||||
namespace Penumbra;
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -247,6 +247,8 @@ public sealed class CollectionCache : IDisposable
|
||||||
AddManipulation(mod, identifier, entry);
|
AddManipulation(mod, identifier, entry);
|
||||||
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
foreach (var (identifier, entry) in files.Manipulations.Shp)
|
||||||
AddManipulation(mod, identifier, entry);
|
AddManipulation(mod, identifier, entry);
|
||||||
|
foreach (var (identifier, entry) in files.Manipulations.Atr)
|
||||||
|
AddManipulation(mod, identifier, entry);
|
||||||
foreach (var identifier in files.Manipulations.GlobalEqp)
|
foreach (var identifier in files.Manipulations.GlobalEqp)
|
||||||
AddManipulation(mod, identifier, null!);
|
AddManipulation(mod, identifier, null!);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
|
private readonly HashSet<PrimaryId> _doNotHideRingR = [];
|
||||||
private bool _doNotHideVieraHats;
|
private bool _doNotHideVieraHats;
|
||||||
private bool _doNotHideHrothgarHats;
|
private bool _doNotHideHrothgarHats;
|
||||||
|
private bool _hideAuRaHorns;
|
||||||
|
private bool _hideVieraEars;
|
||||||
|
private bool _hideMiqoteEars;
|
||||||
|
|
||||||
public new void Clear()
|
public new void Clear()
|
||||||
{
|
{
|
||||||
|
|
@ -26,6 +29,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
_doNotHideRingR.Clear();
|
_doNotHideRingR.Clear();
|
||||||
_doNotHideHrothgarHats = false;
|
_doNotHideHrothgarHats = false;
|
||||||
_doNotHideVieraHats = false;
|
_doNotHideVieraHats = false;
|
||||||
|
_hideAuRaHorns = false;
|
||||||
|
_hideVieraEars = false;
|
||||||
|
_hideMiqoteEars = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
|
public unsafe EqpEntry Apply(EqpEntry original, CharacterArmor* armor)
|
||||||
|
|
@ -39,8 +45,20 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
if (_doNotHideHrothgarHats)
|
if (_doNotHideHrothgarHats)
|
||||||
original |= EqpEntry.HeadShowHrothgarHat;
|
original |= EqpEntry.HeadShowHrothgarHat;
|
||||||
|
|
||||||
|
if (_hideAuRaHorns)
|
||||||
|
original &= ~EqpEntry.HeadShowEarAuRa;
|
||||||
|
|
||||||
|
if (_hideVieraEars)
|
||||||
|
original &= ~EqpEntry.HeadShowEarViera;
|
||||||
|
|
||||||
|
if (_hideMiqoteEars)
|
||||||
|
original &= ~EqpEntry.HeadShowEarMiqote;
|
||||||
|
|
||||||
if (_doNotHideEarrings.Contains(armor[5].Set))
|
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))
|
if (_doNotHideNecklace.Contains(armor[6].Set))
|
||||||
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
original |= EqpEntry.BodyShowNecklace | EqpEntry.HeadShowNecklace;
|
||||||
|
|
@ -53,6 +71,7 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
|
|
||||||
if (_doNotHideRingL.Contains(armor[9].Set))
|
if (_doNotHideRingL.Contains(armor[9].Set))
|
||||||
original |= EqpEntry.HandShowRingL;
|
original |= EqpEntry.HandShowRingL;
|
||||||
|
|
||||||
return original;
|
return original;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -71,6 +90,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
|
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Add(manipulation.Condition),
|
||||||
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
|
GlobalEqpType.DoNotHideHrothgarHats => !_doNotHideHrothgarHats && (_doNotHideHrothgarHats = true),
|
||||||
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
|
GlobalEqpType.DoNotHideVieraHats => !_doNotHideVieraHats && (_doNotHideVieraHats = true),
|
||||||
|
GlobalEqpType.HideHorns => !_hideAuRaHorns && (_hideAuRaHorns = true),
|
||||||
|
GlobalEqpType.HideMiqoteEars => !_hideMiqoteEars && (_hideMiqoteEars = true),
|
||||||
|
GlobalEqpType.HideVieraEars => !_hideVieraEars && (_hideVieraEars = true),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
|
|
@ -90,6 +112,9 @@ public class GlobalEqpCache : ReadWriteDictionary<GlobalEqpManipulation, IMod>,
|
||||||
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
|
GlobalEqpType.DoNotHideRingL => _doNotHideRingL.Remove(manipulation.Condition),
|
||||||
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
|
GlobalEqpType.DoNotHideHrothgarHats => _doNotHideHrothgarHats && !(_doNotHideHrothgarHats = false),
|
||||||
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
|
GlobalEqpType.DoNotHideVieraHats => _doNotHideVieraHats && !(_doNotHideVieraHats = false),
|
||||||
|
GlobalEqpType.HideHorns => _hideAuRaHorns && (_hideAuRaHorns = false),
|
||||||
|
GlobalEqpType.HideMiqoteEars => _hideMiqoteEars && (_hideMiqoteEars = false),
|
||||||
|
GlobalEqpType.HideVieraEars => _hideVieraEars && (_hideVieraEars = false),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -17,11 +17,12 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
public readonly ImcCache Imc = new(manager, collection);
|
public readonly ImcCache Imc = new(manager, collection);
|
||||||
public readonly AtchCache Atch = new(manager, collection);
|
public readonly AtchCache Atch = new(manager, collection);
|
||||||
public readonly ShpCache Shp = new(manager, collection);
|
public readonly ShpCache Shp = new(manager, collection);
|
||||||
|
public readonly AtrCache Atr = new(manager, collection);
|
||||||
public readonly GlobalEqpCache GlobalEqp = new();
|
public readonly GlobalEqpCache GlobalEqp = new();
|
||||||
public bool IsDisposed { get; private set; }
|
public bool IsDisposed { get; private set; }
|
||||||
|
|
||||||
public int Count
|
public int Count
|
||||||
=> Eqp.Count + Eqdp.Count + Est.Count + Gmp.Count + Rsp.Count + Imc.Count + Atch.Count + Shp.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
|
public IEnumerable<(IMetaIdentifier, IMod)> IdentifierSources
|
||||||
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
=> Eqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source))
|
||||||
|
|
@ -32,6 +33,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
.Concat(Imc.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(Atch.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value.Source)))
|
||||||
.Concat(Shp.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)));
|
.Concat(GlobalEqp.Select(kvp => ((IMetaIdentifier)kvp.Key, kvp.Value)));
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
|
|
@ -44,6 +46,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
Imc.Reset();
|
Imc.Reset();
|
||||||
Atch.Reset();
|
Atch.Reset();
|
||||||
Shp.Reset();
|
Shp.Reset();
|
||||||
|
Atr.Reset();
|
||||||
GlobalEqp.Clear();
|
GlobalEqp.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,6 +64,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
Imc.Dispose();
|
Imc.Dispose();
|
||||||
Atch.Dispose();
|
Atch.Dispose();
|
||||||
Shp.Dispose();
|
Shp.Dispose();
|
||||||
|
Atr.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
public bool TryGetMod(IMetaIdentifier identifier, [NotNullWhen(true)] out IMod? mod)
|
||||||
|
|
@ -76,6 +80,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
RspIdentifier i => Rsp.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),
|
AtchIdentifier i => Atch.TryGetValue(i, out var p) && Convert(p, out mod),
|
||||||
ShpIdentifier i => Shp.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),
|
GlobalEqpManipulation i => GlobalEqp.TryGetValue(i, out mod),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
@ -98,6 +103,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
RspIdentifier i => Rsp.RevertMod(i, out mod),
|
||||||
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
AtchIdentifier i => Atch.RevertMod(i, out mod),
|
||||||
ShpIdentifier i => Shp.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),
|
GlobalEqpManipulation i => GlobalEqp.RevertMod(i, out mod),
|
||||||
_ => (mod = null) != null,
|
_ => (mod = null) != null,
|
||||||
};
|
};
|
||||||
|
|
@ -115,6 +121,7 @@ public class MetaCache(MetaFileManager manager, ModCollection collection)
|
||||||
RspIdentifier i when entry is RspEntry e => Rsp.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),
|
AtchIdentifier i when entry is AtchEntry e => Atch.ApplyMod(mod, i, e),
|
||||||
ShpIdentifier i when entry is ShpEntry e => Shp.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),
|
GlobalEqpManipulation i => GlobalEqp.ApplyMod(mod, i),
|
||||||
_ => false,
|
_ => 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,162 +7,100 @@ namespace Penumbra.Collections.Cache;
|
||||||
|
|
||||||
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
|
public sealed class ShpCache(MetaFileManager manager, ModCollection collection) : MetaCacheBase<ShpIdentifier, ShpEntry>(manager, collection)
|
||||||
{
|
{
|
||||||
public bool ShouldBeEnabled(in ShapeString shape, HumanSlot slot, PrimaryId id)
|
public bool ShouldBeEnabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||||
=> _shpData.TryGetValue(shape, out var value) && value.Contains(slot, id);
|
=> EnabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is true;
|
||||||
|
|
||||||
internal IReadOnlyDictionary<ShapeString, ShpHashSet> State
|
public bool ShouldBeDisabled(in ShapeAttributeString shape, HumanSlot slot, PrimaryId id, GenderRace genderRace)
|
||||||
=> _shpData;
|
=> DisabledCount > 0 && _shpData.TryGetValue(shape, out var value) && value.CheckEntry(slot, id, genderRace) is false;
|
||||||
|
|
||||||
internal IEnumerable<(ShapeString, IReadOnlyDictionary<ShapeString, ShpHashSet>)> ConditionState
|
internal IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> State(ShapeConnectorCondition connector)
|
||||||
=> _conditionalSet.Select(kvp => (kvp.Key, (IReadOnlyDictionary<ShapeString, ShpHashSet>)kvp.Value));
|
=> connector switch
|
||||||
|
|
||||||
public bool CheckConditionState(ShapeString condition, [NotNullWhen(true)] out IReadOnlyDictionary<ShapeString, ShpHashSet>? dict)
|
|
||||||
{
|
|
||||||
if (_conditionalSet.TryGetValue(condition, out var d))
|
|
||||||
{
|
{
|
||||||
dict = d;
|
ShapeConnectorCondition.None => _shpData,
|
||||||
return true;
|
ShapeConnectorCondition.Wrists => _wristConnectors,
|
||||||
}
|
ShapeConnectorCondition.Waist => _waistConnectors,
|
||||||
|
ShapeConnectorCondition.Ankles => _ankleConnectors,
|
||||||
|
_ => [],
|
||||||
|
};
|
||||||
|
|
||||||
dict = null;
|
public int EnabledCount { get; private set; }
|
||||||
return false;
|
public int DisabledCount { get; private set; }
|
||||||
}
|
|
||||||
|
|
||||||
|
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _shpData = [];
|
||||||
public sealed class ShpHashSet : HashSet<(HumanSlot Slot, PrimaryId Id)>
|
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _wristConnectors = [];
|
||||||
{
|
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _waistConnectors = [];
|
||||||
private readonly BitArray _allIds = new(ShapeManager.ModelSlotSize);
|
private readonly Dictionary<ShapeAttributeString, ShapeAttributeHashSet> _ankleConnectors = [];
|
||||||
|
|
||||||
public bool All
|
|
||||||
{
|
|
||||||
get => _allIds[^1];
|
|
||||||
set => _allIds[^1] = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool this[HumanSlot slot]
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (slot is HumanSlot.Unknown)
|
|
||||||
return All;
|
|
||||||
|
|
||||||
return _allIds[(int)slot];
|
|
||||||
}
|
|
||||||
set
|
|
||||||
{
|
|
||||||
if (slot is HumanSlot.Unknown)
|
|
||||||
_allIds[^1] = value;
|
|
||||||
else
|
|
||||||
_allIds[(int)slot] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Contains(HumanSlot slot, PrimaryId id)
|
|
||||||
=> All || this[slot] || Contains((slot, id));
|
|
||||||
|
|
||||||
public bool TrySet(HumanSlot slot, PrimaryId? id, ShpEntry value)
|
|
||||||
{
|
|
||||||
if (slot is HumanSlot.Unknown)
|
|
||||||
{
|
|
||||||
var old = All;
|
|
||||||
All = value.Value;
|
|
||||||
return old != value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!id.HasValue)
|
|
||||||
{
|
|
||||||
var old = this[slot];
|
|
||||||
this[slot] = value.Value;
|
|
||||||
return old != value.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value.Value)
|
|
||||||
return Add((slot, id.Value));
|
|
||||||
|
|
||||||
return Remove((slot, id.Value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public new void Clear()
|
|
||||||
{
|
|
||||||
base.Clear();
|
|
||||||
_allIds.SetAll(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsEmpty
|
|
||||||
=> !_allIds.HasAnySet() && Count is 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<ShapeString, ShpHashSet> _shpData = [];
|
|
||||||
private readonly Dictionary<ShapeString, Dictionary<ShapeString, ShpHashSet>> _conditionalSet = [];
|
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Clear();
|
Clear();
|
||||||
_shpData.Clear();
|
_shpData.Clear();
|
||||||
_conditionalSet.Clear();
|
_wristConnectors.Clear();
|
||||||
|
_waistConnectors.Clear();
|
||||||
|
_ankleConnectors.Clear();
|
||||||
|
EnabledCount = 0;
|
||||||
|
DisabledCount = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool _)
|
protected override void Dispose(bool _)
|
||||||
=> Clear();
|
=> Reset();
|
||||||
|
|
||||||
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
protected override void ApplyModInternal(ShpIdentifier identifier, ShpEntry entry)
|
||||||
{
|
{
|
||||||
if (identifier.ShapeCondition.Length > 0)
|
switch (identifier.ConnectorCondition)
|
||||||
{
|
{
|
||||||
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
|
case ShapeConnectorCondition.None: Func(_shpData); break;
|
||||||
{
|
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
|
||||||
if (!entry.Value)
|
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
|
||||||
return;
|
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
|
||||||
|
|
||||||
shapes = new Dictionary<ShapeString, ShpHashSet>();
|
|
||||||
_conditionalSet.Add(identifier.ShapeCondition, shapes);
|
|
||||||
}
|
|
||||||
|
|
||||||
Func(shapes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Func(_shpData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
return;
|
||||||
|
|
||||||
|
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||||
{
|
{
|
||||||
if (!dict.TryGetValue(identifier.Shape, out var value))
|
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||||
{
|
{
|
||||||
if (!entry.Value)
|
|
||||||
return;
|
|
||||||
|
|
||||||
value = [];
|
value = [];
|
||||||
dict.Add(identifier.Shape, value);
|
dict.Add(identifier.Shape, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
value.TrySet(identifier.Slot, identifier.Id, entry);
|
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, entry.Value, out _))
|
||||||
|
{
|
||||||
|
if (entry.Value)
|
||||||
|
++EnabledCount;
|
||||||
|
else
|
||||||
|
++DisabledCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void RevertModInternal(ShpIdentifier identifier)
|
protected override void RevertModInternal(ShpIdentifier identifier)
|
||||||
{
|
{
|
||||||
if (identifier.ShapeCondition.Length > 0)
|
switch (identifier.ConnectorCondition)
|
||||||
{
|
{
|
||||||
if (!_conditionalSet.TryGetValue(identifier.ShapeCondition, out var shapes))
|
case ShapeConnectorCondition.None: Func(_shpData); break;
|
||||||
return;
|
case ShapeConnectorCondition.Wrists: Func(_wristConnectors); break;
|
||||||
|
case ShapeConnectorCondition.Waist: Func(_waistConnectors); break;
|
||||||
Func(shapes);
|
case ShapeConnectorCondition.Ankles: Func(_ankleConnectors); break;
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Func(_shpData);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
void Func(Dictionary<ShapeString, ShpHashSet> dict)
|
void Func(Dictionary<ShapeAttributeString, ShapeAttributeHashSet> dict)
|
||||||
{
|
{
|
||||||
if (!_shpData.TryGetValue(identifier.Shape, out var value))
|
if (!dict.TryGetValue(identifier.Shape, out var value))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (value.TrySet(identifier.Slot, identifier.Id, ShpEntry.False) && value.IsEmpty)
|
if (value.TrySet(identifier.Slot, identifier.Id, identifier.GenderRaceCondition, null, out var which))
|
||||||
_shpData.Remove(identifier.Shape);
|
{
|
||||||
|
if (which)
|
||||||
|
--EnabledCount;
|
||||||
|
else
|
||||||
|
--DisabledCount;
|
||||||
|
if (value.IsEmpty)
|
||||||
|
dict.Remove(identifier.Shape);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,8 +59,15 @@ public sealed class CollectionAutoSelector : IService, IDisposable
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var collection = _resolver.PlayerCollection();
|
var collection = _resolver.PlayerCollection();
|
||||||
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
if (collection.Identity.Id == Guid.Empty)
|
||||||
_collections.SetCollection(collection, CollectionType.Current);
|
{
|
||||||
|
Penumbra.Log.Debug($"Not setting current collection because character has no mods assigned.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Penumbra.Log.Debug($"Setting current collection to {collection.Identity.Identifier} through automatic collection selection.");
|
||||||
|
_collections.SetCollection(collection, CollectionType.Current);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
using Dalamud.Game.Text.SeStringHandling;
|
using Dalamud.Game.Text.SeStringHandling;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using Penumbra.Api;
|
||||||
using Penumbra.Api.Api;
|
using Penumbra.Api.Api;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.Services;
|
||||||
|
|
||||||
namespace Penumbra.Communication;
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
|
|
@ -20,11 +21,14 @@ public sealed class ModPathChanged()
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
|
/// <seealso cref="PcpService.OnModPathChange"/>
|
||||||
|
PcpService = int.MinValue,
|
||||||
|
|
||||||
/// <seealso cref="ModsApi.OnModPathChange"/>
|
/// <seealso cref="ModsApi.OnModPathChange"/>
|
||||||
ApiMods = int.MinValue,
|
ApiMods = int.MinValue + 1,
|
||||||
|
|
||||||
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
/// <seealso cref="ModSettingsApi.OnModPathChange"/>
|
||||||
ApiModSettings = int.MinValue,
|
ApiModSettings = int.MinValue + 1,
|
||||||
|
|
||||||
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
/// <seealso cref="EphemeralConfig.OnModPathChanged"/>
|
||||||
EphemeralConfig = -500,
|
EphemeralConfig = -500,
|
||||||
|
|
|
||||||
21
Penumbra/Communication/PcpCreation.cs
Normal file
21
Penumbra/Communication/PcpCreation.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggered when the character.json file for a .pcp file is written.
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Parameter is the JObject that gets written to file. </item>
|
||||||
|
/// <item>Parameter is the object index of the game object this is written for. </item>
|
||||||
|
/// <item>Parameter is the full path to the directory being set up for the PCP creation. </item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PcpCreation() : EventWrapper<JObject, ushort, string, PcpCreation.Priority>(nameof(PcpCreation))
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="Api.Api.ModsApi"/>
|
||||||
|
ModsApi = int.MinValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Penumbra/Communication/PcpParsing.cs
Normal file
21
Penumbra/Communication/PcpParsing.cs
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
|
||||||
|
namespace Penumbra.Communication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Triggered when the character.json file for a .pcp file is parsed and applied.
|
||||||
|
/// <list type="number">
|
||||||
|
/// <item>Parameter is parsed JObject that contains the data. </item>
|
||||||
|
/// <item>Parameter is the identifier of the created mod. </item>
|
||||||
|
/// <item>Parameter is the GUID of the created collection. </item>
|
||||||
|
/// </list>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PcpParsing() : EventWrapper<JObject, string, Guid, PcpParsing.Priority>(nameof(PcpParsing))
|
||||||
|
{
|
||||||
|
public enum Priority
|
||||||
|
{
|
||||||
|
/// <seealso cref="Api.Api.ModsApi"/>
|
||||||
|
ModsApi = int.MinValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,15 @@ using ErrorEventArgs = Newtonsoft.Json.Serialization.ErrorEventArgs;
|
||||||
|
|
||||||
namespace Penumbra;
|
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]
|
[Serializable]
|
||||||
public class Configuration : IPluginConfiguration, ISavable, IService
|
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 ModDirectory { get; set; } = string.Empty;
|
||||||
public string ExportDirectory { 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? UseCrashHandler { get; set; } = null;
|
||||||
public bool OpenWindowAtStart { get; set; } = false;
|
public bool OpenWindowAtStart { get; set; } = false;
|
||||||
|
|
@ -67,10 +77,13 @@ public class Configuration : IPluginConfiguration, ISavable, IService
|
||||||
public bool HideRedrawBar { get; set; } = false;
|
public bool HideRedrawBar { get; set; } = false;
|
||||||
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
public bool HideMachinistOffhandFromChangedItems { get; set; } = true;
|
||||||
public bool DefaultTemporaryMode { get; set; } = false;
|
public bool DefaultTemporaryMode { get; set; } = false;
|
||||||
|
public bool EnableDirectoryWatch { get; set; } = false;
|
||||||
|
public bool EnableAutomaticModImport { get; set; } = false;
|
||||||
public bool EnableCustomShapes { get; set; } = true;
|
public bool EnableCustomShapes { get; set; } = true;
|
||||||
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
public PcpSettings PcpSettings = new();
|
||||||
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
public RenameField ShowRename { get; set; } = RenameField.BothDataPrio;
|
||||||
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
public ChangedItemMode ChangedItemDisplay { get; set; } = ChangedItemMode.GroupedCollapsed;
|
||||||
|
public int OptionGroupCollapsibleMin { get; set; } = 5;
|
||||||
|
|
||||||
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
|
public Vector2 MinimumSize = new(Constants.MinimumSizeX, Constants.MinimumSizeY);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,6 @@ namespace Penumbra;
|
||||||
|
|
||||||
public class DebugConfiguration
|
public class DebugConfiguration
|
||||||
{
|
{
|
||||||
public static bool WriteImcBytesToLog = false;
|
public static bool WriteImcBytesToLog = false;
|
||||||
|
public static bool UseSkinMaterialProcessing = true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Lumina.Data.Parsing;
|
using Lumina.Data.Parsing;
|
||||||
using Penumbra.GameData.Files;
|
using Penumbra.GameData.Files;
|
||||||
using Penumbra.GameData.Files.MaterialStructs;
|
using Penumbra.GameData.Files.MaterialStructs;
|
||||||
|
using Penumbra.UI.AdvancedWindow.Materials;
|
||||||
using SharpGLTF.Materials;
|
using SharpGLTF.Materials;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
using SixLabors.ImageSharp.Advanced;
|
using SixLabors.ImageSharp.Advanced;
|
||||||
|
|
@ -140,13 +141,13 @@ public class MaterialExporter
|
||||||
|
|
||||||
// Lerp between table row values to fetch final pixel values for each subtexture.
|
// Lerp between table row values to fetch final pixel values for each subtexture.
|
||||||
var lerpedDiffuse = Vector3.Lerp((Vector3)prevRow.DiffuseColor, (Vector3)nextRow.DiffuseColor, rowBlend);
|
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);
|
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);
|
var lerpedEmissive = Vector3.Lerp((Vector3)prevRow.EmissiveColor, (Vector3)nextRow.EmissiveColor, rowBlend);
|
||||||
emissiveSpan[x].FromVector4(new Vector4(lerpedEmissive, 1));
|
emissiveSpan[x].FromVector4(new Vector4(MtrlTab.PseudoSqrtRgb(lerpedEmissive), 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -360,11 +360,11 @@ public class MeshExporter
|
||||||
// (Bi)tangents are universally stored as ByteFloat4, which uses 0..1 to represent the full -1..1 range.
|
// (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.
|
// TODO: While this assumption is safe, it would be sensible to actually check.
|
||||||
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
|
var bitangent = ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Tangent1)) * 2 - Vector4.One;
|
||||||
|
|
||||||
return new VertexPositionNormalTangent(
|
return new VertexPositionNormalTangent(
|
||||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Position)),
|
||||||
ToVector3(GetFirstSafe(attributes, MdlFile.VertexUsage.Normal)),
|
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 = (
|
var materialUsages = (
|
||||||
uvCount,
|
uvCount,
|
||||||
usages.ContainsKey(MdlFile.VertexUsage.Color)
|
nColors
|
||||||
);
|
);
|
||||||
|
|
||||||
return materialUsages switch
|
return materialUsages switch
|
||||||
{
|
{
|
||||||
(3, true) => typeof(VertexTexture3ColorFfxiv),
|
(3, 2) => typeof(VertexTexture3Color2Ffxiv),
|
||||||
(3, false) => typeof(VertexTexture3),
|
(3, 1) => typeof(VertexTexture3ColorFfxiv),
|
||||||
(2, true) => typeof(VertexTexture2ColorFfxiv),
|
(3, 0) => typeof(VertexTexture3),
|
||||||
(2, false) => typeof(VertexTexture2),
|
(2, 2) => typeof(VertexTexture2Color2Ffxiv),
|
||||||
(1, true) => typeof(VertexTexture1ColorFfxiv),
|
(2, 1) => typeof(VertexTexture2ColorFfxiv),
|
||||||
(1, false) => typeof(VertexTexture1),
|
(2, 0) => typeof(VertexTexture2),
|
||||||
(0, true) => typeof(VertexColorFfxiv),
|
(1, 2) => typeof(VertexTexture1Color2Ffxiv),
|
||||||
(0, false) => typeof(VertexEmpty),
|
(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))
|
if (_materialType == typeof(VertexColorFfxiv))
|
||||||
return new VertexColorFfxiv(ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color)));
|
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))
|
if (_materialType == typeof(VertexTexture1))
|
||||||
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
|
return new VertexTexture1(ToVector2(GetFirstSafe(attributes, MdlFile.VertexUsage.UV)));
|
||||||
|
|
||||||
|
|
@ -428,6 +441,16 @@ public class MeshExporter
|
||||||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
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.
|
// XIV packs two UVs into a single vec4 attribute.
|
||||||
|
|
||||||
if (_materialType == typeof(VertexTexture2))
|
if (_materialType == typeof(VertexTexture2))
|
||||||
|
|
@ -448,6 +471,20 @@ public class MeshExporter
|
||||||
ToVector4(GetFirstSafe(attributes, MdlFile.VertexUsage.Color))
|
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))
|
if (_materialType == typeof(VertexTexture3))
|
||||||
{
|
{
|
||||||
// Not 100% sure about this
|
// 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}");
|
throw _notifier.Exception($"Unknown material type {_skinningType}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -537,6 +589,17 @@ public class MeshExporter
|
||||||
|
|
||||||
return list[0];
|
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>
|
/// <summary> Convert a vertex attribute value to a Vector2. Supported inputs are Vector2, Vector3, and Vector4. </summary>
|
||||||
private static Vector2 ToVector2(object data)
|
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 struct VertexTexture1ColorFfxiv(Vector2 texCoord0, Vector4 ffxivColor) : IVertexCustom
|
||||||
{
|
{
|
||||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
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 struct VertexTexture2ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector4 ffxivColor) : IVertexCustom
|
||||||
{
|
{
|
||||||
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
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)
|
public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor)
|
||||||
: IVertexCustom
|
: IVertexCustom
|
||||||
{
|
{
|
||||||
|
|
@ -367,3 +694,126 @@ public struct VertexTexture3ColorFfxiv(Vector2 texCoord0, Vector2 texCoord1, Vec
|
||||||
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
throw new ArgumentOutOfRangeException(nameof(FfxivColor));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct VertexTexture3Color2Ffxiv(Vector2 texCoord0, Vector2 texCoord1, Vector2 texCoord2, Vector4 ffxivColor0, Vector4 ffxivColor1)
|
||||||
|
: IVertexCustom
|
||||||
|
{
|
||||||
|
public IEnumerable<KeyValuePair<string, AttributeFormat>> GetEncodingAttributes()
|
||||||
|
{
|
||||||
|
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_0",
|
||||||
|
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||||
|
yield return new KeyValuePair<string, AttributeFormat>("TEXCOORD_1",
|
||||||
|
new AttributeFormat(DimensionType.VEC2, EncodingType.FLOAT, false));
|
||||||
|
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_0",
|
||||||
|
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||||
|
yield return new KeyValuePair<string, AttributeFormat>("_FFXIV_COLOR_1",
|
||||||
|
new AttributeFormat(DimensionType.VEC4, EncodingType.UNSIGNED_SHORT, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector2 TexCoord0 = texCoord0;
|
||||||
|
public Vector2 TexCoord1 = texCoord1;
|
||||||
|
public Vector2 TexCoord2 = texCoord2;
|
||||||
|
public Vector4 FfxivColor0 = ffxivColor0;
|
||||||
|
public Vector4 FfxivColor1 = ffxivColor1;
|
||||||
|
|
||||||
|
public int MaxColors
|
||||||
|
=> 0;
|
||||||
|
|
||||||
|
public int MaxTextCoords
|
||||||
|
=> 3;
|
||||||
|
|
||||||
|
private static readonly string[] CustomNames = ["_FFXIV_COLOR_0", "_FFXIV_COLOR_1"];
|
||||||
|
|
||||||
|
public IEnumerable<string> CustomAttributes
|
||||||
|
=> CustomNames;
|
||||||
|
|
||||||
|
public void Add(in VertexMaterialDelta delta)
|
||||||
|
{
|
||||||
|
TexCoord0 += delta.TexCoord0Delta;
|
||||||
|
TexCoord1 += delta.TexCoord1Delta;
|
||||||
|
TexCoord2 += delta.TexCoord2Delta;
|
||||||
|
}
|
||||||
|
|
||||||
|
public VertexMaterialDelta Subtract(IVertexMaterial baseValue)
|
||||||
|
=> new(Vector4.Zero, Vector4.Zero, TexCoord0 - baseValue.GetTexCoord(0), TexCoord1 - baseValue.GetTexCoord(1));
|
||||||
|
|
||||||
|
public Vector2 GetTexCoord(int index)
|
||||||
|
=> index switch
|
||||||
|
{
|
||||||
|
0 => TexCoord0,
|
||||||
|
1 => TexCoord1,
|
||||||
|
2 => TexCoord2,
|
||||||
|
_ => throw new ArgumentOutOfRangeException(nameof(index)),
|
||||||
|
};
|
||||||
|
|
||||||
|
public void SetTexCoord(int setIndex, Vector2 coord)
|
||||||
|
{
|
||||||
|
if (setIndex == 0)
|
||||||
|
TexCoord0 = coord;
|
||||||
|
if (setIndex == 1)
|
||||||
|
TexCoord1 = coord;
|
||||||
|
if (setIndex == 2)
|
||||||
|
TexCoord2 = coord;
|
||||||
|
if (setIndex >= 3)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(setIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetCustomAttribute(string attributeName, out object? value)
|
||||||
|
{
|
||||||
|
switch (attributeName)
|
||||||
|
{
|
||||||
|
case "_FFXIV_COLOR_0":
|
||||||
|
value = FfxivColor0;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case "_FFXIV_COLOR_1":
|
||||||
|
value = FfxivColor1;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
value = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetCustomAttribute(string attributeName, object value)
|
||||||
|
{
|
||||||
|
switch (attributeName)
|
||||||
|
{
|
||||||
|
case "_FFXIV_COLOR_0" when value is Vector4 valueVector4:
|
||||||
|
FfxivColor0 = valueVector4;
|
||||||
|
break;
|
||||||
|
case "_FFXIV_COLOR_1" when value is Vector4 valueVector4:
|
||||||
|
FfxivColor1 = valueVector4;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector4 GetColor(int index)
|
||||||
|
=> throw new ArgumentOutOfRangeException(nameof(index));
|
||||||
|
|
||||||
|
public void SetColor(int setIndex, Vector4 color)
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
var components = new[]
|
||||||
|
{
|
||||||
|
FfxivColor0.X,
|
||||||
|
FfxivColor0.Y,
|
||||||
|
FfxivColor0.Z,
|
||||||
|
FfxivColor0.W,
|
||||||
|
};
|
||||||
|
if (components.Any(component => component is < 0 or > 1))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(FfxivColor0));
|
||||||
|
components =
|
||||||
|
[
|
||||||
|
FfxivColor1.X,
|
||||||
|
FfxivColor1.Y,
|
||||||
|
FfxivColor1.Z,
|
||||||
|
FfxivColor1.W,
|
||||||
|
];
|
||||||
|
if (components.Any(component => component is < 0 or > 1))
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(FfxivColor1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -319,7 +319,7 @@ public class VertexAttribute
|
||||||
|
|
||||||
var normals = normalAccessor.AsVector3Array();
|
var normals = normalAccessor.AsVector3Array();
|
||||||
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
var tangents = accessors.TryGetValue("TANGENT", out var accessor)
|
||||||
? accessor.AsVector4Array()
|
? accessor.AsVector4Array().ToArray()
|
||||||
: CalculateTangents(accessors, indices, normals, notifier);
|
: CalculateTangents(accessors, indices, normals, notifier);
|
||||||
|
|
||||||
if (tangents == null)
|
if (tangents == null)
|
||||||
|
|
|
||||||
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
69
Penumbra/Import/Models/ModelExtensions.cs
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
namespace Penumbra.Import.Models;
|
||||||
|
|
||||||
|
public static class ModelExtensions
|
||||||
|
{
|
||||||
|
// https://github.com/vpenades/SharpGLTF/blob/2073cf3cd671f8ecca9667f9a8c7f04ed865d3ac/src/Shared/_Extensions.cs#L158
|
||||||
|
private const float UnitLengthThresholdVec3 = 0.00674f;
|
||||||
|
private const float UnitLengthThresholdVec4 = 0.00769f;
|
||||||
|
|
||||||
|
internal static bool _IsFinite(this float value)
|
||||||
|
{
|
||||||
|
return float.IsFinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool _IsFinite(this Vector2 v)
|
||||||
|
{
|
||||||
|
return v.X._IsFinite() && v.Y._IsFinite();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool _IsFinite(this Vector3 v)
|
||||||
|
{
|
||||||
|
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool _IsFinite(this in Vector4 v)
|
||||||
|
{
|
||||||
|
return v.X._IsFinite() && v.Y._IsFinite() && v.Z._IsFinite() && v.W._IsFinite();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Boolean IsNormalized(this Vector3 normal)
|
||||||
|
{
|
||||||
|
if (!normal._IsFinite()) return false;
|
||||||
|
|
||||||
|
return Math.Abs(normal.Length() - 1) <= UnitLengthThresholdVec3;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ValidateNormal(this Vector3 normal, string msg)
|
||||||
|
{
|
||||||
|
if (!normal._IsFinite()) throw new NotFiniteNumberException($"{msg} is invalid.");
|
||||||
|
|
||||||
|
if (!normal.IsNormalized()) throw new ArithmeticException($"{msg} is not unit length.");
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void ValidateTangent(this Vector4 tangent, string msg)
|
||||||
|
{
|
||||||
|
if (tangent.W != 1 && tangent.W != -1) throw new ArithmeticException(msg);
|
||||||
|
|
||||||
|
new Vector3(tangent.X, tangent.Y, tangent.Z).ValidateNormal(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector3 SanitizeNormal(this Vector3 normal)
|
||||||
|
{
|
||||||
|
if (normal == Vector3.Zero) return Vector3.UnitX;
|
||||||
|
return normal.IsNormalized() ? normal : Vector3.Normalize(normal);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool IsValidTangent(this Vector4 tangent)
|
||||||
|
{
|
||||||
|
if (tangent.W != 1 && tangent.W != -1) return false;
|
||||||
|
|
||||||
|
return new Vector3(tangent.X, tangent.Y, tangent.Z).IsNormalized();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static Vector4 SanitizeTangent(this Vector4 tangent)
|
||||||
|
{
|
||||||
|
var n = new Vector3(tangent.X, tangent.Y, tangent.Z).SanitizeNormal();
|
||||||
|
var s = float.IsNaN(tangent.W) ? 1 : tangent.W;
|
||||||
|
return new Vector4(n, s > 0 ? 1 : -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -119,7 +119,7 @@ public partial class TexToolsImporter : IDisposable
|
||||||
// Puts out warnings if extension does not correspond to data.
|
// Puts out warnings if extension does not correspond to data.
|
||||||
private DirectoryInfo VerifyVersionAndImport(FileInfo modPackFile)
|
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);
|
return HandleRegularArchive(modPackFile);
|
||||||
|
|
||||||
using var zfs = modPackFile.OpenRead();
|
using var zfs = modPackFile.OpenRead();
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ using Newtonsoft.Json.Linq;
|
||||||
using OtterGui.Filesystem;
|
using OtterGui.Filesystem;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
using Penumbra.Mods;
|
using Penumbra.Mods;
|
||||||
|
using Penumbra.Services;
|
||||||
using SharpCompress.Archives;
|
using SharpCompress.Archives;
|
||||||
using SharpCompress.Archives.Rar;
|
using SharpCompress.Archives.Rar;
|
||||||
using SharpCompress.Archives.SevenZip;
|
using SharpCompress.Archives.SevenZip;
|
||||||
|
|
@ -146,6 +147,9 @@ public partial class TexToolsImporter
|
||||||
case ".mtrl":
|
case ".mtrl":
|
||||||
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
|
_migrationManager.MigrateMtrlDuringExtraction(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||||
break;
|
break;
|
||||||
|
case ".tex":
|
||||||
|
_migrationManager.FixMipMaps(reader, _currentModDirectory!.FullName, _extractionOptions);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
|
reader.WriteEntryToDirectory(_currentModDirectory!.FullName, _extractionOptions);
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using Penumbra.Import.Structs;
|
using Penumbra.Import.Structs;
|
||||||
|
|
|
||||||
|
|
@ -259,6 +259,7 @@ public partial class TexToolsImporter
|
||||||
{
|
{
|
||||||
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
|
".mdl" => _migrationManager.MigrateTtmpModel(extractedFile.FullName, data.Data),
|
||||||
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
|
".mtrl" => _migrationManager.MigrateTtmpMaterial(extractedFile.FullName, data.Data),
|
||||||
|
".tex" => _migrationManager.FixTtmpMipMaps(extractedFile.FullName, data.Data),
|
||||||
_ => data.Data,
|
_ => data.Data,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using ImGuiNET;
|
using Dalamud.Bindings.ImGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
using SixLabors.ImageSharp.PixelFormats;
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,76 @@ public static class TexFileParser
|
||||||
return 13;
|
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)
|
private static unsafe void CopyData(ScratchImage image, BinaryReader r)
|
||||||
{
|
{
|
||||||
fixed (byte* ptr = image.Pixels)
|
fixed (byte* ptr = image.Pixels)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using ImGuiNET;
|
|
||||||
using Lumina.Data.Files;
|
using Lumina.Data.Files;
|
||||||
using OtterGui;
|
using OtterGui;
|
||||||
using OtterGui.Raii;
|
using OtterGui.Raii;
|
||||||
|
|
@ -20,7 +20,7 @@ public static class TextureDrawer
|
||||||
{
|
{
|
||||||
size = texture.TextureWrap.Size.Contain(size);
|
size = texture.TextureWrap.Size.Contain(size);
|
||||||
|
|
||||||
ImGui.Image(texture.TextureWrap.ImGuiHandle, size);
|
ImGui.Image(texture.TextureWrap.Handle, size);
|
||||||
DrawData(texture);
|
DrawData(texture);
|
||||||
}
|
}
|
||||||
else if (texture.LoadError != null)
|
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.
|
// 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)
|
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>();
|
var dxgiDevice = device.QueryInterface<DxgiDevice>();
|
||||||
|
|
||||||
using var deviceClone = new Device(dxgiDevice.Adapter, device.CreationFlags, device.FeatureLevel);
|
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);
|
private readonly ThreadLocal<ResolveData> _characterSoundData = new(() => ResolveData.Invalid, true);
|
||||||
|
|
||||||
public ResolveData SoundData
|
|
||||||
=> _animationLoadData.Value;
|
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public ResolveData SetSoundData(ResolveData data)
|
public ResolveData SetSoundData(ResolveData data)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ public sealed unsafe class ChangeCustomize : FastHook<ChangeCustomize.Delegate>
|
||||||
{
|
{
|
||||||
_collectionResolver = collectionResolver;
|
_collectionResolver = collectionResolver;
|
||||||
_metaState = metaState;
|
_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);
|
public delegate bool Delegate(Human* human, CustomizeArray* data, byte skipEquipment);
|
||||||
|
|
|
||||||
|
|
@ -35,14 +35,14 @@ public sealed unsafe class WeaponReload : EventWrapperPtr<DrawDataContainer, Cha
|
||||||
public bool Finished
|
public bool Finished
|
||||||
=> _task.IsCompletedSuccessfully;
|
=> _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;
|
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));
|
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);
|
_postEvent.Invoke(drawData, gameObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,8 @@ public sealed unsafe class AttributeHook : EventWrapper<Actor, Model, ModCollect
|
||||||
{
|
{
|
||||||
public enum Priority
|
public enum Priority
|
||||||
{
|
{
|
||||||
/// <seealso cref="ShapeManager.OnAttributeComputed"/>
|
/// <seealso cref="ShapeAttributeManager.OnAttributeComputed"/>
|
||||||
ShapeManager = 0,
|
ShapeAttributeManager = 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly CollectionResolver _resolver;
|
private readonly CollectionResolver _resolver;
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ public sealed unsafe class PreBoneDeformerReplacer : IDisposable, IRequiredServi
|
||||||
if (!_framework.IsInFrameworkUpdateThread)
|
if (!_framework.IsInFrameworkUpdateThread)
|
||||||
Penumbra.Log.Warning(
|
Penumbra.Log.Warning(
|
||||||
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
|
$"{nameof(PreBoneDeformerReplacer)}.{nameof(SetupHssReplacements)}(0x{(nint)drawObject:X}, {slotIndex}) called out of framework thread");
|
||||||
|
|
||||||
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
|
var preBoneDeformer = GetPreBoneDeformerForCharacter(drawObject);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using Dalamud.Hooking;
|
using Dalamud.Hooking;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using Dalamud.Utility.Signatures;
|
using Dalamud.Utility.Signatures;
|
||||||
using FFXIVClientStructs.FFXIV.Client.System.Resource;
|
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);
|
ResourceType* pResourceType, int* pResourceHash, byte* pPath, GetResourceParameters* pGetResParams, nint unk7, uint unk8);
|
||||||
|
|
||||||
private delegate ResourceHandle* GetResourceAsyncPrototype(ResourceManager* resourceManager, ResourceCategory* pCategoryId,
|
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))]
|
[Signature(Sigs.GetResourceSync, DetourName = nameof(GetResourceSyncDetour))]
|
||||||
private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!;
|
private readonly Hook<GetResourceSyncPrototype> _getResourceSyncHook = null!;
|
||||||
|
|
@ -118,18 +120,26 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
||||||
unk9);
|
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;
|
ResourceHandle* returnValue = null;
|
||||||
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync,
|
ResourceRequested?.Invoke(ref *categoryId, ref *resourceType, ref *resourceHash, ref gamePath, original, pGetResParams, ref isSync,
|
||||||
ref returnValue);
|
ref returnValue);
|
||||||
if (returnValue != null)
|
if (returnValue != null)
|
||||||
return returnValue;
|
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>
|
/// <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)
|
GetResourceParameters* resourceParameters = null, byte unk = 0, nint unk8 = 0, uint unk9 = 0)
|
||||||
{
|
{
|
||||||
var previous = _currentGetResourcePath.Value;
|
var previous = _currentGetResourcePath.Value;
|
||||||
|
|
@ -141,7 +151,8 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
||||||
resourceParameters, unk8, unk9)
|
resourceParameters, unk8, unk9)
|
||||||
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
|
: _getResourceAsyncHook.OriginalDisposeSafe(_resourceManager.ResourceManager, &categoryId, &type, &hash, path.Path,
|
||||||
resourceParameters, unk, unk8, unk9);
|
resourceParameters, unk, unk8, unk9);
|
||||||
} finally
|
}
|
||||||
|
finally
|
||||||
{
|
{
|
||||||
_currentGetResourcePath.Value = previous;
|
_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="syncOriginal">The original game path of the resource, if loaded synchronously.</param>
|
||||||
/// <param name="previousState">The previous state of the resource.</param>
|
/// <param name="previousState">The previous state of the resource.</param>
|
||||||
/// <param name="returnValue">The return value to use.</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>
|
/// <summary>
|
||||||
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
|
/// <inheritdoc cref="ResourceStateUpdatingDelegate"/> <para/>
|
||||||
|
|
@ -185,7 +197,7 @@ public unsafe class ResourceService : IDisposable, IRequiredService
|
||||||
private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread)
|
private uint UpdateResourceStateDetour(ResourceHandle* handle, byte offFileThread)
|
||||||
{
|
{
|
||||||
var previousState = (handle->UnkState, handle->LoadState);
|
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);
|
ResourceStateUpdating?.Invoke(handle, syncOriginal);
|
||||||
var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread);
|
var ret = _updateResourceStateHook.OriginalDisposeSafe(handle, offFileThread);
|
||||||
ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret);
|
ResourceStateUpdated?.Invoke(handle, syncOriginal, previousState, ref ret);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using Penumbra.Collections;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.PathResolving;
|
using Penumbra.Interop.PathResolving;
|
||||||
|
using Penumbra.Interop.Processing;
|
||||||
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
using static FFXIVClientStructs.FFXIV.Client.Game.Character.ActionEffectHandler;
|
||||||
|
|
||||||
namespace Penumbra.Interop.Hooks.Resources;
|
namespace Penumbra.Interop.Hooks.Resources;
|
||||||
|
|
@ -35,6 +36,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
|
private readonly Hook<MPapResolveDelegate> _resolveMPapPathHook;
|
||||||
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
|
private readonly Hook<PerSlotResolveDelegate> _resolveMdlPathHook;
|
||||||
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
|
private readonly Hook<NamedResolveDelegate> _resolveMtrlPathHook;
|
||||||
|
private readonly Hook<PerSlotResolveDelegate> _resolveSkinMtrlPathHook;
|
||||||
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
|
private readonly Hook<NamedResolveDelegate> _resolvePapPathHook;
|
||||||
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
|
private readonly Hook<PerSlotResolveDelegate> _resolveKdbPathHook;
|
||||||
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
|
private readonly Hook<PerSlotResolveDelegate> _resolvePhybPathHook;
|
||||||
|
|
@ -52,22 +54,23 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
{
|
{
|
||||||
_parent = parent;
|
_parent = parent;
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
_resolveSklbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSklb)}", hooks, vTable[76], type, ResolveSklb, ResolveSklbHuman);
|
||||||
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
|
_resolveMdlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveMdl)}", hooks, vTable[77], type, ResolveMdl, ResolveMdlHuman);
|
||||||
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
|
_resolveSkpPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkp)}", hooks, vTable[78], type, ResolveSkp, ResolveSkpHuman);
|
||||||
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
|
_resolvePhybPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolvePhyb)}", hooks, vTable[79], type, ResolvePhyb, ResolvePhybHuman);
|
||||||
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
|
_resolveKdbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveKdb)}", hooks, vTable[80], type, ResolveKdb, ResolveKdbHuman);
|
||||||
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
|
_vFunc81Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc81)}", hooks, vTable[81], type, null, VFunc81);
|
||||||
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
|
_resolveBnmbPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveBnmb)}", hooks, vTable[82], type, ResolveBnmb, ResolveBnmbHuman);
|
||||||
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
|
_vFunc83Hook = Create<SkeletonVFuncDelegate>( $"{name}.{nameof(VFunc83)}", hooks, vTable[83], type, null, VFunc83);
|
||||||
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
|
_resolvePapPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolvePap)}", hooks, vTable[84], type, ResolvePap, ResolvePapHuman);
|
||||||
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
|
_resolveTmbPathHook = Create<TmbResolveDelegate>( $"{name}.{nameof(ResolveTmb)}", hooks, vTable[85], ResolveTmb);
|
||||||
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
|
_resolveMPapPathHook = Create<MPapResolveDelegate>( $"{name}.{nameof(ResolveMPap)}", hooks, vTable[87], ResolveMPap);
|
||||||
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
_resolveImcPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveImc)}", hooks, vTable[89], ResolveImc);
|
||||||
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
_resolveMtrlPathHook = Create<NamedResolveDelegate>( $"{name}.{nameof(ResolveMtrl)}", hooks, vTable[90], ResolveMtrl);
|
||||||
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
_resolveSkinMtrlPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveSkinMtrl)}", hooks, vTable[91], ResolveSkinMtrl);
|
||||||
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
_resolveDecalPathHook = Create<PerSlotResolveDelegate>($"{name}.{nameof(ResolveDecal)}", hooks, vTable[92], ResolveDecal);
|
||||||
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
_resolveVfxPathHook = Create<VfxResolveDelegate>( $"{name}.{nameof(ResolveVfx)}", hooks, vTable[93], type, ResolveVfx, ResolveVfxHuman);
|
||||||
|
_resolveEidPathHook = Create<SingleResolveDelegate>( $"{name}.{nameof(ResolveEid)}", hooks, vTable[94], ResolveEid);
|
||||||
|
|
||||||
|
|
||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
@ -83,6 +86,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
_resolveMPapPathHook.Enable();
|
_resolveMPapPathHook.Enable();
|
||||||
_resolveMdlPathHook.Enable();
|
_resolveMdlPathHook.Enable();
|
||||||
_resolveMtrlPathHook.Enable();
|
_resolveMtrlPathHook.Enable();
|
||||||
|
_resolveSkinMtrlPathHook.Enable();
|
||||||
_resolvePapPathHook.Enable();
|
_resolvePapPathHook.Enable();
|
||||||
_resolveKdbPathHook.Enable();
|
_resolveKdbPathHook.Enable();
|
||||||
_resolvePhybPathHook.Enable();
|
_resolvePhybPathHook.Enable();
|
||||||
|
|
@ -103,6 +107,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
_resolveMPapPathHook.Disable();
|
_resolveMPapPathHook.Disable();
|
||||||
_resolveMdlPathHook.Disable();
|
_resolveMdlPathHook.Disable();
|
||||||
_resolveMtrlPathHook.Disable();
|
_resolveMtrlPathHook.Disable();
|
||||||
|
_resolveSkinMtrlPathHook.Disable();
|
||||||
_resolvePapPathHook.Disable();
|
_resolvePapPathHook.Disable();
|
||||||
_resolveKdbPathHook.Disable();
|
_resolveKdbPathHook.Disable();
|
||||||
_resolvePhybPathHook.Disable();
|
_resolvePhybPathHook.Disable();
|
||||||
|
|
@ -123,6 +128,7 @@ public sealed unsafe class ResolvePathHooksBase : IDisposable
|
||||||
_resolveMPapPathHook.Dispose();
|
_resolveMPapPathHook.Dispose();
|
||||||
_resolveMdlPathHook.Dispose();
|
_resolveMdlPathHook.Dispose();
|
||||||
_resolveMtrlPathHook.Dispose();
|
_resolveMtrlPathHook.Dispose();
|
||||||
|
_resolveSkinMtrlPathHook.Dispose();
|
||||||
_resolvePapPathHook.Dispose();
|
_resolvePapPathHook.Dispose();
|
||||||
_resolveKdbPathHook.Dispose();
|
_resolveKdbPathHook.Dispose();
|
||||||
_resolvePhybPathHook.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)
|
private nint ResolveMtrl(nint drawObject, nint pathBuffer, nint pathBufferSize, uint slotIndex, nint mtrlFileName)
|
||||||
=> ResolvePath(drawObject, _resolveMtrlPathHook.Original(drawObject, pathBuffer, pathBufferSize, slotIndex, 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)
|
private nint ResolvePap(nint drawObject, nint pathBuffer, nint pathBufferSize, uint unkAnimationIndex, nint animationName)
|
||||||
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
|
=> ResolvePath(drawObject, _resolvePapPathHook.Original(drawObject, pathBuffer, pathBufferSize, unkAnimationIndex, animationName));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ public readonly record struct MaterialInfo(ObjectIndex ObjectIndex, DrawObjectTy
|
||||||
if (mtrlHandle == null)
|
if (mtrlHandle == null)
|
||||||
continue;
|
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);
|
var fileName = CiByteString.FromSpanUnsafe(path, true);
|
||||||
if (fileName == needle)
|
if (fileName == needle)
|
||||||
result.Add(new MaterialInfo(index, type, i, j));
|
result.Add(new MaterialInfo(index, type, i, j));
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,7 @@ public sealed unsafe class CollectionResolver(
|
||||||
{
|
{
|
||||||
var item = charaEntry.Value;
|
var item = charaEntry.Value;
|
||||||
var identifier = actors.CreatePlayer(new ByteString(item->Name), item->HomeWorldId);
|
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")}.");
|
$"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)
|
if (identifier.IsValid && CollectionByIdentifier(identifier) is { } coll)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ public sealed class CutsceneService : IRequiredService, IDisposable
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
_copiedCharacters[copyIdx - CutsceneStartIdx] = (short)parentIdx;
|
||||||
|
_objects.InvokeRequiredUpdates();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
7
Penumbra/Interop/ProcessThreadApi.cs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
namespace Penumbra.Interop;
|
||||||
|
|
||||||
|
public static partial class ProcessThreadApi
|
||||||
|
{
|
||||||
|
[LibraryImport("kernel32.dll")]
|
||||||
|
public static partial uint GetCurrentThreadId();
|
||||||
|
}
|
||||||
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
119
Penumbra/Interop/Processing/PbdFilePostProcessor.cs
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
using Dalamud.Game;
|
||||||
|
using Dalamud.Plugin.Services;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.GameData;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
using Penumbra.String;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Processing;
|
||||||
|
|
||||||
|
public sealed class PbdFilePostProcessor : IFilePostProcessor
|
||||||
|
{
|
||||||
|
private readonly IFileAllocator _allocator;
|
||||||
|
private byte[] _epbdData;
|
||||||
|
private unsafe delegate* unmanaged<ResourceHandle*, void> _loadEpbdData;
|
||||||
|
|
||||||
|
public ResourceType Type
|
||||||
|
=> ResourceType.Pbd;
|
||||||
|
|
||||||
|
public unsafe PbdFilePostProcessor(IDataManager dataManager, XivFileAllocator allocator, ISigScanner scanner)
|
||||||
|
{
|
||||||
|
_allocator = allocator;
|
||||||
|
_epbdData = SetEpbdData(dataManager);
|
||||||
|
_loadEpbdData = (delegate* unmanaged<ResourceHandle*, void>)scanner.ScanText(Sigs.LoadEpbdData);
|
||||||
|
}
|
||||||
|
|
||||||
|
public unsafe void PostProcess(ResourceHandle* resource, CiByteString originalGamePath, ReadOnlySpan<byte> additionalData)
|
||||||
|
{
|
||||||
|
if (_epbdData.Length is 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (resource->LoadState is not LoadState.Success)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} failed load ({resource->LoadState}).");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var (data, length) = resource->GetData();
|
||||||
|
if (length is 0 || data == nint.Zero)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"[ResourceLoader] Requested PBD at {resource->FileName()} succeeded load but has no data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var span = new ReadOnlySpan<byte>((void*)data, (int)resource->FileSize);
|
||||||
|
var reader = new PackReader(span);
|
||||||
|
if (reader.HasData)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Excessive($"[ResourceLoader] Successfully loaded PBD at {resource->FileName()} with EPBD data.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newData = AppendData(span);
|
||||||
|
fixed (byte* ptr = newData)
|
||||||
|
{
|
||||||
|
// Set the appended data and the actual file size, then re-load the EPBD data via game function call.
|
||||||
|
if (resource->SetData((nint)ptr, newData.Length))
|
||||||
|
{
|
||||||
|
resource->FileSize = (uint)newData.Length;
|
||||||
|
resource->CsHandle.FileSize2 = (uint)newData.Length;
|
||||||
|
resource->CsHandle.FileSize3 = (uint)newData.Length;
|
||||||
|
_loadEpbdData(resource);
|
||||||
|
// Free original data.
|
||||||
|
_allocator.Release((void*)data, length);
|
||||||
|
Penumbra.Log.Debug($"[ResourceLoader] Loaded {resource->FileName()} from file and appended default EPBD data.");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning(
|
||||||
|
$"[ResourceLoader] Failed to append EPBD data to custom PBD at {resource->FileName()}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Combine the given data with the default PBD data using the game's file allocator. </summary>
|
||||||
|
private unsafe ReadOnlySpan<byte> AppendData(ReadOnlySpan<byte> data)
|
||||||
|
{
|
||||||
|
// offset has to be set, otherwise not called.
|
||||||
|
var newLength = data.Length + _epbdData.Length;
|
||||||
|
var memory = _allocator.Allocate(newLength);
|
||||||
|
var span = new Span<byte>(memory, newLength);
|
||||||
|
data.CopyTo(span);
|
||||||
|
_epbdData.CopyTo(span[data.Length..]);
|
||||||
|
return span;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary> Fetch the default EPBD data from the .pbd file of the game's installation. </summary>
|
||||||
|
private static byte[] SetEpbdData(IDataManager dataManager)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var file = dataManager.GetFile(GamePaths.Pbd.Path);
|
||||||
|
if (file is null || file.Data.Length is 0)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning("Default PBD file has no data.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadOnlySpan<byte> span = file.Data;
|
||||||
|
var reader = new PackReader(span);
|
||||||
|
if (!reader.HasData)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning("Default PBD file has no EPBD section.");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = span.Length - (int)reader.PackLength;
|
||||||
|
var ret = span[offset..];
|
||||||
|
Penumbra.Log.Verbose($"Default PBD file has EPBD section of length {ret.Length} at offset {offset}.");
|
||||||
|
return ret.ToArray();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Penumbra.Log.Error($"Unknown error getting default EPBD data:\n{ex}");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
63
Penumbra/Interop/Processing/SkinMtrlPathEarlyProcessing.cs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
|
||||||
|
|
||||||
|
namespace Penumbra.Interop.Processing;
|
||||||
|
|
||||||
|
public static unsafe class SkinMtrlPathEarlyProcessing
|
||||||
|
{
|
||||||
|
public static void Process(Span<byte> path, CharacterBase* character, uint slotIndex)
|
||||||
|
{
|
||||||
|
var end = path.IndexOf(MaterialExtension());
|
||||||
|
if (end < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var suffixPos = path[..end].LastIndexOf((byte)'_');
|
||||||
|
if (suffixPos < 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var handle = GetModelResourceHandle(character, slotIndex);
|
||||||
|
if (handle == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var skinSuffix = GetSkinSuffix(handle);
|
||||||
|
if (skinSuffix.IsEmpty || skinSuffix.Length > path.Length - suffixPos - 7)
|
||||||
|
return;
|
||||||
|
|
||||||
|
++suffixPos;
|
||||||
|
skinSuffix.CopyTo(path[suffixPos..]);
|
||||||
|
suffixPos += skinSuffix.Length;
|
||||||
|
MaterialExtension().CopyTo(path[suffixPos..]);
|
||||||
|
return;
|
||||||
|
|
||||||
|
static ReadOnlySpan<byte> MaterialExtension()
|
||||||
|
=> ".mtrl\0"u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ModelResourceHandle* GetModelResourceHandle(CharacterBase* character, uint slotIndex)
|
||||||
|
{
|
||||||
|
if (character is null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (character->PerSlotStagingArea is not null)
|
||||||
|
{
|
||||||
|
var handle = character->PerSlotStagingArea[slotIndex].ModelResourceHandle;
|
||||||
|
if (handle != null)
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var model = character->Models[slotIndex];
|
||||||
|
return model is null ? null : model->ModelResourceHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ReadOnlySpan<byte> GetSkinSuffix(ModelResourceHandle* handle)
|
||||||
|
{
|
||||||
|
foreach (var (attribute, _) in handle->Attributes)
|
||||||
|
{
|
||||||
|
var attributeSpan = attribute.AsSpan();
|
||||||
|
if (attributeSpan.Length > 12 && attributeSpan[..11].SequenceEqual("skin_suffix"u8) && attributeSpan[11] is (byte)'=' or (byte)'_')
|
||||||
|
return attributeSpan[12..];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -338,6 +338,34 @@ internal partial record ResolveContext
|
||||||
return Utf8GamePath.FromByteString(path, out var gamePath) ? gamePath : Utf8GamePath.Empty;
|
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)
|
private unsafe Utf8GamePath ResolveMaterialAnimationPath(ResourceHandle* imc)
|
||||||
{
|
{
|
||||||
var animation = ResolveImcData(imc).MaterialAnimationId;
|
var animation = ResolveImcData(imc).MaterialAnimationId;
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ internal unsafe partial record ResolveContext(
|
||||||
if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path))
|
if (!Utf8GamePath.FromByteString(CiByteString.Join((byte)'/', ShpkPrefix, gamePath), out var path))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, &resourceHandle->ResourceHandle, path);
|
return GetOrCreateNode(ResourceType.Shpk, (nint)resourceHandle->ShaderPackage, (ResourceHandle*)resourceHandle, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SkipLocalsInit]
|
[SkipLocalsInit]
|
||||||
|
|
@ -188,7 +188,8 @@ internal unsafe partial record ResolveContext(
|
||||||
return GetOrCreateNode(ResourceType.Tex, (nint)tex->Texture, &tex->ResourceHandle, gamePath);
|
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)
|
if (mdl is null || mdl->ModelResourceHandle is null)
|
||||||
return 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)
|
if (CreateNodeFromDecal(decalHandle, imc) is { } decalNode)
|
||||||
node.Children.Add(decalNode);
|
node.Children.Add(decalNode);
|
||||||
|
|
||||||
|
|
@ -238,7 +245,7 @@ internal unsafe partial record ResolveContext(
|
||||||
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
if (Global.Nodes.TryGetValue((path, (nint)resource), out var cached))
|
||||||
return 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));
|
var shpkNode = CreateNodeFromShpk(resource->ShaderPackageResourceHandle, new CiByteString(resource->ShpkName.Value));
|
||||||
if (shpkNode is not null)
|
if (shpkNode is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -364,7 +371,8 @@ internal unsafe partial record ResolveContext(
|
||||||
return node;
|
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)
|
if (sklb is null || sklb->SkeletonResourceHandle is null)
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -379,6 +387,8 @@ internal unsafe partial record ResolveContext(
|
||||||
node.Children.Add(skpNode);
|
node.Children.Add(skpNode);
|
||||||
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
if (CreateNodeFromPhyb(phybHandle, partialSkeletonIndex) is { } phybNode)
|
||||||
node.Children.Add(phybNode);
|
node.Children.Add(phybNode);
|
||||||
|
if (CreateNodeFromKdb(kdbHandle, partialSkeletonIndex) is { } kdbNode)
|
||||||
|
node.Children.Add(kdbNode);
|
||||||
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
Global.Nodes.Add((path, (nint)sklb->SkeletonResourceHandle), node);
|
||||||
|
|
||||||
return node;
|
return node;
|
||||||
|
|
@ -420,6 +430,24 @@ internal unsafe partial record ResolveContext(
|
||||||
return node;
|
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)
|
internal ResourceNode.UiData GuessModelUiData(Utf8GamePath gamePath)
|
||||||
{
|
{
|
||||||
var path = gamePath.Path.Split((byte)'/');
|
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>
|
/// <summary> Whether to treat the file as protected (require holding the Mod Deletion Modifier to make a quick import). </summary>
|
||||||
public bool Protected
|
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)
|
internal ResourceNode(ResourceType type, nint objectAddress, nint resourceHandle, ulong length, ResolveContext? resolveContext)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -70,12 +70,16 @@ public class ResourceTree(
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(model);
|
var genericContext = globalContext.CreateContext(model);
|
||||||
|
|
||||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
var mpapArrayPtr = model->MaterialAnimationPacks;
|
||||||
var mpapArrayPtr = *(ResourceHandle***)((nint)model + 0x948);
|
|
||||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, model->SlotCount) : [];
|
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
|
var decalArray = modelType switch
|
||||||
{
|
{
|
||||||
ModelType.Human => human->SlotDecalsSpan,
|
ModelType.Human => human->SlotDecals,
|
||||||
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
|
ModelType.DemiHuman => ((Demihuman*)model)->SlotDecals,
|
||||||
ModelType.Weapon => [((Weapon*)model)->Decal],
|
ModelType.Weapon => [((Weapon*)model)->Decal],
|
||||||
ModelType.Monster => [((Monster*)model)->Decal],
|
ModelType.Monster => [((Monster*)model)->Decal],
|
||||||
|
|
@ -108,7 +112,8 @@ public class ResourceTree(
|
||||||
|
|
||||||
var mdl = model->Models[i];
|
var mdl = model->Models[i];
|
||||||
if (slotContext.CreateNodeFromModel(mdl, imc, i < decalArray.Length ? decalArray[(int)i].Value : null,
|
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)
|
if (globalContext.WithUiData)
|
||||||
mdlNode.FallbackName = $"Model #{i}";
|
mdlNode.FallbackName = $"Model #{i}";
|
||||||
|
|
@ -116,9 +121,8 @@ public class ResourceTree(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(Nodes, genericContext, model->EID, model->Skeleton, model->BonePhysicsModule);
|
AddSkeleton(Nodes, genericContext, model);
|
||||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
AddMaterialAnimationSkeleton(Nodes, genericContext, model->MaterialAnimationSkeleton);
|
||||||
AddMaterialAnimationSkeleton(Nodes, genericContext, *(SkeletonResourceHandle**)((nint)model + 0x940));
|
|
||||||
|
|
||||||
AddWeapons(globalContext, model);
|
AddWeapons(globalContext, model);
|
||||||
|
|
||||||
|
|
@ -149,8 +153,7 @@ public class ResourceTree(
|
||||||
|
|
||||||
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
var genericContext = globalContext.CreateContext(subObject, 0xFFFFFFFFu, slot, equipment, weaponType);
|
||||||
|
|
||||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
var mpapArrayPtr = subObject->MaterialAnimationPacks;
|
||||||
var mpapArrayPtr = *(ResourceHandle***)((nint)subObject + 0x948);
|
|
||||||
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
|
var mpapArray = mpapArrayPtr is not null ? new ReadOnlySpan<Pointer<ResourceHandle>>(mpapArrayPtr, subObject->SlotCount) : [];
|
||||||
|
|
||||||
for (var i = 0; i < subObject->SlotCount; ++i)
|
for (var i = 0; i < subObject->SlotCount; ++i)
|
||||||
|
|
@ -166,7 +169,8 @@ public class ResourceTree(
|
||||||
}
|
}
|
||||||
|
|
||||||
var mdl = subObject->Models[i];
|
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)
|
if (globalContext.WithUiData)
|
||||||
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
mdlNode.FallbackName = $"Weapon #{weaponIndex}, Model #{i}";
|
||||||
|
|
@ -174,10 +178,8 @@ public class ResourceTree(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AddSkeleton(weaponNodes, genericContext, subObject->EID, subObject->Skeleton, subObject->BonePhysicsModule,
|
AddSkeleton(weaponNodes, genericContext, subObject, $"Weapon #{weaponIndex}, ");
|
||||||
$"Weapon #{weaponIndex}, ");
|
AddMaterialAnimationSkeleton(weaponNodes, genericContext, subObject->MaterialAnimationSkeleton,
|
||||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
|
||||||
AddMaterialAnimationSkeleton(weaponNodes, genericContext, *(SkeletonResourceHandle**)((nint)subObject + 0x940),
|
|
||||||
$"Weapon #{weaponIndex}, ");
|
$"Weapon #{weaponIndex}, ");
|
||||||
|
|
||||||
++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,
|
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);
|
var eidNode = context.CreateNodeFromEid((ResourceHandle*)eid);
|
||||||
if (eidNode != null)
|
if (eidNode != null)
|
||||||
|
|
@ -255,9 +260,9 @@ public class ResourceTree(
|
||||||
|
|
||||||
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
for (var i = 0; i < skeleton->PartialSkeletonCount; ++i)
|
||||||
{
|
{
|
||||||
// TODO ClientStructs-ify (aers/FFXIVClientStructs#1312)
|
var phybHandle = physics != null ? physics->BonePhysicsResourceHandles[i] : null;
|
||||||
var phybHandle = physics != null ? ((ResourceHandle**)((nint)physics + 0x190))[i] : null;
|
var kdbHandle = kineDriver != null ? kineDriver->PartialSkeletonEntries[i].KineDriverResourceHandle : null;
|
||||||
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, (uint)i) is { } sklbNode)
|
if (context.CreateNodeFromPartialSkeleton(&skeleton->PartialSkeletons[i], phybHandle, kdbHandle, (uint)i) is { } sklbNode)
|
||||||
{
|
{
|
||||||
if (context.Global.WithUiData)
|
if (context.Global.WithUiData)
|
||||||
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
sklbNode.FallbackName = $"{prefix}Skeleton #{i}";
|
||||||
|
|
|
||||||
|
|
@ -421,9 +421,9 @@ public sealed unsafe partial class RedrawService : IDisposable
|
||||||
return;
|
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)
|
if (gameObject == null)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using SharpDX.Direct3D;
|
using SharpDX.Direct3D;
|
||||||
|
|
@ -16,7 +17,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
||||||
private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
|
private readonly HashSet<(nint XivTexture, byte SliceIndex)> _expiredKeys = [];
|
||||||
|
|
||||||
/// <remarks> Caching this across frames will cause a crash to desktop. </remarks>
|
/// <remarks> Caching this across frames will cause a crash to desktop. </remarks>
|
||||||
public nint GetImGuiHandle(Texture* texture, byte sliceIndex)
|
public ImTextureID GetImGuiHandle(Texture* texture, byte sliceIndex)
|
||||||
{
|
{
|
||||||
if (texture == null)
|
if (texture == null)
|
||||||
throw new ArgumentNullException(nameof(texture));
|
throw new ArgumentNullException(nameof(texture));
|
||||||
|
|
@ -25,7 +26,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
||||||
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
|
if (_activeSlices.TryGetValue(((nint)texture, sliceIndex), out var state))
|
||||||
{
|
{
|
||||||
state.Refresh();
|
state.Refresh();
|
||||||
return (nint)state.ShaderResourceView;
|
return new ImTextureID((nint)state.ShaderResourceView);
|
||||||
}
|
}
|
||||||
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
|
var srv = (ShaderResourceView)(nint)texture->D3D11ShaderResourceView;
|
||||||
var description = srv.Description;
|
var description = srv.Description;
|
||||||
|
|
@ -60,7 +61,7 @@ public sealed unsafe class TextureArraySlicer : IUiService, IDisposable
|
||||||
}
|
}
|
||||||
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
|
state = new SliceState(new ShaderResourceView(srv.Device, srv.Resource, description));
|
||||||
_activeSlices.Add(((nint)texture, sliceIndex), state);
|
_activeSlices.Add(((nint)texture, sliceIndex), state);
|
||||||
return (nint)state.ShaderResourceView;
|
return new ImTextureID((nint)state.ShaderResourceView);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick()
|
public void Tick()
|
||||||
|
|
|
||||||
|
|
@ -10,28 +10,34 @@ internal static class StructExtensions
|
||||||
public static CiByteString AsByteString(in this StdString str)
|
public static CiByteString AsByteString(in this StdString str)
|
||||||
=> CiByteString.FromSpanUnsafe(str.AsSpan(), true);
|
=> CiByteString.FromSpanUnsafe(str.AsSpan(), true);
|
||||||
|
|
||||||
public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character)
|
public static CiByteString ResolveEidPathAsByteString(ref this CharacterBase character)
|
||||||
{
|
{
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveEidPath(pathBuffer));
|
return ToOwnedByteString(character.ResolveEidPath(pathBuffer));
|
||||||
}
|
|
||||||
|
|
||||||
public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
|
||||||
{
|
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
|
||||||
return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
public static CiByteString ResolveImcPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||||
{
|
{
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex));
|
return ToOwnedByteString(character.ResolveImcPath(pathBuffer, slotIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName)
|
public static CiByteString ResolveMdlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||||
{
|
{
|
||||||
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
return ToOwnedByteString(character.ResolveMdlPath(pathBuffer, slotIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe CiByteString ResolveMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex, byte* mtrlFileName)
|
||||||
|
{
|
||||||
|
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
return ToOwnedByteString(character.ResolveMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex, mtrlFileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe CiByteString ResolveSkinMtrlPathAsByteString(ref this CharacterBase character, uint slotIndex)
|
||||||
|
{
|
||||||
|
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
return ToOwnedByteString(character.ResolveSkinMtrlPath(pathBuffer, CharacterBase.PathBufferSize, slotIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
|
public static CiByteString ResolveMaterialPapPathAsByteString(ref this CharacterBase character, uint slotIndex, uint unkSId)
|
||||||
|
|
@ -40,16 +46,16 @@ internal static class StructExtensions
|
||||||
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
|
return ToOwnedByteString(character.ResolveMaterialPapPath(pathBuffer, slotIndex, unkSId));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
public static CiByteString ResolveSklbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex));
|
return ToOwnedByteString(character.ResolveSklbPath(pathBuffer, partialSkeletonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
public static CiByteString ResolveSkpPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
{
|
{
|
||||||
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
Span<byte> pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
return ToOwnedByteString(character.ResolveSkpPath(pathBuffer, partialSkeletonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
public static CiByteString ResolvePhybPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
|
|
@ -58,6 +64,12 @@ internal static class StructExtensions
|
||||||
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
|
return ToOwnedByteString(character.ResolvePhybPath(pathBuffer, partialSkeletonIndex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static unsafe CiByteString ResolveKdbPathAsByteString(ref this CharacterBase character, uint partialSkeletonIndex)
|
||||||
|
{
|
||||||
|
var pathBuffer = stackalloc byte[CharacterBase.PathBufferSize];
|
||||||
|
return ToOwnedByteString(character.ResolveKdbPath(pathBuffer, CharacterBase.PathBufferSize, partialSkeletonIndex));
|
||||||
|
}
|
||||||
|
|
||||||
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
|
private static unsafe CiByteString ToOwnedByteString(CStringPointer str)
|
||||||
=> str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty;
|
=> str.HasValue ? new CiByteString(str.Value).Clone() : CiByteString.Empty;
|
||||||
|
|
||||||
|
|
|
||||||
145
Penumbra/Meta/Manipulations/AtrIdentifier.cs
Normal file
145
Penumbra/Meta/Manipulations/AtrIdentifier.cs
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.Structs;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
|
||||||
|
namespace Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
public readonly record struct AtrIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeAttributeString Attribute, GenderRace GenderRaceCondition)
|
||||||
|
: IComparable<AtrIdentifier>, IMetaIdentifier
|
||||||
|
{
|
||||||
|
public int CompareTo(AtrIdentifier other)
|
||||||
|
{
|
||||||
|
var slotComparison = Slot.CompareTo(other.Slot);
|
||||||
|
if (slotComparison is not 0)
|
||||||
|
return slotComparison;
|
||||||
|
|
||||||
|
if (Id.HasValue)
|
||||||
|
{
|
||||||
|
if (other.Id.HasValue)
|
||||||
|
{
|
||||||
|
var idComparison = Id.Value.Id.CompareTo(other.Id.Value.Id);
|
||||||
|
if (idComparison is not 0)
|
||||||
|
return idComparison;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (other.Id.HasValue)
|
||||||
|
{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
|
||||||
|
if (genderRaceComparison is not 0)
|
||||||
|
return genderRaceComparison;
|
||||||
|
|
||||||
|
return Attribute.CompareTo(other.Attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder(64);
|
||||||
|
sb.Append("Shp - ")
|
||||||
|
.Append(Attribute);
|
||||||
|
if (Slot is HumanSlot.Unknown)
|
||||||
|
{
|
||||||
|
sb.Append(" - All Slots & IDs");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Append(" - ")
|
||||||
|
.Append(Slot.ToName())
|
||||||
|
.Append(" - ");
|
||||||
|
if (Id.HasValue)
|
||||||
|
sb.Append(Id.Value.Id);
|
||||||
|
else
|
||||||
|
sb.Append("All IDs");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GenderRaceCondition is not GenderRace.Unknown)
|
||||||
|
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
|
||||||
|
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
||||||
|
{
|
||||||
|
// Nothing for now since it depends entirely on the shape key.
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaIndex FileIndex()
|
||||||
|
=> (MetaIndex)(-1);
|
||||||
|
|
||||||
|
public bool Validate()
|
||||||
|
{
|
||||||
|
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Slot is HumanSlot.Unknown && Id is not null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return Attribute.ValidateCustomAttributeString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public JObject AddToJson(JObject jObj)
|
||||||
|
{
|
||||||
|
if (Slot is not HumanSlot.Unknown)
|
||||||
|
jObj["Slot"] = Slot.ToString();
|
||||||
|
if (Id.HasValue)
|
||||||
|
jObj["Id"] = Id.Value.Id.ToString();
|
||||||
|
jObj["Attribute"] = Attribute.ToString();
|
||||||
|
if (GenderRaceCondition is not GenderRace.Unknown)
|
||||||
|
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
|
||||||
|
return jObj;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AtrIdentifier? FromJson(JObject jObj)
|
||||||
|
{
|
||||||
|
var attribute = jObj["Attribute"]?.ToObject<string>();
|
||||||
|
if (attribute is null || !ShapeAttributeString.TryRead(attribute, out var attributeString))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
||||||
|
var id = jObj["Id"]?.ToObject<ushort>();
|
||||||
|
var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
|
||||||
|
var identifier = new AtrIdentifier(slot, id, attributeString, genderRaceCondition);
|
||||||
|
return identifier.Validate() ? identifier : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetaManipulationType Type
|
||||||
|
=> MetaManipulationType.Atr;
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(Converter))]
|
||||||
|
public readonly record struct AtrEntry(bool Value)
|
||||||
|
{
|
||||||
|
public static readonly AtrEntry True = new(true);
|
||||||
|
public static readonly AtrEntry False = new(false);
|
||||||
|
|
||||||
|
private class Converter : JsonConverter<AtrEntry>
|
||||||
|
{
|
||||||
|
public override void WriteJson(JsonWriter writer, AtrEntry value, JsonSerializer serializer)
|
||||||
|
=> serializer.Serialize(writer, value.Value);
|
||||||
|
|
||||||
|
public override AtrEntry ReadJson(JsonReader reader, Type objectType, AtrEntry existingValue, bool hasExistingValue,
|
||||||
|
JsonSerializer serializer)
|
||||||
|
=> new(serializer.Deserialize<bool>(reader));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,10 +16,10 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
|
||||||
if (!Enum.IsDefined(Type))
|
if (!Enum.IsDefined(Type))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (Type is GlobalEqpType.DoNotHideVieraHats or GlobalEqpType.DoNotHideHrothgarHats)
|
if (Type.HasCondition())
|
||||||
return Condition == 0;
|
return Condition.Id is not 0;
|
||||||
|
|
||||||
return Condition != 0;
|
return Condition.Id is 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JObject AddToJson(JObject jObj)
|
public JObject AddToJson(JObject jObj)
|
||||||
|
|
@ -89,6 +89,12 @@ public readonly struct GlobalEqpManipulation : IMetaIdentifier
|
||||||
changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName());
|
changedItems.UpdateCountOrSet("All Hats for Viera", () => new IdentifiedName());
|
||||||
else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
|
else if (Type is GlobalEqpType.DoNotHideHrothgarHats)
|
||||||
changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName());
|
changedItems.UpdateCountOrSet("All Hats for Hrothgar", () => new IdentifiedName());
|
||||||
|
else if (Type is GlobalEqpType.HideHorns)
|
||||||
|
changedItems.UpdateCountOrSet("All Au Ra Horns", () => new IdentifiedName());
|
||||||
|
else if (Type is GlobalEqpType.HideVieraEars)
|
||||||
|
changedItems.UpdateCountOrSet("All Viera Ears", () => new IdentifiedName());
|
||||||
|
else if (Type is GlobalEqpType.HideMiqoteEars)
|
||||||
|
changedItems.UpdateCountOrSet("All Miqo'te Ears", () => new IdentifiedName());
|
||||||
}
|
}
|
||||||
|
|
||||||
public MetaIndex FileIndex()
|
public MetaIndex FileIndex()
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ public enum GlobalEqpType
|
||||||
DoNotHideRingL,
|
DoNotHideRingL,
|
||||||
DoNotHideHrothgarHats,
|
DoNotHideHrothgarHats,
|
||||||
DoNotHideVieraHats,
|
DoNotHideVieraHats,
|
||||||
|
HideHorns,
|
||||||
|
HideVieraEars,
|
||||||
|
HideMiqoteEars,
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class GlobalEqpExtensions
|
public static class GlobalEqpExtensions
|
||||||
|
|
@ -27,6 +30,9 @@ public static class GlobalEqpExtensions
|
||||||
GlobalEqpType.DoNotHideRingL => true,
|
GlobalEqpType.DoNotHideRingL => true,
|
||||||
GlobalEqpType.DoNotHideHrothgarHats => false,
|
GlobalEqpType.DoNotHideHrothgarHats => false,
|
||||||
GlobalEqpType.DoNotHideVieraHats => false,
|
GlobalEqpType.DoNotHideVieraHats => false,
|
||||||
|
GlobalEqpType.HideHorns => false,
|
||||||
|
GlobalEqpType.HideVieraEars => false,
|
||||||
|
GlobalEqpType.HideMiqoteEars => false,
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -41,6 +47,9 @@ public static class GlobalEqpExtensions
|
||||||
GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8,
|
GlobalEqpType.DoNotHideRingL => "Always Show Rings (Left Finger)"u8,
|
||||||
GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8,
|
GlobalEqpType.DoNotHideHrothgarHats => "Always Show Hats for Hrothgar"u8,
|
||||||
GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8,
|
GlobalEqpType.DoNotHideVieraHats => "Always Show Hats for Viera"u8,
|
||||||
|
GlobalEqpType.HideHorns => "Always Hide Horns (Au Ra)"u8,
|
||||||
|
GlobalEqpType.HideVieraEars => "Always Hide Ears (Viera)"u8,
|
||||||
|
GlobalEqpType.HideMiqoteEars => "Always Hide Ears (Miqo'te)"u8,
|
||||||
_ => "\0"u8,
|
_ => "\0"u8,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -60,6 +69,9 @@ public static class GlobalEqpExtensions
|
||||||
"Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8,
|
"Prevents the game from hiding any hats for Hrothgar that are normally flagged to not display on them."u8,
|
||||||
GlobalEqpType.DoNotHideVieraHats =>
|
GlobalEqpType.DoNotHideVieraHats =>
|
||||||
"Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8,
|
"Prevents the game from hiding any hats for Viera that are normally flagged to not display on them."u8,
|
||||||
_ => "\0"u8,
|
GlobalEqpType.HideHorns => "Forces the game to hide Au Ra horns regardless of headwear."u8,
|
||||||
|
GlobalEqpType.HideVieraEars => "Forces the game to hide Viera ears regardless of headwear."u8,
|
||||||
|
GlobalEqpType.HideMiqoteEars => "Forces the game to hide Miqo'te ears regardless of headwear."u8,
|
||||||
|
_ => "\0"u8,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ public enum MetaManipulationType : byte
|
||||||
GlobalEqp = 7,
|
GlobalEqp = 7,
|
||||||
Atch = 8,
|
Atch = 8,
|
||||||
Shp = 9,
|
Shp = 9,
|
||||||
|
Atr = 10,
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IMetaIdentifier
|
public interface IMetaIdentifier
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using Penumbra.Collections.Cache;
|
using Penumbra.Collections.Cache;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Files.AtchStructs;
|
using Penumbra.GameData.Files.AtchStructs;
|
||||||
|
using Penumbra.GameData.Interop;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Util;
|
using Penumbra.Util;
|
||||||
using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
|
using ImcEntry = Penumbra.GameData.Structs.ImcEntry;
|
||||||
|
|
@ -11,129 +14,333 @@ namespace Penumbra.Meta.Manipulations;
|
||||||
[JsonConverter(typeof(Converter))]
|
[JsonConverter(typeof(Converter))]
|
||||||
public class MetaDictionary
|
public class MetaDictionary
|
||||||
{
|
{
|
||||||
private readonly Dictionary<ImcIdentifier, ImcEntry> _imc = [];
|
private class Wrapper : HashSet<GlobalEqpManipulation>
|
||||||
private readonly Dictionary<EqpIdentifier, EqpEntryInternal> _eqp = [];
|
{
|
||||||
private readonly Dictionary<EqdpIdentifier, EqdpEntryInternal> _eqdp = [];
|
public readonly Dictionary<ImcIdentifier, ImcEntry> Imc = [];
|
||||||
private readonly Dictionary<EstIdentifier, EstEntry> _est = [];
|
public readonly Dictionary<EqpIdentifier, EqpEntryInternal> Eqp = [];
|
||||||
private readonly Dictionary<RspIdentifier, RspEntry> _rsp = [];
|
public readonly Dictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp = [];
|
||||||
private readonly Dictionary<GmpIdentifier, GmpEntry> _gmp = [];
|
public readonly Dictionary<EstIdentifier, EstEntry> Est = [];
|
||||||
private readonly Dictionary<AtchIdentifier, AtchEntry> _atch = [];
|
public readonly Dictionary<RspIdentifier, RspEntry> Rsp = [];
|
||||||
private readonly Dictionary<ShpIdentifier, ShpEntry> _shp = [];
|
public readonly Dictionary<GmpIdentifier, GmpEntry> Gmp = [];
|
||||||
private readonly HashSet<GlobalEqpManipulation> _globalEqp = [];
|
public readonly Dictionary<AtchIdentifier, AtchEntry> Atch = [];
|
||||||
|
public readonly Dictionary<ShpIdentifier, ShpEntry> Shp = [];
|
||||||
|
public readonly Dictionary<AtrIdentifier, AtrEntry> Atr = [];
|
||||||
|
|
||||||
|
public Wrapper()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public Wrapper(MetaCache cache)
|
||||||
|
{
|
||||||
|
Imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
|
||||||
|
Eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
|
||||||
|
Est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
Atr = cache.Atr.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
||||||
|
foreach (var geqp in cache.GlobalEqp.Keys)
|
||||||
|
Add(geqp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe Wrapper Filtered(MetaCache cache, Actor actor)
|
||||||
|
{
|
||||||
|
if (!actor.IsCharacter)
|
||||||
|
return new Wrapper(cache);
|
||||||
|
|
||||||
|
var model = actor.Model;
|
||||||
|
if (!model.IsHuman)
|
||||||
|
return new Wrapper(cache);
|
||||||
|
|
||||||
|
var headId = model.GetModelId(HumanSlot.Head);
|
||||||
|
var bodyId = model.GetModelId(HumanSlot.Body);
|
||||||
|
var equipIdSet = ((IEnumerable<PrimaryId>)
|
||||||
|
[
|
||||||
|
headId,
|
||||||
|
bodyId,
|
||||||
|
model.GetModelId(HumanSlot.Hands),
|
||||||
|
model.GetModelId(HumanSlot.Legs),
|
||||||
|
model.GetModelId(HumanSlot.Feet),
|
||||||
|
]).ToFrozenSet();
|
||||||
|
var earsId = model.GetModelId(HumanSlot.Ears);
|
||||||
|
var neckId = model.GetModelId(HumanSlot.Neck);
|
||||||
|
var wristId = model.GetModelId(HumanSlot.Wrists);
|
||||||
|
var rFingerId = model.GetModelId(HumanSlot.RFinger);
|
||||||
|
var lFingerId = model.GetModelId(HumanSlot.LFinger);
|
||||||
|
|
||||||
|
var wrapper = new Wrapper();
|
||||||
|
// Check for all relevant primary IDs due to slot overlap.
|
||||||
|
foreach (var (eqp, value) in cache.Eqp)
|
||||||
|
{
|
||||||
|
if (eqp.Slot.IsEquipment())
|
||||||
|
{
|
||||||
|
if (equipIdSet.Contains(eqp.SetId))
|
||||||
|
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (eqp.Slot)
|
||||||
|
{
|
||||||
|
case EquipSlot.Ears when eqp.SetId == earsId:
|
||||||
|
case EquipSlot.Neck when eqp.SetId == neckId:
|
||||||
|
case EquipSlot.Wrists when eqp.SetId == wristId:
|
||||||
|
case EquipSlot.RFinger when eqp.SetId == rFingerId:
|
||||||
|
case EquipSlot.LFinger when eqp.SetId == lFingerId:
|
||||||
|
wrapper.Eqp.Add(eqp, new EqpEntryInternal(value.Entry, eqp.Slot));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check also for body IDs due to body occupying head.
|
||||||
|
foreach (var (gmp, value) in cache.Gmp)
|
||||||
|
{
|
||||||
|
if (gmp.SetId == headId || gmp.SetId == bodyId)
|
||||||
|
wrapper.Gmp.Add(gmp, value.Entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for all races due to inheritance and all slots due to overlap.
|
||||||
|
foreach (var (eqdp, value) in cache.Eqdp)
|
||||||
|
{
|
||||||
|
if (eqdp.Slot.IsEquipment())
|
||||||
|
{
|
||||||
|
if (equipIdSet.Contains(eqdp.SetId))
|
||||||
|
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (eqdp.Slot)
|
||||||
|
{
|
||||||
|
case EquipSlot.Ears when eqdp.SetId == earsId:
|
||||||
|
case EquipSlot.Neck when eqdp.SetId == neckId:
|
||||||
|
case EquipSlot.Wrists when eqdp.SetId == wristId:
|
||||||
|
case EquipSlot.RFinger when eqdp.SetId == rFingerId:
|
||||||
|
case EquipSlot.LFinger when eqdp.SetId == lFingerId:
|
||||||
|
wrapper.Eqdp.Add(eqdp, new EqdpEntryInternal(value.Entry, eqdp.Slot));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var genderRace = (GenderRace)model.AsHuman->RaceSexId;
|
||||||
|
var hairId = model.GetModelId(HumanSlot.Hair);
|
||||||
|
var faceId = model.GetModelId(HumanSlot.Face);
|
||||||
|
// We do not need to care for racial inheritance for ESTs.
|
||||||
|
foreach (var (est, value) in cache.Est)
|
||||||
|
{
|
||||||
|
switch (est.Slot)
|
||||||
|
{
|
||||||
|
case EstType.Hair when est.SetId == hairId && est.GenderRace == genderRace:
|
||||||
|
case EstType.Face when est.SetId == faceId && est.GenderRace == genderRace:
|
||||||
|
case EstType.Body when est.SetId == bodyId && est.GenderRace == genderRace:
|
||||||
|
case EstType.Head when (est.SetId == headId || est.SetId == bodyId) && est.GenderRace == genderRace:
|
||||||
|
wrapper.Est.Add(est, value.Entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (geqp, _) in cache.GlobalEqp)
|
||||||
|
{
|
||||||
|
switch (geqp.Type)
|
||||||
|
{
|
||||||
|
case GlobalEqpType.DoNotHideEarrings when geqp.Condition != earsId:
|
||||||
|
case GlobalEqpType.DoNotHideNecklace when geqp.Condition != neckId:
|
||||||
|
case GlobalEqpType.DoNotHideBracelets when geqp.Condition != wristId:
|
||||||
|
case GlobalEqpType.DoNotHideRingR when geqp.Condition != rFingerId:
|
||||||
|
case GlobalEqpType.DoNotHideRingL when geqp.Condition != lFingerId:
|
||||||
|
continue;
|
||||||
|
default: wrapper.Add(geqp); break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (_, _, main, off) = model.GetWeapons(actor);
|
||||||
|
foreach (var (imc, value) in cache.Imc)
|
||||||
|
{
|
||||||
|
switch (imc.ObjectType)
|
||||||
|
{
|
||||||
|
case ObjectType.Equipment when equipIdSet.Contains(imc.PrimaryId): wrapper.Imc.Add(imc, value.Entry); break;
|
||||||
|
|
||||||
|
case ObjectType.Weapon:
|
||||||
|
if (imc.PrimaryId == main.Skeleton && imc.SecondaryId == main.Weapon)
|
||||||
|
wrapper.Imc.Add(imc, value.Entry);
|
||||||
|
else if (imc.PrimaryId == off.Skeleton && imc.SecondaryId == off.Weapon)
|
||||||
|
wrapper.Imc.Add(imc, value.Entry);
|
||||||
|
break;
|
||||||
|
case ObjectType.Accessory:
|
||||||
|
switch (imc.EquipSlot)
|
||||||
|
{
|
||||||
|
case EquipSlot.Ears when imc.PrimaryId == earsId:
|
||||||
|
case EquipSlot.Neck when imc.PrimaryId == neckId:
|
||||||
|
case EquipSlot.Wrists when imc.PrimaryId == wristId:
|
||||||
|
case EquipSlot.RFinger when imc.PrimaryId == rFingerId:
|
||||||
|
case EquipSlot.LFinger when imc.PrimaryId == lFingerId:
|
||||||
|
wrapper.Imc.Add(imc, value.Entry);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var subRace = (SubRace)model.AsHuman->Customize[4];
|
||||||
|
foreach (var (rsp, value) in cache.Rsp)
|
||||||
|
{
|
||||||
|
if (rsp.SubRace == subRace)
|
||||||
|
wrapper.Rsp.Add(rsp, value.Entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep all atch, atr and shp.
|
||||||
|
wrapper.Atch.EnsureCapacity(cache.Atch.Count);
|
||||||
|
wrapper.Shp.EnsureCapacity(cache.Shp.Count);
|
||||||
|
wrapper.Atr.EnsureCapacity(cache.Atr.Count);
|
||||||
|
foreach (var (atch, value) in cache.Atch)
|
||||||
|
wrapper.Atch.Add(atch, value.Entry);
|
||||||
|
foreach (var (shp, value) in cache.Shp)
|
||||||
|
wrapper.Shp.Add(shp, value.Entry);
|
||||||
|
foreach (var (atr, value) in cache.Atr)
|
||||||
|
wrapper.Atr.Add(atr, value.Entry);
|
||||||
|
return wrapper;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Wrapper? _data;
|
||||||
|
|
||||||
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
|
public IReadOnlyDictionary<ImcIdentifier, ImcEntry> Imc
|
||||||
=> _imc;
|
=> _data?.Imc ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> Eqp
|
public IReadOnlyDictionary<EqpIdentifier, EqpEntryInternal> Eqp
|
||||||
=> _eqp;
|
=> _data?.Eqp ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp
|
public IReadOnlyDictionary<EqdpIdentifier, EqdpEntryInternal> Eqdp
|
||||||
=> _eqdp;
|
=> _data?.Eqdp ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<EstIdentifier, EstEntry> Est
|
public IReadOnlyDictionary<EstIdentifier, EstEntry> Est
|
||||||
=> _est;
|
=> _data?.Est ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<GmpIdentifier, GmpEntry> Gmp
|
public IReadOnlyDictionary<GmpIdentifier, GmpEntry> Gmp
|
||||||
=> _gmp;
|
=> _data?.Gmp ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
|
public IReadOnlyDictionary<RspIdentifier, RspEntry> Rsp
|
||||||
=> _rsp;
|
=> _data?.Rsp ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
|
public IReadOnlyDictionary<AtchIdentifier, AtchEntry> Atch
|
||||||
=> _atch;
|
=> _data?.Atch ?? [];
|
||||||
|
|
||||||
public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp
|
public IReadOnlyDictionary<ShpIdentifier, ShpEntry> Shp
|
||||||
=> _shp;
|
=> _data?.Shp ?? [];
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<AtrIdentifier, AtrEntry> Atr
|
||||||
|
=> _data?.Atr ?? [];
|
||||||
|
|
||||||
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
|
public IReadOnlySet<GlobalEqpManipulation> GlobalEqp
|
||||||
=> _globalEqp;
|
=> _data ?? [];
|
||||||
|
|
||||||
public int Count { get; private set; }
|
public int Count { get; private set; }
|
||||||
|
|
||||||
public int GetCount(MetaManipulationType type)
|
public int GetCount(MetaManipulationType type)
|
||||||
=> type switch
|
=> _data is null
|
||||||
{
|
? 0
|
||||||
MetaManipulationType.Imc => _imc.Count,
|
: type switch
|
||||||
MetaManipulationType.Eqdp => _eqdp.Count,
|
{
|
||||||
MetaManipulationType.Eqp => _eqp.Count,
|
MetaManipulationType.Imc => _data.Imc.Count,
|
||||||
MetaManipulationType.Est => _est.Count,
|
MetaManipulationType.Eqdp => _data.Eqdp.Count,
|
||||||
MetaManipulationType.Gmp => _gmp.Count,
|
MetaManipulationType.Eqp => _data.Eqp.Count,
|
||||||
MetaManipulationType.Rsp => _rsp.Count,
|
MetaManipulationType.Est => _data.Est.Count,
|
||||||
MetaManipulationType.Atch => _atch.Count,
|
MetaManipulationType.Gmp => _data.Gmp.Count,
|
||||||
MetaManipulationType.Shp => _shp.Count,
|
MetaManipulationType.Rsp => _data.Rsp.Count,
|
||||||
MetaManipulationType.GlobalEqp => _globalEqp.Count,
|
MetaManipulationType.Atch => _data.Atch.Count,
|
||||||
_ => 0,
|
MetaManipulationType.Shp => _data.Shp.Count,
|
||||||
};
|
MetaManipulationType.Atr => _data.Atr.Count,
|
||||||
|
MetaManipulationType.GlobalEqp => _data.Count,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
public bool Contains(IMetaIdentifier identifier)
|
public bool Contains(IMetaIdentifier identifier)
|
||||||
=> identifier switch
|
=> _data is not null
|
||||||
{
|
&& identifier switch
|
||||||
EqdpIdentifier i => _eqdp.ContainsKey(i),
|
{
|
||||||
EqpIdentifier i => _eqp.ContainsKey(i),
|
EqdpIdentifier i => _data.Eqdp.ContainsKey(i),
|
||||||
EstIdentifier i => _est.ContainsKey(i),
|
EqpIdentifier i => _data.Eqp.ContainsKey(i),
|
||||||
GlobalEqpManipulation i => _globalEqp.Contains(i),
|
EstIdentifier i => _data.Est.ContainsKey(i),
|
||||||
GmpIdentifier i => _gmp.ContainsKey(i),
|
GlobalEqpManipulation i => _data.Contains(i),
|
||||||
ImcIdentifier i => _imc.ContainsKey(i),
|
GmpIdentifier i => _data.Gmp.ContainsKey(i),
|
||||||
AtchIdentifier i => _atch.ContainsKey(i),
|
ImcIdentifier i => _data.Imc.ContainsKey(i),
|
||||||
ShpIdentifier i => _shp.ContainsKey(i),
|
AtchIdentifier i => _data.Atch.ContainsKey(i),
|
||||||
RspIdentifier i => _rsp.ContainsKey(i),
|
ShpIdentifier i => _data.Shp.ContainsKey(i),
|
||||||
_ => false,
|
AtrIdentifier i => _data.Atr.ContainsKey(i),
|
||||||
};
|
RspIdentifier i => _data.Rsp.ContainsKey(i),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
|
_data = null;
|
||||||
Count = 0;
|
Count = 0;
|
||||||
_imc.Clear();
|
|
||||||
_eqp.Clear();
|
|
||||||
_eqdp.Clear();
|
|
||||||
_est.Clear();
|
|
||||||
_rsp.Clear();
|
|
||||||
_gmp.Clear();
|
|
||||||
_atch.Clear();
|
|
||||||
_shp.Clear();
|
|
||||||
_globalEqp.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearForDefault()
|
public void ClearForDefault()
|
||||||
{
|
{
|
||||||
Count = _globalEqp.Count;
|
if (_data is null)
|
||||||
_imc.Clear();
|
return;
|
||||||
_eqp.Clear();
|
|
||||||
_eqdp.Clear();
|
if (_data.Count is 0 && Shp.Count is 0 && Atr.Count is 0)
|
||||||
_est.Clear();
|
{
|
||||||
_rsp.Clear();
|
_data = null;
|
||||||
_gmp.Clear();
|
Count = 0;
|
||||||
_atch.Clear();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Count = GlobalEqp.Count + Shp.Count + Atr.Count;
|
||||||
|
_data!.Imc.Clear();
|
||||||
|
_data!.Eqp.Clear();
|
||||||
|
_data!.Eqdp.Clear();
|
||||||
|
_data!.Est.Clear();
|
||||||
|
_data!.Rsp.Clear();
|
||||||
|
_data!.Gmp.Clear();
|
||||||
|
_data!.Atch.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Equals(MetaDictionary other)
|
public bool Equals(MetaDictionary other)
|
||||||
=> Count == other.Count
|
{
|
||||||
&& _imc.SetEquals(other._imc)
|
if (Count != other.Count)
|
||||||
&& _eqp.SetEquals(other._eqp)
|
return false;
|
||||||
&& _eqdp.SetEquals(other._eqdp)
|
|
||||||
&& _est.SetEquals(other._est)
|
if (_data is null)
|
||||||
&& _rsp.SetEquals(other._rsp)
|
return true;
|
||||||
&& _gmp.SetEquals(other._gmp)
|
|
||||||
&& _atch.SetEquals(other._atch)
|
return _data.Imc.SetEquals(other._data!.Imc)
|
||||||
&& _shp.SetEquals(other._shp)
|
&& _data.Eqp.SetEquals(other._data!.Eqp)
|
||||||
&& _globalEqp.SetEquals(other._globalEqp);
|
&& _data.Eqdp.SetEquals(other._data!.Eqdp)
|
||||||
|
&& _data.Est.SetEquals(other._data!.Est)
|
||||||
|
&& _data.Rsp.SetEquals(other._data!.Rsp)
|
||||||
|
&& _data.Gmp.SetEquals(other._data!.Gmp)
|
||||||
|
&& _data.Atch.SetEquals(other._data!.Atch)
|
||||||
|
&& _data.Shp.SetEquals(other._data!.Shp)
|
||||||
|
&& _data.Atr.SetEquals(other._data!.Atr)
|
||||||
|
&& _data.SetEquals(other._data!);
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<IMetaIdentifier> Identifiers
|
public IEnumerable<IMetaIdentifier> Identifiers
|
||||||
=> _imc.Keys.Cast<IMetaIdentifier>()
|
=> _data is null
|
||||||
.Concat(_eqdp.Keys.Cast<IMetaIdentifier>())
|
? []
|
||||||
.Concat(_eqp.Keys.Cast<IMetaIdentifier>())
|
: _data.Imc.Keys.Cast<IMetaIdentifier>()
|
||||||
.Concat(_est.Keys.Cast<IMetaIdentifier>())
|
.Concat(_data!.Eqdp.Keys.Cast<IMetaIdentifier>())
|
||||||
.Concat(_gmp.Keys.Cast<IMetaIdentifier>())
|
.Concat(_data!.Eqp.Keys.Cast<IMetaIdentifier>())
|
||||||
.Concat(_rsp.Keys.Cast<IMetaIdentifier>())
|
.Concat(_data!.Est.Keys.Cast<IMetaIdentifier>())
|
||||||
.Concat(_atch.Keys.Cast<IMetaIdentifier>())
|
.Concat(_data!.Gmp.Keys.Cast<IMetaIdentifier>())
|
||||||
.Concat(_shp.Keys.Cast<IMetaIdentifier>())
|
.Concat(_data!.Rsp.Keys.Cast<IMetaIdentifier>())
|
||||||
.Concat(_globalEqp.Cast<IMetaIdentifier>());
|
.Concat(_data!.Atch.Keys.Cast<IMetaIdentifier>())
|
||||||
|
.Concat(_data!.Shp.Keys.Cast<IMetaIdentifier>())
|
||||||
|
.Concat(_data!.Atr.Keys.Cast<IMetaIdentifier>())
|
||||||
|
.Concat(_data!.Cast<IMetaIdentifier>());
|
||||||
|
|
||||||
#region TryAdd
|
#region TryAdd
|
||||||
|
|
||||||
public bool TryAdd(ImcIdentifier identifier, ImcEntry entry)
|
public bool TryAdd(ImcIdentifier identifier, ImcEntry entry)
|
||||||
{
|
{
|
||||||
if (!_imc.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Imc.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -142,7 +349,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry)
|
public bool TryAdd(EqpIdentifier identifier, EqpEntryInternal entry)
|
||||||
{
|
{
|
||||||
if (!_eqp.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Eqp.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -154,7 +362,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry)
|
public bool TryAdd(EqdpIdentifier identifier, EqdpEntryInternal entry)
|
||||||
{
|
{
|
||||||
if (!_eqdp.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Eqdp.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -166,7 +375,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(EstIdentifier identifier, EstEntry entry)
|
public bool TryAdd(EstIdentifier identifier, EstEntry entry)
|
||||||
{
|
{
|
||||||
if (!_est.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Est.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -175,7 +385,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(GmpIdentifier identifier, GmpEntry entry)
|
public bool TryAdd(GmpIdentifier identifier, GmpEntry entry)
|
||||||
{
|
{
|
||||||
if (!_gmp.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Gmp.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -184,7 +395,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(RspIdentifier identifier, RspEntry entry)
|
public bool TryAdd(RspIdentifier identifier, RspEntry entry)
|
||||||
{
|
{
|
||||||
if (!_rsp.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Rsp.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -193,7 +405,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry)
|
public bool TryAdd(AtchIdentifier identifier, in AtchEntry entry)
|
||||||
{
|
{
|
||||||
if (!_atch.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Atch.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -202,7 +415,18 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry)
|
public bool TryAdd(ShpIdentifier identifier, in ShpEntry entry)
|
||||||
{
|
{
|
||||||
if (!_shp.TryAdd(identifier, entry))
|
_data ??= [];
|
||||||
|
if (!_data!.Shp.TryAdd(identifier, entry))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
++Count;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAdd(AtrIdentifier identifier, in AtrEntry entry)
|
||||||
|
{
|
||||||
|
_data ??= [];
|
||||||
|
if (!_data!.Atr.TryAdd(identifier, entry))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -211,7 +435,8 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool TryAdd(GlobalEqpManipulation identifier)
|
public bool TryAdd(GlobalEqpManipulation identifier)
|
||||||
{
|
{
|
||||||
if (!_globalEqp.Add(identifier))
|
_data ??= [];
|
||||||
|
if (!_data.Add(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
++Count;
|
++Count;
|
||||||
|
|
@ -224,19 +449,19 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool Update(ImcIdentifier identifier, ImcEntry entry)
|
public bool Update(ImcIdentifier identifier, ImcEntry entry)
|
||||||
{
|
{
|
||||||
if (!_imc.ContainsKey(identifier))
|
if (_data is null || !_data.Imc.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_imc[identifier] = entry;
|
_data.Imc[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(EqpIdentifier identifier, EqpEntryInternal entry)
|
public bool Update(EqpIdentifier identifier, EqpEntryInternal entry)
|
||||||
{
|
{
|
||||||
if (!_eqp.ContainsKey(identifier))
|
if (_data is null || !_data.Eqp.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_eqp[identifier] = entry;
|
_data.Eqp[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,10 +470,10 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry)
|
public bool Update(EqdpIdentifier identifier, EqdpEntryInternal entry)
|
||||||
{
|
{
|
||||||
if (!_eqdp.ContainsKey(identifier))
|
if (_data is null || !_data.Eqdp.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_eqdp[identifier] = entry;
|
_data.Eqdp[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -257,46 +482,55 @@ public class MetaDictionary
|
||||||
|
|
||||||
public bool Update(EstIdentifier identifier, EstEntry entry)
|
public bool Update(EstIdentifier identifier, EstEntry entry)
|
||||||
{
|
{
|
||||||
if (!_est.ContainsKey(identifier))
|
if (_data is null || !_data.Est.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_est[identifier] = entry;
|
_data.Est[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(GmpIdentifier identifier, GmpEntry entry)
|
public bool Update(GmpIdentifier identifier, GmpEntry entry)
|
||||||
{
|
{
|
||||||
if (!_gmp.ContainsKey(identifier))
|
if (_data is null || !_data.Gmp.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_gmp[identifier] = entry;
|
_data.Gmp[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(RspIdentifier identifier, RspEntry entry)
|
public bool Update(RspIdentifier identifier, RspEntry entry)
|
||||||
{
|
{
|
||||||
if (!_rsp.ContainsKey(identifier))
|
if (_data is null || !_data.Rsp.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_rsp[identifier] = entry;
|
_data.Rsp[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(AtchIdentifier identifier, in AtchEntry entry)
|
public bool Update(AtchIdentifier identifier, in AtchEntry entry)
|
||||||
{
|
{
|
||||||
if (!_atch.ContainsKey(identifier))
|
if (_data is null || !_data.Atch.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_atch[identifier] = entry;
|
_data.Atch[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool Update(ShpIdentifier identifier, in ShpEntry entry)
|
public bool Update(ShpIdentifier identifier, in ShpEntry entry)
|
||||||
{
|
{
|
||||||
if (!_shp.ContainsKey(identifier))
|
if (_data is null || !_data.Shp.ContainsKey(identifier))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
_shp[identifier] = entry;
|
_data.Shp[identifier] = entry;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Update(AtrIdentifier identifier, in AtrEntry entry)
|
||||||
|
{
|
||||||
|
if (_data is null || !_data.Atr.ContainsKey(identifier))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_data.Atr[identifier] = entry;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -305,48 +539,63 @@ public class MetaDictionary
|
||||||
#region TryGetValue
|
#region TryGetValue
|
||||||
|
|
||||||
public bool TryGetValue(EstIdentifier identifier, out EstEntry value)
|
public bool TryGetValue(EstIdentifier identifier, out EstEntry value)
|
||||||
=> _est.TryGetValue(identifier, out value);
|
=> _data?.Est.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value)
|
public bool TryGetValue(EqpIdentifier identifier, out EqpEntryInternal value)
|
||||||
=> _eqp.TryGetValue(identifier, out value);
|
=> _data?.Eqp.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value)
|
public bool TryGetValue(EqdpIdentifier identifier, out EqdpEntryInternal value)
|
||||||
=> _eqdp.TryGetValue(identifier, out value);
|
=> _data?.Eqdp.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value)
|
public bool TryGetValue(GmpIdentifier identifier, out GmpEntry value)
|
||||||
=> _gmp.TryGetValue(identifier, out value);
|
=> _data?.Gmp.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(RspIdentifier identifier, out RspEntry value)
|
public bool TryGetValue(RspIdentifier identifier, out RspEntry value)
|
||||||
=> _rsp.TryGetValue(identifier, out value);
|
=> _data?.Rsp.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
|
public bool TryGetValue(ImcIdentifier identifier, out ImcEntry value)
|
||||||
=> _imc.TryGetValue(identifier, out value);
|
=> _data?.Imc.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value)
|
public bool TryGetValue(AtchIdentifier identifier, out AtchEntry value)
|
||||||
=> _atch.TryGetValue(identifier, out value);
|
=> _data?.Atch.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value)
|
public bool TryGetValue(ShpIdentifier identifier, out ShpEntry value)
|
||||||
=> _shp.TryGetValue(identifier, out value);
|
=> _data?.Shp.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
|
public bool TryGetValue(AtrIdentifier identifier, out AtrEntry value)
|
||||||
|
=> _data?.Atr.TryGetValue(identifier, out value) ?? SetDefault(out value);
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
|
private static bool SetDefault<T>(out T? value)
|
||||||
|
{
|
||||||
|
value = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public bool Remove(IMetaIdentifier identifier)
|
public bool Remove(IMetaIdentifier identifier)
|
||||||
{
|
{
|
||||||
|
if (_data is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
var ret = identifier switch
|
var ret = identifier switch
|
||||||
{
|
{
|
||||||
EqdpIdentifier i => _eqdp.Remove(i),
|
EqdpIdentifier i => _data.Eqdp.Remove(i),
|
||||||
EqpIdentifier i => _eqp.Remove(i),
|
EqpIdentifier i => _data.Eqp.Remove(i),
|
||||||
EstIdentifier i => _est.Remove(i),
|
EstIdentifier i => _data.Est.Remove(i),
|
||||||
GlobalEqpManipulation i => _globalEqp.Remove(i),
|
GlobalEqpManipulation i => _data.Remove(i),
|
||||||
GmpIdentifier i => _gmp.Remove(i),
|
GmpIdentifier i => _data.Gmp.Remove(i),
|
||||||
ImcIdentifier i => _imc.Remove(i),
|
ImcIdentifier i => _data.Imc.Remove(i),
|
||||||
RspIdentifier i => _rsp.Remove(i),
|
RspIdentifier i => _data.Rsp.Remove(i),
|
||||||
AtchIdentifier i => _atch.Remove(i),
|
AtchIdentifier i => _data.Atch.Remove(i),
|
||||||
ShpIdentifier i => _shp.Remove(i),
|
ShpIdentifier i => _data.Shp.Remove(i),
|
||||||
|
AtrIdentifier i => _data.Atr.Remove(i),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
if (ret)
|
if (ret && --Count is 0)
|
||||||
--Count;
|
_data = null;
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,86 +603,106 @@ public class MetaDictionary
|
||||||
|
|
||||||
public void UnionWith(MetaDictionary manips)
|
public void UnionWith(MetaDictionary manips)
|
||||||
{
|
{
|
||||||
foreach (var (identifier, entry) in manips._imc)
|
if (manips.Count is 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_data ??= [];
|
||||||
|
foreach (var (identifier, entry) in manips._data!.Imc)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._eqp)
|
foreach (var (identifier, entry) in manips._data!.Eqp)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._eqdp)
|
foreach (var (identifier, entry) in manips._data!.Eqdp)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._gmp)
|
foreach (var (identifier, entry) in manips._data!.Gmp)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._rsp)
|
foreach (var (identifier, entry) in manips._data!.Rsp)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._est)
|
foreach (var (identifier, entry) in manips._data!.Est)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._atch)
|
foreach (var (identifier, entry) in manips._data!.Atch)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var (identifier, entry) in manips._shp)
|
foreach (var (identifier, entry) in manips._data!.Shp)
|
||||||
TryAdd(identifier, entry);
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
foreach (var identifier in manips._globalEqp)
|
foreach (var (identifier, entry) in manips._data!.Atr)
|
||||||
|
TryAdd(identifier, entry);
|
||||||
|
|
||||||
|
foreach (var identifier in manips._data!)
|
||||||
TryAdd(identifier);
|
TryAdd(identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary> Try to merge all manipulations from manips into this, and return the first failure, if any. </summary>
|
/// <summary> Try to merge all manipulations from manips into this, and return the first failure, if any. </summary>
|
||||||
public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier)
|
public bool MergeForced(MetaDictionary manips, out IMetaIdentifier? failedIdentifier)
|
||||||
{
|
{
|
||||||
foreach (var (identifier, _) in manips._imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
if (manips.Count is 0)
|
||||||
|
{
|
||||||
|
failedIdentifier = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_data ??= [];
|
||||||
|
foreach (var (identifier, _) in manips._data!.Imc.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Eqp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Eqdp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Gmp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Rsp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._est.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Est.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Atch.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var (identifier, _) in manips._shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
foreach (var (identifier, _) in manips._data!.Shp.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var identifier in manips._globalEqp.Where(identifier => !TryAdd(identifier)))
|
foreach (var (identifier, _) in manips._data!.Atr.Where(kvp => !TryAdd(kvp.Key, kvp.Value)))
|
||||||
|
{
|
||||||
|
failedIdentifier = identifier;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var identifier in manips._data!.Where(identifier => !TryAdd(identifier)))
|
||||||
{
|
{
|
||||||
failedIdentifier = identifier;
|
failedIdentifier = identifier;
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -445,30 +714,53 @@ public class MetaDictionary
|
||||||
|
|
||||||
public void SetTo(MetaDictionary other)
|
public void SetTo(MetaDictionary other)
|
||||||
{
|
{
|
||||||
_imc.SetTo(other._imc);
|
if (other.Count is 0)
|
||||||
_eqp.SetTo(other._eqp);
|
{
|
||||||
_eqdp.SetTo(other._eqdp);
|
_data = null;
|
||||||
_est.SetTo(other._est);
|
Count = 0;
|
||||||
_rsp.SetTo(other._rsp);
|
return;
|
||||||
_gmp.SetTo(other._gmp);
|
}
|
||||||
_atch.SetTo(other._atch);
|
|
||||||
_shp.SetTo(other._shp);
|
_data ??= [];
|
||||||
_globalEqp.SetTo(other._globalEqp);
|
_data!.Imc.SetTo(other._data!.Imc);
|
||||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count;
|
_data!.Eqp.SetTo(other._data!.Eqp);
|
||||||
|
_data!.Eqdp.SetTo(other._data!.Eqdp);
|
||||||
|
_data!.Est.SetTo(other._data!.Est);
|
||||||
|
_data!.Rsp.SetTo(other._data!.Rsp);
|
||||||
|
_data!.Gmp.SetTo(other._data!.Gmp);
|
||||||
|
_data!.Atch.SetTo(other._data!.Atch);
|
||||||
|
_data!.Shp.SetTo(other._data!.Shp);
|
||||||
|
_data!.Atr.SetTo(other._data!.Atr);
|
||||||
|
_data!.SetTo(other._data!);
|
||||||
|
Count = other.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateTo(MetaDictionary other)
|
public void UpdateTo(MetaDictionary other)
|
||||||
{
|
{
|
||||||
_imc.UpdateTo(other._imc);
|
if (other.Count is 0)
|
||||||
_eqp.UpdateTo(other._eqp);
|
return;
|
||||||
_eqdp.UpdateTo(other._eqdp);
|
|
||||||
_est.UpdateTo(other._est);
|
_data ??= [];
|
||||||
_rsp.UpdateTo(other._rsp);
|
_data!.Imc.UpdateTo(other._data!.Imc);
|
||||||
_gmp.UpdateTo(other._gmp);
|
_data!.Eqp.UpdateTo(other._data!.Eqp);
|
||||||
_atch.UpdateTo(other._atch);
|
_data!.Eqdp.UpdateTo(other._data!.Eqdp);
|
||||||
_shp.UpdateTo(other._shp);
|
_data!.Est.UpdateTo(other._data!.Est);
|
||||||
_globalEqp.UnionWith(other._globalEqp);
|
_data!.Rsp.UpdateTo(other._data!.Rsp);
|
||||||
Count = _imc.Count + _eqp.Count + _eqdp.Count + _est.Count + _rsp.Count + _gmp.Count + _atch.Count + _shp.Count + _globalEqp.Count;
|
_data!.Gmp.UpdateTo(other._data!.Gmp);
|
||||||
|
_data!.Atch.UpdateTo(other._data!.Atch);
|
||||||
|
_data!.Shp.UpdateTo(other._data!.Shp);
|
||||||
|
_data!.Atr.UpdateTo(other._data!.Atr);
|
||||||
|
_data!.UnionWith(other._data!);
|
||||||
|
Count = _data!.Imc.Count
|
||||||
|
+ _data!.Eqp.Count
|
||||||
|
+ _data!.Eqdp.Count
|
||||||
|
+ _data!.Est.Count
|
||||||
|
+ _data!.Rsp.Count
|
||||||
|
+ _data!.Gmp.Count
|
||||||
|
+ _data!.Atch.Count
|
||||||
|
+ _data!.Shp.Count
|
||||||
|
+ _data!.Atr.Count
|
||||||
|
+ _data!.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
@ -566,6 +858,16 @@ public class MetaDictionary
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
public static JObject Serialize(AtrIdentifier identifier, AtrEntry entry)
|
||||||
|
=> new()
|
||||||
|
{
|
||||||
|
["Type"] = MetaManipulationType.Atr.ToString(),
|
||||||
|
["Manipulation"] = identifier.AddToJson(new JObject
|
||||||
|
{
|
||||||
|
["Entry"] = entry.Value,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
public static JObject Serialize(GlobalEqpManipulation identifier)
|
public static JObject Serialize(GlobalEqpManipulation identifier)
|
||||||
=> new()
|
=> new()
|
||||||
{
|
{
|
||||||
|
|
@ -597,6 +899,8 @@ public class MetaDictionary
|
||||||
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
|
return Serialize(Unsafe.As<TIdentifier, AtchIdentifier>(ref identifier), Unsafe.As<TEntry, AtchEntry>(ref entry));
|
||||||
if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry))
|
if (typeof(TIdentifier) == typeof(ShpIdentifier) && typeof(TEntry) == typeof(ShpEntry))
|
||||||
return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry));
|
return Serialize(Unsafe.As<TIdentifier, ShpIdentifier>(ref identifier), Unsafe.As<TEntry, ShpEntry>(ref entry));
|
||||||
|
if (typeof(TIdentifier) == typeof(AtrIdentifier) && typeof(TEntry) == typeof(AtrEntry))
|
||||||
|
return Serialize(Unsafe.As<TIdentifier, AtrIdentifier>(ref identifier), Unsafe.As<TEntry, AtrEntry>(ref entry));
|
||||||
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
|
if (typeof(TIdentifier) == typeof(GlobalEqpManipulation))
|
||||||
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
|
return Serialize(Unsafe.As<TIdentifier, GlobalEqpManipulation>(ref identifier));
|
||||||
|
|
||||||
|
|
@ -635,15 +939,20 @@ public class MetaDictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
var array = new JArray();
|
var array = new JArray();
|
||||||
SerializeTo(array, value._imc);
|
if (value._data is not null)
|
||||||
SerializeTo(array, value._eqp);
|
{
|
||||||
SerializeTo(array, value._eqdp);
|
SerializeTo(array, value._data!.Imc);
|
||||||
SerializeTo(array, value._est);
|
SerializeTo(array, value._data!.Eqp);
|
||||||
SerializeTo(array, value._rsp);
|
SerializeTo(array, value._data!.Eqdp);
|
||||||
SerializeTo(array, value._gmp);
|
SerializeTo(array, value._data!.Est);
|
||||||
SerializeTo(array, value._atch);
|
SerializeTo(array, value._data!.Rsp);
|
||||||
SerializeTo(array, value._shp);
|
SerializeTo(array, value._data!.Gmp);
|
||||||
SerializeTo(array, value._globalEqp);
|
SerializeTo(array, value._data!.Atch);
|
||||||
|
SerializeTo(array, value._data!.Shp);
|
||||||
|
SerializeTo(array, value._data!.Atr);
|
||||||
|
SerializeTo(array, value._data!);
|
||||||
|
}
|
||||||
|
|
||||||
array.WriteTo(writer);
|
array.WriteTo(writer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -750,6 +1059,16 @@ public class MetaDictionary
|
||||||
Penumbra.Log.Warning("Invalid SHP Manipulation encountered.");
|
Penumbra.Log.Warning("Invalid SHP Manipulation encountered.");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case MetaManipulationType.Atr:
|
||||||
|
{
|
||||||
|
var identifier = AtrIdentifier.FromJson(manip);
|
||||||
|
var entry = new AtrEntry(manip["Entry"]?.Value<bool>() ?? true);
|
||||||
|
if (identifier.HasValue)
|
||||||
|
dict.TryAdd(identifier.Value, entry);
|
||||||
|
else
|
||||||
|
Penumbra.Log.Warning("Invalid ATR Manipulation encountered.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
case MetaManipulationType.GlobalEqp:
|
case MetaManipulationType.GlobalEqp:
|
||||||
{
|
{
|
||||||
var identifier = GlobalEqpManipulation.FromJson(manip);
|
var identifier = GlobalEqpManipulation.FromJson(manip);
|
||||||
|
|
@ -771,18 +1090,30 @@ public class MetaDictionary
|
||||||
|
|
||||||
public MetaDictionary(MetaCache? cache)
|
public MetaDictionary(MetaCache? cache)
|
||||||
{
|
{
|
||||||
if (cache == null)
|
if (cache is null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_imc = cache.Imc.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
_data = new Wrapper(cache);
|
||||||
_eqp = cache.Eqp.ToDictionary(kvp => kvp.Key, kvp => new EqpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
|
Count = cache.Count;
|
||||||
_eqdp = cache.Eqdp.ToDictionary(kvp => kvp.Key, kvp => new EqdpEntryInternal(kvp.Value.Entry, kvp.Key.Slot));
|
}
|
||||||
_est = cache.Est.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
|
||||||
_gmp = cache.Gmp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
public MetaDictionary(MetaCache? cache, Actor actor)
|
||||||
_rsp = cache.Rsp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
{
|
||||||
_atch = cache.Atch.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
if (cache is null)
|
||||||
_shp = cache.Shp.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Entry);
|
return;
|
||||||
_globalEqp = cache.GlobalEqp.Select(kvp => kvp.Key).ToHashSet();
|
|
||||||
Count = cache.Count;
|
_data = Wrapper.Filtered(cache, actor);
|
||||||
|
Count = _data.Count
|
||||||
|
+ _data.Eqp.Count
|
||||||
|
+ _data.Eqdp.Count
|
||||||
|
+ _data.Est.Count
|
||||||
|
+ _data.Gmp.Count
|
||||||
|
+ _data.Imc.Count
|
||||||
|
+ _data.Rsp.Count
|
||||||
|
+ _data.Atch.Count
|
||||||
|
+ _data.Atr.Count
|
||||||
|
+ _data.Shp.Count;
|
||||||
|
if (Count is 0)
|
||||||
|
_data = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,30 @@
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Converters;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
using Penumbra.GameData.Enums;
|
using Penumbra.GameData.Enums;
|
||||||
using Penumbra.GameData.Structs;
|
using Penumbra.GameData.Structs;
|
||||||
using Penumbra.Interop.Structs;
|
using Penumbra.Interop.Structs;
|
||||||
|
using Penumbra.Meta.Files;
|
||||||
|
|
||||||
namespace Penumbra.Meta.Manipulations;
|
namespace Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, ShapeString Shape, ShapeString ShapeCondition)
|
[JsonConverter(typeof(StringEnumConverter))]
|
||||||
|
public enum ShapeConnectorCondition : byte
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Wrists = 1,
|
||||||
|
Waist = 2,
|
||||||
|
Ankles = 3,
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct ShpIdentifier(
|
||||||
|
HumanSlot Slot,
|
||||||
|
PrimaryId? Id,
|
||||||
|
ShapeAttributeString Shape,
|
||||||
|
ShapeConnectorCondition ConnectorCondition,
|
||||||
|
GenderRace GenderRaceCondition)
|
||||||
: IComparable<ShpIdentifier>, IMetaIdentifier
|
: IComparable<ShpIdentifier>, IMetaIdentifier
|
||||||
{
|
{
|
||||||
public int CompareTo(ShpIdentifier other)
|
public int CompareTo(ShpIdentifier other)
|
||||||
|
|
@ -34,11 +51,15 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var shapeComparison = Shape.CompareTo(other.Shape);
|
var conditionComparison = ConnectorCondition.CompareTo(other.ConnectorCondition);
|
||||||
if (shapeComparison is not 0)
|
if (conditionComparison is not 0)
|
||||||
return shapeComparison;
|
return conditionComparison;
|
||||||
|
|
||||||
return ShapeCondition.CompareTo(other.ShapeCondition);
|
var genderRaceComparison = GenderRaceCondition.CompareTo(other.GenderRaceCondition);
|
||||||
|
if (genderRaceComparison is not 0)
|
||||||
|
return genderRaceComparison;
|
||||||
|
|
||||||
|
return Shape.CompareTo(other.Shape);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -62,9 +83,16 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
sb.Append("All IDs");
|
sb.Append("All IDs");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShapeCondition.Length > 0)
|
switch (ConnectorCondition)
|
||||||
sb.Append(" - ")
|
{
|
||||||
.Append(ShapeCondition);
|
case ShapeConnectorCondition.Wrists: sb.Append(" - Wrist Connector"); break;
|
||||||
|
case ShapeConnectorCondition.Waist: sb.Append(" - Waist Connector"); break;
|
||||||
|
case ShapeConnectorCondition.Ankles: sb.Append(" - Ankle Connector"); break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GenderRaceCondition is not GenderRace.Unknown)
|
||||||
|
sb.Append(" - ").Append(GenderRaceCondition.ToRaceCode());
|
||||||
|
|
||||||
return sb.ToString();
|
return sb.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -81,63 +109,34 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
|
if (!Enum.IsDefined(Slot) || Slot is HumanSlot.UnkBonus)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (!ShapeAttributeHashSet.GenderRaceIndices.ContainsKey(GenderRaceCondition))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!Enum.IsDefined(ConnectorCondition))
|
||||||
|
return false;
|
||||||
|
|
||||||
if (Slot is HumanSlot.Unknown && Id is not null)
|
if (Slot is HumanSlot.Unknown && Id is not null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!ValidateCustomShapeString(Shape))
|
if (Slot.ToSpecificEnum() is BodySlot && Id is { Id: > byte.MaxValue })
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (ShapeCondition.Length is 0)
|
if (Id is { Id: > ExpandedEqpGmpBase.Count - 1 })
|
||||||
return true;
|
|
||||||
|
|
||||||
if (!ValidateCustomShapeString(ShapeCondition))
|
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
return Slot switch
|
if (!Shape.ValidateCustomShapeString())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ConnectorCondition switch
|
||||||
{
|
{
|
||||||
HumanSlot.Hands when ShapeCondition.IsWrist() => true,
|
ShapeConnectorCondition.None => true,
|
||||||
HumanSlot.Body when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() => true,
|
ShapeConnectorCondition.Wrists => Slot is HumanSlot.Body or HumanSlot.Hands or HumanSlot.Unknown,
|
||||||
HumanSlot.Legs when ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
|
ShapeConnectorCondition.Waist => Slot is HumanSlot.Body or HumanSlot.Legs or HumanSlot.Unknown,
|
||||||
HumanSlot.Feet when ShapeCondition.IsAnkle() => true,
|
ShapeConnectorCondition.Ankles => Slot is HumanSlot.Legs or HumanSlot.Feet or HumanSlot.Unknown,
|
||||||
HumanSlot.Unknown when ShapeCondition.IsWrist() || ShapeCondition.IsWaist() || ShapeCondition.IsAnkle() => true,
|
_ => false,
|
||||||
_ => false,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static unsafe bool ValidateCustomShapeString(byte* shape)
|
|
||||||
{
|
|
||||||
// "shpx_*"
|
|
||||||
if (shape is null)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (*shape++ is not (byte)'s'
|
|
||||||
|| *shape++ is not (byte)'h'
|
|
||||||
|| *shape++ is not (byte)'p'
|
|
||||||
|| *shape++ is not (byte)'x'
|
|
||||||
|| *shape++ is not (byte)'_'
|
|
||||||
|| *shape is 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static bool ValidateCustomShapeString(in ShapeString shape)
|
|
||||||
{
|
|
||||||
// "shpx_*"
|
|
||||||
if (shape.Length < 6)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
var span = shape.AsSpan;
|
|
||||||
if (span[0] is not (byte)'s'
|
|
||||||
|| span[1] is not (byte)'h'
|
|
||||||
|| span[2] is not (byte)'p'
|
|
||||||
|| span[3] is not (byte)'x'
|
|
||||||
|| span[4] is not (byte)'_')
|
|
||||||
return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public JObject AddToJson(JObject jObj)
|
public JObject AddToJson(JObject jObj)
|
||||||
{
|
{
|
||||||
if (Slot is not HumanSlot.Unknown)
|
if (Slot is not HumanSlot.Unknown)
|
||||||
|
|
@ -145,22 +144,24 @@ public readonly record struct ShpIdentifier(HumanSlot Slot, PrimaryId? Id, Shape
|
||||||
if (Id.HasValue)
|
if (Id.HasValue)
|
||||||
jObj["Id"] = Id.Value.Id.ToString();
|
jObj["Id"] = Id.Value.Id.ToString();
|
||||||
jObj["Shape"] = Shape.ToString();
|
jObj["Shape"] = Shape.ToString();
|
||||||
if (ShapeCondition.Length > 0)
|
if (ConnectorCondition is not ShapeConnectorCondition.None)
|
||||||
jObj["ShapeCondition"] = ShapeCondition.ToString();
|
jObj["ConnectorCondition"] = ConnectorCondition.ToString();
|
||||||
|
if (GenderRaceCondition is not GenderRace.Unknown)
|
||||||
|
jObj["GenderRaceCondition"] = (uint)GenderRaceCondition;
|
||||||
return jObj;
|
return jObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ShpIdentifier? FromJson(JObject jObj)
|
public static ShpIdentifier? FromJson(JObject jObj)
|
||||||
{
|
{
|
||||||
var shape = jObj["Shape"]?.ToObject<string>();
|
var shape = jObj["Shape"]?.ToObject<string>();
|
||||||
if (shape is null || !ShapeString.TryRead(shape, out var shapeString))
|
if (shape is null || !ShapeAttributeString.TryRead(shape, out var shapeString))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
var slot = jObj["Slot"]?.ToObject<HumanSlot>() ?? HumanSlot.Unknown;
|
||||||
var id = jObj["Id"]?.ToObject<ushort>();
|
var id = jObj["Id"]?.ToObject<ushort>();
|
||||||
var shapeCondition = jObj["ShapeCondition"]?.ToObject<string>();
|
var connectorCondition = jObj["ConnectorCondition"]?.ToObject<ShapeConnectorCondition>() ?? ShapeConnectorCondition.None;
|
||||||
var shapeConditionString = shapeCondition is null || !ShapeString.TryRead(shapeCondition, out var s) ? ShapeString.Empty : s;
|
var genderRaceCondition = jObj["GenderRaceCondition"]?.ToObject<GenderRace>() ?? 0;
|
||||||
var identifier = new ShpIdentifier(slot, id, shapeString, shapeConditionString);
|
var identifier = new ShpIdentifier(slot, id, shapeString, connectorCondition, genderRaceCondition);
|
||||||
return identifier.Validate() ? identifier : null;
|
return identifier.Validate() ? identifier : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
227
Penumbra/Meta/ShapeAttributeManager.cs
Normal file
227
Penumbra/Meta/ShapeAttributeManager.cs
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
using OtterGui.Services;
|
||||||
|
using Penumbra.Collections;
|
||||||
|
using Penumbra.Collections.Cache;
|
||||||
|
using Penumbra.GameData.Enums;
|
||||||
|
using Penumbra.GameData.Interop;
|
||||||
|
using Penumbra.GameData.Structs;
|
||||||
|
using Penumbra.Interop.Hooks.PostProcessing;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
|
||||||
|
namespace Penumbra.Meta;
|
||||||
|
|
||||||
|
public unsafe class ShapeAttributeManager : IRequiredService, IDisposable
|
||||||
|
{
|
||||||
|
public const int NumSlots = 14;
|
||||||
|
public const int ModelSlotSize = 18;
|
||||||
|
private readonly AttributeHook _attributeHook;
|
||||||
|
|
||||||
|
public static ReadOnlySpan<HumanSlot> UsedModels
|
||||||
|
=>
|
||||||
|
[
|
||||||
|
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
|
||||||
|
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
|
||||||
|
];
|
||||||
|
|
||||||
|
public ShapeAttributeManager(AttributeHook attributeHook)
|
||||||
|
{
|
||||||
|
_attributeHook = attributeHook;
|
||||||
|
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeAttributeManager);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<ShapeAttributeString, short>[] _temporaryShapes =
|
||||||
|
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeAttributeString, short>()).ToArray();
|
||||||
|
|
||||||
|
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
|
||||||
|
|
||||||
|
private HumanSlot _modelIndex;
|
||||||
|
private int _slotIndex;
|
||||||
|
private GenderRace _genderRace;
|
||||||
|
|
||||||
|
private FFXIVClientStructs.FFXIV.Client.Graphics.Render.Model* _model;
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
=> _attributeHook.Unsubscribe(OnAttributeComputed);
|
||||||
|
|
||||||
|
private void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
|
||||||
|
{
|
||||||
|
if (!collection.HasCache)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_genderRace = (GenderRace)model.AsHuman->RaceSexId;
|
||||||
|
for (_slotIndex = 0; _slotIndex < NumSlots; ++_slotIndex)
|
||||||
|
{
|
||||||
|
_modelIndex = UsedModels[_slotIndex];
|
||||||
|
_model = model.AsHuman->Models[_modelIndex.ToIndex()];
|
||||||
|
if (_model is null || _model->ModelResourceHandle is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_ids[(int)_modelIndex] = model.GetModelId(_modelIndex);
|
||||||
|
CheckShapes(collection.MetaCache!.Shp);
|
||||||
|
CheckAttributes(collection.MetaCache!.Atr);
|
||||||
|
if (_modelIndex is <= HumanSlot.LFinger and >= HumanSlot.Ears)
|
||||||
|
AccessoryImcCheck(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateDefaultMasks(model, collection.MetaCache!.Shp);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AccessoryImcCheck(Model model)
|
||||||
|
{
|
||||||
|
var imcMask = (ushort)(0x03FF & *(ushort*)(model.Address + 0xAAC + 6 * (int)_modelIndex));
|
||||||
|
|
||||||
|
Span<byte> attr =
|
||||||
|
[
|
||||||
|
(byte)'a',
|
||||||
|
(byte)'t',
|
||||||
|
(byte)'r',
|
||||||
|
(byte)'_',
|
||||||
|
AccessoryByte(_modelIndex),
|
||||||
|
(byte)'v',
|
||||||
|
(byte)'_',
|
||||||
|
(byte)'a',
|
||||||
|
0,
|
||||||
|
];
|
||||||
|
for (var i = 1; i < 10; ++i)
|
||||||
|
{
|
||||||
|
var flag = (ushort)(1 << i);
|
||||||
|
if ((imcMask & flag) is not 0)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
attr[^2] = (byte)('a' + i);
|
||||||
|
|
||||||
|
foreach (var (attribute, index) in _model->ModelResourceHandle->Attributes)
|
||||||
|
{
|
||||||
|
if (!EqualAttribute(attr, attribute.Value))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
_model->EnabledAttributeIndexMask &= ~(1u << index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveOptimization | MethodImplOptions.AggressiveInlining)]
|
||||||
|
private static bool EqualAttribute(Span<byte> needle, byte* haystack)
|
||||||
|
{
|
||||||
|
foreach (var character in needle)
|
||||||
|
{
|
||||||
|
if (*haystack++ != character)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AccessoryByte(HumanSlot slot)
|
||||||
|
=> slot switch
|
||||||
|
{
|
||||||
|
HumanSlot.Head => (byte)'m',
|
||||||
|
HumanSlot.Ears => (byte)'e',
|
||||||
|
HumanSlot.Neck => (byte)'n',
|
||||||
|
HumanSlot.Wrists => (byte)'w',
|
||||||
|
HumanSlot.RFinger => (byte)'r',
|
||||||
|
HumanSlot.LFinger => (byte)'r',
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
private void CheckAttributes(AtrCache attributeCache)
|
||||||
|
{
|
||||||
|
if (attributeCache.DisabledCount is 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ref var attributes = ref _model->ModelResourceHandle->Attributes;
|
||||||
|
foreach (var (attribute, index) in attributes.Where(kvp => ShapeAttributeString.ValidateCustomAttributeString(kvp.Key.Value)))
|
||||||
|
{
|
||||||
|
if (ShapeAttributeString.TryRead(attribute.Value, out var attributeString))
|
||||||
|
{
|
||||||
|
// Mask out custom attributes if they are disabled. Attributes are enabled by default.
|
||||||
|
if (attributeCache.ShouldBeDisabled(attributeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
|
||||||
|
_model->EnabledAttributeIndexMask &= ~(1u << index);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"Trying to read a attribute string that is too long: {attribute}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CheckShapes(ShpCache shapeCache)
|
||||||
|
{
|
||||||
|
_temporaryShapes[_slotIndex].Clear();
|
||||||
|
ref var shapes = ref _model->ModelResourceHandle->Shapes;
|
||||||
|
foreach (var (shape, index) in shapes.Where(kvp => ShapeAttributeString.ValidateCustomShapeString(kvp.Key.Value)))
|
||||||
|
{
|
||||||
|
if (ShapeAttributeString.TryRead(shape.Value, out var shapeString))
|
||||||
|
{
|
||||||
|
_temporaryShapes[_slotIndex].TryAdd(shapeString, index);
|
||||||
|
// Add custom shapes if they are enabled. Shapes are disabled by default.
|
||||||
|
if (shapeCache.ShouldBeEnabled(shapeString, _modelIndex, _ids[_modelIndex.ToIndex()], _genderRace))
|
||||||
|
_model->EnabledShapeKeyIndexMask |= 1u << index;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateDefaultMasks(Model human, ShpCache cache)
|
||||||
|
{
|
||||||
|
var genderRace = (GenderRace)human.AsHuman->RaceSexId;
|
||||||
|
foreach (var (shape, topIndex) in _temporaryShapes[1])
|
||||||
|
{
|
||||||
|
if (shape.IsWrist()
|
||||||
|
&& _temporaryShapes[2].TryGetValue(shape, out var handIndex)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Hands, _ids[2], genderRace)
|
||||||
|
&& human.AsHuman->Models[1] is not null
|
||||||
|
&& human.AsHuman->Models[2] is not null)
|
||||||
|
{
|
||||||
|
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
|
||||||
|
human.AsHuman->Models[2]->EnabledShapeKeyIndexMask |= 1u << handIndex;
|
||||||
|
CheckCondition(cache.State(ShapeConnectorCondition.Wrists), genderRace, HumanSlot.Body, HumanSlot.Hands, 1, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shape.IsWaist()
|
||||||
|
&& _temporaryShapes[3].TryGetValue(shape, out var legIndex)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Body, _ids[1], genderRace)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
|
||||||
|
&& human.AsHuman->Models[1] is not null
|
||||||
|
&& human.AsHuman->Models[3] is not null)
|
||||||
|
{
|
||||||
|
human.AsHuman->Models[1]->EnabledShapeKeyIndexMask |= 1u << topIndex;
|
||||||
|
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << legIndex;
|
||||||
|
CheckCondition(cache.State(ShapeConnectorCondition.Waist), genderRace, HumanSlot.Body, HumanSlot.Legs, 1, 3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (shape, bottomIndex) in _temporaryShapes[3])
|
||||||
|
{
|
||||||
|
if (shape.IsAnkle()
|
||||||
|
&& _temporaryShapes[4].TryGetValue(shape, out var footIndex)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Legs, _ids[3], genderRace)
|
||||||
|
&& !cache.ShouldBeDisabled(shape, HumanSlot.Feet, _ids[4], genderRace)
|
||||||
|
&& human.AsHuman->Models[3] is not null
|
||||||
|
&& human.AsHuman->Models[4] is not null)
|
||||||
|
{
|
||||||
|
human.AsHuman->Models[3]->EnabledShapeKeyIndexMask |= 1u << bottomIndex;
|
||||||
|
human.AsHuman->Models[4]->EnabledShapeKeyIndexMask |= 1u << footIndex;
|
||||||
|
CheckCondition(cache.State(ShapeConnectorCondition.Ankles), genderRace, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void CheckCondition(IReadOnlyDictionary<ShapeAttributeString, ShapeAttributeHashSet> dict, GenderRace genderRace, HumanSlot slot1,
|
||||||
|
HumanSlot slot2, int idx1, int idx2)
|
||||||
|
{
|
||||||
|
foreach (var (shape, set) in dict)
|
||||||
|
{
|
||||||
|
if (set.CheckEntry(slot1, _ids[idx1], genderRace) is true && _temporaryShapes[idx1].TryGetValue(shape, out var index1))
|
||||||
|
human.AsHuman->Models[idx1]->EnabledShapeKeyIndexMask |= 1u << index1;
|
||||||
|
if (set.CheckEntry(slot2, _ids[idx2], genderRace) is true && _temporaryShapes[idx2].TryGetValue(shape, out var index2))
|
||||||
|
human.AsHuman->Models[idx2]->EnabledShapeKeyIndexMask |= 1u << index2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,11 @@ using Penumbra.String.Functions;
|
||||||
namespace Penumbra.Meta;
|
namespace Penumbra.Meta;
|
||||||
|
|
||||||
[JsonConverter(typeof(Converter))]
|
[JsonConverter(typeof(Converter))]
|
||||||
public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
public struct ShapeAttributeString : IEquatable<ShapeAttributeString>, IComparable<ShapeAttributeString>
|
||||||
{
|
{
|
||||||
public const int MaxLength = 30;
|
public const int MaxLength = 30;
|
||||||
|
|
||||||
public static readonly ShapeString Empty = new();
|
public static readonly ShapeAttributeString Empty = new();
|
||||||
|
|
||||||
private FixedString32 _buffer;
|
private FixedString32 _buffer;
|
||||||
|
|
||||||
|
|
@ -37,6 +37,72 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static unsafe bool ValidateCustomShapeString(byte* shape)
|
||||||
|
{
|
||||||
|
// "shpx_*"
|
||||||
|
if (shape is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (*shape++ is not (byte)'s'
|
||||||
|
|| *shape++ is not (byte)'h'
|
||||||
|
|| *shape++ is not (byte)'p'
|
||||||
|
|| *shape++ is not (byte)'x'
|
||||||
|
|| *shape++ is not (byte)'_'
|
||||||
|
|| *shape is 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateCustomShapeString()
|
||||||
|
{
|
||||||
|
// "shpx_*"
|
||||||
|
if (Length < 6)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_buffer[0] is not (byte)'s'
|
||||||
|
|| _buffer[1] is not (byte)'h'
|
||||||
|
|| _buffer[2] is not (byte)'p'
|
||||||
|
|| _buffer[3] is not (byte)'x'
|
||||||
|
|| _buffer[4] is not (byte)'_')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static unsafe bool ValidateCustomAttributeString(byte* shape)
|
||||||
|
{
|
||||||
|
// "atrx_*"
|
||||||
|
if (shape is null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (*shape++ is not (byte)'a'
|
||||||
|
|| *shape++ is not (byte)'t'
|
||||||
|
|| *shape++ is not (byte)'r'
|
||||||
|
|| *shape++ is not (byte)'x'
|
||||||
|
|| *shape++ is not (byte)'_'
|
||||||
|
|| *shape is 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ValidateCustomAttributeString()
|
||||||
|
{
|
||||||
|
// "atrx_*"
|
||||||
|
if (Length < 6)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (_buffer[0] is not (byte)'a'
|
||||||
|
|| _buffer[1] is not (byte)'t'
|
||||||
|
|| _buffer[2] is not (byte)'r'
|
||||||
|
|| _buffer[3] is not (byte)'x'
|
||||||
|
|| _buffer[4] is not (byte)'_')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
|
||||||
public bool IsAnkle()
|
public bool IsAnkle()
|
||||||
=> CheckCenter('a', 'n');
|
=> CheckCenter('a', 'n');
|
||||||
|
|
@ -53,28 +119,28 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
private bool CheckCenter(char first, char second)
|
private bool CheckCenter(char first, char second)
|
||||||
=> Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_';
|
=> Length > 8 && _buffer[5] == first && _buffer[6] == second && _buffer[7] is (byte)'_';
|
||||||
|
|
||||||
public bool Equals(ShapeString other)
|
public bool Equals(ShapeAttributeString other)
|
||||||
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
|
=> Length == other.Length && _buffer[..Length].SequenceEqual(other._buffer[..Length]);
|
||||||
|
|
||||||
public override bool Equals(object? obj)
|
public override bool Equals(object? obj)
|
||||||
=> obj is ShapeString other && Equals(other);
|
=> obj is ShapeAttributeString other && Equals(other);
|
||||||
|
|
||||||
public override int GetHashCode()
|
public override int GetHashCode()
|
||||||
=> (int)Crc32.Get(_buffer[..Length]);
|
=> (int)Crc32.Get(_buffer[..Length]);
|
||||||
|
|
||||||
public static bool operator ==(ShapeString left, ShapeString right)
|
public static bool operator ==(ShapeAttributeString left, ShapeAttributeString right)
|
||||||
=> left.Equals(right);
|
=> left.Equals(right);
|
||||||
|
|
||||||
public static bool operator !=(ShapeString left, ShapeString right)
|
public static bool operator !=(ShapeAttributeString left, ShapeAttributeString right)
|
||||||
=> !left.Equals(right);
|
=> !left.Equals(right);
|
||||||
|
|
||||||
public static unsafe bool TryRead(byte* pointer, out ShapeString ret)
|
public static unsafe bool TryRead(byte* pointer, out ShapeAttributeString ret)
|
||||||
{
|
{
|
||||||
var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer);
|
var span = MemoryMarshal.CreateReadOnlySpanFromNullTerminated(pointer);
|
||||||
return TryRead(span, out ret);
|
return TryRead(span, out ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public unsafe int CompareTo(ShapeString other)
|
public unsafe int CompareTo(ShapeAttributeString other)
|
||||||
{
|
{
|
||||||
fixed (void* lhs = &this)
|
fixed (void* lhs = &this)
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +148,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeString ret)
|
public static bool TryRead(ReadOnlySpan<byte> utf8, out ShapeAttributeString ret)
|
||||||
{
|
{
|
||||||
if (utf8.Length is 0 or > MaxLength)
|
if (utf8.Length is 0 or > MaxLength)
|
||||||
{
|
{
|
||||||
|
|
@ -97,7 +163,7 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeString ret)
|
public static bool TryRead(ReadOnlySpan<char> utf16, out ShapeAttributeString ret)
|
||||||
{
|
{
|
||||||
ret = Empty;
|
ret = Empty;
|
||||||
if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written))
|
if (!Encoding.UTF8.TryGetBytes(utf16, ret._buffer[..MaxLength], out var written))
|
||||||
|
|
@ -116,19 +182,20 @@ public struct ShapeString : IEquatable<ShapeString>, IComparable<ShapeString>
|
||||||
_buffer[31] = length;
|
_buffer[31] = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class Converter : JsonConverter<ShapeString>
|
private sealed class Converter : JsonConverter<ShapeAttributeString>
|
||||||
{
|
{
|
||||||
public override void WriteJson(JsonWriter writer, ShapeString value, JsonSerializer serializer)
|
public override void WriteJson(JsonWriter writer, ShapeAttributeString value, JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
writer.WriteValue(value.ToString());
|
writer.WriteValue(value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ShapeString ReadJson(JsonReader reader, Type objectType, ShapeString existingValue, bool hasExistingValue,
|
public override ShapeAttributeString ReadJson(JsonReader reader, Type objectType, ShapeAttributeString existingValue,
|
||||||
|
bool hasExistingValue,
|
||||||
JsonSerializer serializer)
|
JsonSerializer serializer)
|
||||||
{
|
{
|
||||||
var value = serializer.Deserialize<string>(reader);
|
var value = serializer.Deserialize<string>(reader);
|
||||||
if (!TryRead(value, out existingValue))
|
if (!TryRead(value, out existingValue))
|
||||||
throw new JsonReaderException($"Could not parse {value} into ShapeString.");
|
throw new JsonReaderException($"Could not parse {value} into ShapeAttributeString.");
|
||||||
|
|
||||||
return existingValue;
|
return existingValue;
|
||||||
}
|
}
|
||||||
|
|
@ -1,144 +0,0 @@
|
||||||
using System.Reflection.Metadata.Ecma335;
|
|
||||||
using OtterGui.Services;
|
|
||||||
using Penumbra.Collections;
|
|
||||||
using Penumbra.Collections.Cache;
|
|
||||||
using Penumbra.GameData.Enums;
|
|
||||||
using Penumbra.GameData.Interop;
|
|
||||||
using Penumbra.GameData.Structs;
|
|
||||||
using Penumbra.Interop.Hooks.PostProcessing;
|
|
||||||
using Penumbra.Meta.Manipulations;
|
|
||||||
|
|
||||||
namespace Penumbra.Meta;
|
|
||||||
|
|
||||||
public class ShapeManager : IRequiredService, IDisposable
|
|
||||||
{
|
|
||||||
public const int NumSlots = 14;
|
|
||||||
public const int ModelSlotSize = 18;
|
|
||||||
private readonly AttributeHook _attributeHook;
|
|
||||||
|
|
||||||
public static ReadOnlySpan<HumanSlot> UsedModels
|
|
||||||
=>
|
|
||||||
[
|
|
||||||
HumanSlot.Head, HumanSlot.Body, HumanSlot.Hands, HumanSlot.Legs, HumanSlot.Feet, HumanSlot.Ears, HumanSlot.Neck, HumanSlot.Wrists,
|
|
||||||
HumanSlot.RFinger, HumanSlot.LFinger, HumanSlot.Glasses, HumanSlot.Hair, HumanSlot.Face, HumanSlot.Ear,
|
|
||||||
];
|
|
||||||
|
|
||||||
public ShapeManager(AttributeHook attributeHook)
|
|
||||||
{
|
|
||||||
_attributeHook = attributeHook;
|
|
||||||
_attributeHook.Subscribe(OnAttributeComputed, AttributeHook.Priority.ShapeManager);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly Dictionary<ShapeString, short>[] _temporaryIndices =
|
|
||||||
Enumerable.Range(0, NumSlots).Select(_ => new Dictionary<ShapeString, short>()).ToArray();
|
|
||||||
|
|
||||||
private readonly uint[] _temporaryMasks = new uint[NumSlots];
|
|
||||||
private readonly uint[] _temporaryValues = new uint[NumSlots];
|
|
||||||
private readonly PrimaryId[] _ids = new PrimaryId[ModelSlotSize];
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
=> _attributeHook.Unsubscribe(OnAttributeComputed);
|
|
||||||
|
|
||||||
private unsafe void OnAttributeComputed(Actor actor, Model model, ModCollection collection)
|
|
||||||
{
|
|
||||||
if (!collection.HasCache)
|
|
||||||
return;
|
|
||||||
|
|
||||||
ComputeCache(model, collection.MetaCache!.Shp);
|
|
||||||
for (var i = 0; i < NumSlots; ++i)
|
|
||||||
{
|
|
||||||
if (_temporaryMasks[i] is 0)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var modelIndex = UsedModels[i];
|
|
||||||
var currentMask = model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask;
|
|
||||||
var newMask = (currentMask & ~_temporaryMasks[i]) | _temporaryValues[i];
|
|
||||||
Penumbra.Log.Excessive($"Changed Model Mask from {currentMask:X} to {newMask:X}.");
|
|
||||||
model.AsHuman->Models[modelIndex.ToIndex()]->EnabledShapeKeyIndexMask = newMask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unsafe void ComputeCache(Model human, ShpCache cache)
|
|
||||||
{
|
|
||||||
for (var i = 0; i < NumSlots; ++i)
|
|
||||||
{
|
|
||||||
_temporaryMasks[i] = 0;
|
|
||||||
_temporaryValues[i] = 0;
|
|
||||||
_temporaryIndices[i].Clear();
|
|
||||||
|
|
||||||
var modelIndex = UsedModels[i];
|
|
||||||
var model = human.AsHuman->Models[modelIndex.ToIndex()];
|
|
||||||
if (model is null || model->ModelResourceHandle is null)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
_ids[(int)modelIndex] = human.GetArmorChanged(modelIndex).Set;
|
|
||||||
|
|
||||||
ref var shapes = ref model->ModelResourceHandle->Shapes;
|
|
||||||
foreach (var (shape, index) in shapes.Where(kvp => ShpIdentifier.ValidateCustomShapeString(kvp.Key.Value)))
|
|
||||||
{
|
|
||||||
if (ShapeString.TryRead(shape.Value, out var shapeString))
|
|
||||||
{
|
|
||||||
_temporaryIndices[i].TryAdd(shapeString, index);
|
|
||||||
_temporaryMasks[i] |= (ushort)(1 << index);
|
|
||||||
if (cache.State.Count > 0
|
|
||||||
&& cache.ShouldBeEnabled(shapeString, modelIndex, _ids[(int)modelIndex]))
|
|
||||||
_temporaryValues[i] |= (ushort)(1 << index);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Penumbra.Log.Warning($"Trying to read a shape string that is too long: {shape}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UpdateDefaultMasks(cache);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateDefaultMasks(ShpCache cache)
|
|
||||||
{
|
|
||||||
foreach (var (shape, topIndex) in _temporaryIndices[1])
|
|
||||||
{
|
|
||||||
if (shape.IsWrist() && _temporaryIndices[2].TryGetValue(shape, out var handIndex))
|
|
||||||
{
|
|
||||||
_temporaryValues[1] |= 1u << topIndex;
|
|
||||||
_temporaryValues[2] |= 1u << handIndex;
|
|
||||||
CheckCondition(shape, HumanSlot.Body, HumanSlot.Hands, 1, 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shape.IsWaist() && _temporaryIndices[3].TryGetValue(shape, out var legIndex))
|
|
||||||
{
|
|
||||||
_temporaryValues[1] |= 1u << topIndex;
|
|
||||||
_temporaryValues[3] |= 1u << legIndex;
|
|
||||||
CheckCondition(shape, HumanSlot.Body, HumanSlot.Legs, 1, 3);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var (shape, bottomIndex) in _temporaryIndices[3])
|
|
||||||
{
|
|
||||||
if (shape.IsAnkle() && _temporaryIndices[4].TryGetValue(shape, out var footIndex))
|
|
||||||
{
|
|
||||||
_temporaryValues[3] |= 1u << bottomIndex;
|
|
||||||
_temporaryValues[4] |= 1u << footIndex;
|
|
||||||
CheckCondition(shape, HumanSlot.Legs, HumanSlot.Feet, 3, 4);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
|
|
||||||
void CheckCondition(in ShapeString shape, HumanSlot slot1, HumanSlot slot2, int idx1, int idx2)
|
|
||||||
{
|
|
||||||
if (!cache.CheckConditionState(shape, out var dict))
|
|
||||||
return;
|
|
||||||
|
|
||||||
foreach (var (subShape, set) in dict)
|
|
||||||
{
|
|
||||||
if (set.Contains(slot1, _ids[idx1]))
|
|
||||||
if (_temporaryIndices[idx1].TryGetValue(subShape, out var subIndex))
|
|
||||||
_temporaryValues[idx1] |= 1u << subIndex;
|
|
||||||
if (set.Contains(slot2, _ids[idx2]))
|
|
||||||
if (_temporaryIndices[idx2].TryGetValue(subShape, out var subIndex))
|
|
||||||
_temporaryValues[idx2] |= 1u << subIndex;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,6 +6,7 @@ using OtterGui.Extensions;
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Api.Enums;
|
using Penumbra.Api.Enums;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
using Penumbra.Mods.Groups;
|
||||||
using Penumbra.Mods.Manager;
|
using Penumbra.Mods.Manager;
|
||||||
using Penumbra.Mods.Manager.OptionEditor;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Mods.SubMods;
|
using Penumbra.Mods.SubMods;
|
||||||
|
|
@ -44,13 +45,13 @@ public class ModMerger : IDisposable, IService
|
||||||
public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates,
|
public ModMerger(ModManager mods, ModGroupEditor editor, ModSelection selection, DuplicateManager duplicates,
|
||||||
CommunicatorService communicator, ModCreator creator, Configuration config)
|
CommunicatorService communicator, ModCreator creator, Configuration config)
|
||||||
{
|
{
|
||||||
_editor = editor;
|
_editor = editor;
|
||||||
_selection = selection;
|
_selection = selection;
|
||||||
_duplicates = duplicates;
|
_duplicates = duplicates;
|
||||||
_communicator = communicator;
|
_communicator = communicator;
|
||||||
_creator = creator;
|
_creator = creator;
|
||||||
_config = config;
|
_config = config;
|
||||||
_mods = mods;
|
_mods = mods;
|
||||||
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
|
_selection.Subscribe(OnSelectionChange, ModSelection.Priority.ModMerger);
|
||||||
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
|
_communicator.ModPathChanged.Subscribe(OnModPathChange, ModPathChanged.Priority.ModMerger);
|
||||||
}
|
}
|
||||||
|
|
@ -99,26 +100,117 @@ public class ModMerger : IDisposable, IService
|
||||||
|
|
||||||
foreach (var originalGroup in MergeFromMod!.Groups)
|
foreach (var originalGroup in MergeFromMod!.Groups)
|
||||||
{
|
{
|
||||||
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name);
|
switch (originalGroup.Type)
|
||||||
if (groupCreated)
|
|
||||||
_createdGroups.Add(groupIdx);
|
|
||||||
if (group == null)
|
|
||||||
throw new Exception(
|
|
||||||
$"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}.");
|
|
||||||
|
|
||||||
foreach (var originalOption in originalGroup.DataContainers)
|
|
||||||
{
|
{
|
||||||
var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.GetName());
|
case GroupType.Single:
|
||||||
if (optionCreated)
|
case GroupType.Multi:
|
||||||
{
|
{
|
||||||
_createdOptions.Add(option!);
|
var (group, groupIdx, groupCreated) = _editor.FindOrAddModGroup(MergeToMod!, originalGroup.Type, originalGroup.Name);
|
||||||
// #TODO DataContainer <> Option.
|
if (group is null)
|
||||||
MergeIntoOption([originalOption], (IModDataContainer)option!, false);
|
throw new Exception(
|
||||||
|
$"The merged group {originalGroup.Name} already existed, but had a different type than the original group of type {originalGroup.Type}.");
|
||||||
|
|
||||||
|
if (groupCreated)
|
||||||
|
{
|
||||||
|
_createdGroups.Add(groupIdx);
|
||||||
|
group.Description = originalGroup.Description;
|
||||||
|
group.Image = originalGroup.Image;
|
||||||
|
group.DefaultSettings = originalGroup.DefaultSettings;
|
||||||
|
group.Page = originalGroup.Page;
|
||||||
|
group.Priority = originalGroup.Priority;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var originalOption in originalGroup.Options)
|
||||||
|
{
|
||||||
|
var (option, _, optionCreated) = _editor.FindOrAddOption(group, originalOption.Name);
|
||||||
|
if (optionCreated)
|
||||||
|
{
|
||||||
|
_createdOptions.Add(option!);
|
||||||
|
MergeIntoOption([(IModDataContainer)originalOption], (IModDataContainer)option!, false);
|
||||||
|
option!.Description = originalOption.Description;
|
||||||
|
if (option is MultiSubMod multiOption)
|
||||||
|
multiOption.Priority = ((MultiSubMod)originalOption).Priority;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new Exception(
|
||||||
|
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
case GroupType.Imc when originalGroup is ImcModGroup imc:
|
||||||
{
|
{
|
||||||
throw new Exception(
|
var group = _editor.ImcEditor.AddModGroup(MergeToMod!, imc.Name, imc.Identifier, imc.DefaultEntry);
|
||||||
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: The option {option!.FullName} already existed.");
|
if (group is null)
|
||||||
|
throw new Exception(
|
||||||
|
$"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged.");
|
||||||
|
|
||||||
|
group.AllVariants = imc.AllVariants;
|
||||||
|
group.OnlyAttributes = imc.OnlyAttributes;
|
||||||
|
group.Description = imc.Description;
|
||||||
|
group.Image = imc.Image;
|
||||||
|
group.DefaultSettings = imc.DefaultSettings;
|
||||||
|
group.Page = imc.Page;
|
||||||
|
group.Priority = imc.Priority;
|
||||||
|
foreach (var originalOption in imc.OptionData)
|
||||||
|
{
|
||||||
|
if (originalOption.IsDisableSubMod)
|
||||||
|
{
|
||||||
|
_editor.ImcEditor.ChangeCanBeDisabled(group, true);
|
||||||
|
var disable = group.OptionData.First(s => s.IsDisableSubMod);
|
||||||
|
disable.Description = originalOption.Description;
|
||||||
|
disable.Name = originalOption.Name;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var newOption = _editor.ImcEditor.AddOption(group, originalOption.Name);
|
||||||
|
if (newOption is null)
|
||||||
|
throw new Exception(
|
||||||
|
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating IMC option {originalOption.FullName}.");
|
||||||
|
|
||||||
|
newOption.Description = originalOption.Description;
|
||||||
|
newOption.AttributeMask = originalOption.AttributeMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case GroupType.Combining when originalGroup is CombiningModGroup combining:
|
||||||
|
{
|
||||||
|
var group = _editor.CombiningEditor.AddModGroup(MergeToMod!, combining.Name);
|
||||||
|
if (group is null)
|
||||||
|
throw new Exception(
|
||||||
|
$"The merged group {originalGroup.Name} already existed, but groups of type {originalGroup.Type} can not be merged.");
|
||||||
|
|
||||||
|
group.Description = combining.Description;
|
||||||
|
group.Image = combining.Image;
|
||||||
|
group.DefaultSettings = combining.DefaultSettings;
|
||||||
|
group.Page = combining.Page;
|
||||||
|
group.Priority = combining.Priority;
|
||||||
|
foreach (var originalOption in combining.OptionData)
|
||||||
|
{
|
||||||
|
var option = _editor.CombiningEditor.AddOption(group, originalOption.Name);
|
||||||
|
if (option is null)
|
||||||
|
throw new Exception(
|
||||||
|
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error when creating combining option {originalOption.FullName}.");
|
||||||
|
|
||||||
|
option.Description = originalOption.Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.Data.Count != combining.Data.Count)
|
||||||
|
throw new Exception(
|
||||||
|
$"Could not merge {MergeFromMod!.Name} into {MergeToMod!.Name}: Unknown error caused data container counts in combining group {originalGroup.Name} to differ.");
|
||||||
|
|
||||||
|
foreach (var (originalContainer, container) in combining.Data.Zip(group.Data))
|
||||||
|
{
|
||||||
|
container.Name = originalContainer.Name;
|
||||||
|
MergeIntoOption([originalContainer], container, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -151,7 +243,6 @@ public class ModMerger : IDisposable, IService
|
||||||
if (!dir.Exists)
|
if (!dir.Exists)
|
||||||
_createdDirectories.Add(dir.FullName);
|
_createdDirectories.Add(dir.FullName);
|
||||||
CopyFiles(dir);
|
CopyFiles(dir);
|
||||||
// #TODO DataContainer <> Option.
|
|
||||||
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true);
|
MergeIntoOption(MergeFromMod!.AllDataContainers.Reverse(), (IModDataContainer)option!, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +372,6 @@ public class ModMerger : IDisposable, IService
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO DataContainer <> Option.
|
|
||||||
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
|
var (group, _, _) = _editor.FindOrAddModGroup(result, originalGroup.Type, originalGroup.Name);
|
||||||
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
|
var (option, _, _) = _editor.FindOrAddOption(group!, originalOption.GetName());
|
||||||
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
|
var folder = Path.Combine(dir.FullName, group!.Name, option!.Name);
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,8 @@ public class ModMetaEditor(
|
||||||
OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est));
|
OtherData[MetaManipulationType.Est].Add(name, option.Manipulations.GetCount(MetaManipulationType.Est));
|
||||||
OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp));
|
OtherData[MetaManipulationType.Rsp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Rsp));
|
||||||
OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch));
|
OtherData[MetaManipulationType.Atch].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atch));
|
||||||
|
OtherData[MetaManipulationType.Shp].Add(name, option.Manipulations.GetCount(MetaManipulationType.Shp));
|
||||||
|
OtherData[MetaManipulationType.Atr].Add(name, option.Manipulations.GetCount(MetaManipulationType.Atr));
|
||||||
OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp));
|
OtherData[MetaManipulationType.GlobalEqp].Add(name, option.Manipulations.GetCount(MetaManipulationType.GlobalEqp));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport);
|
var groupDir = ModCreator.NewOptionDirectory(mod.ModPath, container.Group.Name, config.ReplaceNonAsciiOnImport);
|
||||||
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetName(), config.ReplaceNonAsciiOnImport);
|
var optionDir = ModCreator.NewOptionDirectory(groupDir, container.GetDirectoryName(), config.ReplaceNonAsciiOnImport);
|
||||||
containers[container] = optionDir.FullName;
|
containers[container] = optionDir.FullName;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +286,7 @@ public class ModNormalizer(ModManager modManager, Configuration config, SaveServ
|
||||||
|
|
||||||
void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
|
void HandleSubMod(DirectoryInfo groupDir, IModDataContainer option, Dictionary<Utf8GamePath, FullPath> newDict)
|
||||||
{
|
{
|
||||||
var name = option.GetName();
|
var name = option.GetDirectoryName();
|
||||||
var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true);
|
var optionDir = ModCreator.CreateModFolder(groupDir, name, config.ReplaceNonAsciiOnImport, true);
|
||||||
|
|
||||||
newDict.Clear();
|
newDict.Clear();
|
||||||
|
|
|
||||||
95
Penumbra/Mods/FeatureChecker.cs
Normal file
95
Penumbra/Mods/FeatureChecker.cs
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
using System.Collections.Frozen;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Bindings.ImGui;
|
||||||
|
using OtterGui.Text;
|
||||||
|
using Penumbra.Mods.Manager;
|
||||||
|
using Penumbra.UI.Classes;
|
||||||
|
using Notification = OtterGui.Classes.Notification;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
public static class FeatureChecker
|
||||||
|
{
|
||||||
|
/// <summary> Manually setup supported features to exclude None and Invalid and not make something supported too early. </summary>
|
||||||
|
private static readonly FrozenDictionary<string, FeatureFlags> SupportedFlags = new[]
|
||||||
|
{
|
||||||
|
FeatureFlags.Atch,
|
||||||
|
FeatureFlags.Shp,
|
||||||
|
FeatureFlags.Atr,
|
||||||
|
}.ToFrozenDictionary(f => f.ToString(), f => f);
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<string> SupportedFeatures
|
||||||
|
=> SupportedFlags.Keys;
|
||||||
|
|
||||||
|
public static FeatureFlags ParseFlags(string modDirectory, string modName, IEnumerable<string> features)
|
||||||
|
{
|
||||||
|
var featureFlags = FeatureFlags.None;
|
||||||
|
HashSet<string> missingFeatures = [];
|
||||||
|
foreach (var feature in features)
|
||||||
|
{
|
||||||
|
if (SupportedFlags.TryGetValue(feature, out var featureFlag))
|
||||||
|
featureFlags |= featureFlag;
|
||||||
|
else
|
||||||
|
missingFeatures.Add(feature);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missingFeatures.Count > 0)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.AddMessage(new Notification(
|
||||||
|
$"Please update Penumbra to use the mod {modName}{(modDirectory != modName ? $" at {modDirectory}" : string.Empty)}!\n\nLoading failed because it requires the unsupported feature{(missingFeatures.Count > 1 ? $"s\n\n\t[{string.Join("], [", missingFeatures)}]." : $" [{missingFeatures.First()}].")}",
|
||||||
|
NotificationType.Warning));
|
||||||
|
return FeatureFlags.Invalid;
|
||||||
|
}
|
||||||
|
|
||||||
|
return featureFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Supported(string features)
|
||||||
|
=> SupportedFlags.ContainsKey(features);
|
||||||
|
|
||||||
|
public static void DrawFeatureFlagInput(ModDataEditor editor, Mod mod, float width)
|
||||||
|
{
|
||||||
|
const int numButtons = 5;
|
||||||
|
var innerSpacing = ImGui.GetStyle().ItemInnerSpacing;
|
||||||
|
var size = new Vector2((width - (numButtons - 1) * innerSpacing.X) / numButtons, 0);
|
||||||
|
var buttonColor = ImGui.GetColorU32(ImGuiCol.FrameBg);
|
||||||
|
var textColor = ImGui.GetColorU32(ImGuiCol.TextDisabled);
|
||||||
|
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, innerSpacing)
|
||||||
|
.Push(ImGuiStyleVar.FrameBorderSize, 0);
|
||||||
|
using (var color = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value())
|
||||||
|
.Push(ImGuiCol.Button, buttonColor)
|
||||||
|
.Push(ImGuiCol.Text, textColor))
|
||||||
|
{
|
||||||
|
foreach (var flag in SupportedFlags.Values)
|
||||||
|
{
|
||||||
|
if (mod.RequiredFeatures.HasFlag(flag))
|
||||||
|
{
|
||||||
|
style.Push(ImGuiStyleVar.FrameBorderSize, ImUtf8.GlobalScale);
|
||||||
|
color.Pop(2);
|
||||||
|
if (ImUtf8.Button($"{flag}", size))
|
||||||
|
editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures & ~flag);
|
||||||
|
color.Push(ImGuiCol.Button, buttonColor)
|
||||||
|
.Push(ImGuiCol.Text, textColor);
|
||||||
|
style.Pop();
|
||||||
|
}
|
||||||
|
else if (ImUtf8.Button($"{flag}", size))
|
||||||
|
{
|
||||||
|
editor.ChangeRequiredFeatures(mod, mod.RequiredFeatures | flag);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ImUtf8.ButtonEx("Compute"u8, "Compute the required features automatically from the used features."u8, size))
|
||||||
|
editor.ChangeRequiredFeatures(mod, mod.ComputeRequiredFeatures());
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
if (ImUtf8.ButtonEx("Clear"u8, "Clear all required features."u8, size))
|
||||||
|
editor.ChangeRequiredFeatures(mod, FeatureFlags.None);
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImUtf8.Text("Required Features"u8);
|
||||||
|
}
|
||||||
|
}
|
||||||
180
Penumbra/Mods/Groups/ComplexModGroup.cs
Normal file
180
Penumbra/Mods/Groups/ComplexModGroup.cs
Normal file
|
|
@ -0,0 +1,180 @@
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using OtterGui.Classes;
|
||||||
|
using OtterGui.Extensions;
|
||||||
|
using Penumbra.Api.Enums;
|
||||||
|
using Penumbra.GameData.Data;
|
||||||
|
using Penumbra.Meta.Manipulations;
|
||||||
|
using Penumbra.Mods.Settings;
|
||||||
|
using Penumbra.Mods.SubMods;
|
||||||
|
using Penumbra.String.Classes;
|
||||||
|
using Penumbra.UI.ModsTab.Groups;
|
||||||
|
using Penumbra.Util;
|
||||||
|
|
||||||
|
namespace Penumbra.Mods.Groups;
|
||||||
|
|
||||||
|
public sealed class ComplexModGroup(Mod mod) : IModGroup
|
||||||
|
{
|
||||||
|
public Mod Mod { get; } = mod;
|
||||||
|
public string Name { get; set; } = "Option";
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
public string Image { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public GroupType Type
|
||||||
|
=> GroupType.Complex;
|
||||||
|
|
||||||
|
public GroupDrawBehaviour Behaviour
|
||||||
|
=> GroupDrawBehaviour.Complex;
|
||||||
|
|
||||||
|
public ModPriority Priority { get; set; }
|
||||||
|
public int Page { get; set; }
|
||||||
|
public Setting DefaultSettings { get; set; }
|
||||||
|
|
||||||
|
public readonly List<ComplexSubMod> Options = [];
|
||||||
|
public readonly List<ComplexDataContainer> Containers = [];
|
||||||
|
|
||||||
|
|
||||||
|
public FullPath? FindBestMatch(Utf8GamePath gamePath)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public IModOption? AddOption(string name, string description = "")
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
IReadOnlyList<IModOption> IModGroup.Options
|
||||||
|
=> Options;
|
||||||
|
|
||||||
|
IReadOnlyList<IModDataContainer> IModGroup.DataContainers
|
||||||
|
=> Containers;
|
||||||
|
|
||||||
|
public bool IsOption
|
||||||
|
=> Options.Count > 0;
|
||||||
|
|
||||||
|
public int GetIndex()
|
||||||
|
=> ModGroup.GetIndex(this);
|
||||||
|
|
||||||
|
public IModGroupEditDrawer EditDrawer(ModGroupEditDrawer editDrawer)
|
||||||
|
=> throw new NotImplementedException();
|
||||||
|
|
||||||
|
public void AddData(Setting setting, Dictionary<Utf8GamePath, FullPath> redirections, MetaDictionary manipulations)
|
||||||
|
{
|
||||||
|
foreach (var container in Containers.Where(c => c.Association.IsEnabled(setting)))
|
||||||
|
SubMod.AddContainerTo(container, redirections, manipulations);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddChangedItems(ObjectIdentification identifier, IDictionary<string, IIdentifiedObjectData> changedItems)
|
||||||
|
{
|
||||||
|
foreach (var container in Containers)
|
||||||
|
identifier.AddChangedItems(container, changedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Setting FixSetting(Setting setting)
|
||||||
|
=> new(setting.Value & ((1ul << Options.Count) - 1));
|
||||||
|
|
||||||
|
public void WriteJson(JsonTextWriter jWriter, JsonSerializer serializer, DirectoryInfo? basePath = null)
|
||||||
|
{
|
||||||
|
ModSaveGroup.WriteJsonBase(jWriter, this);
|
||||||
|
jWriter.WritePropertyName("Options");
|
||||||
|
jWriter.WriteStartArray();
|
||||||
|
foreach (var option in Options)
|
||||||
|
{
|
||||||
|
jWriter.WriteStartObject();
|
||||||
|
SubMod.WriteModOption(jWriter, option);
|
||||||
|
if (!option.Conditions.IsZero)
|
||||||
|
{
|
||||||
|
jWriter.WritePropertyName("ConditionMask");
|
||||||
|
jWriter.WriteValue(option.Conditions.Mask.Value);
|
||||||
|
jWriter.WritePropertyName("ConditionValue");
|
||||||
|
jWriter.WriteValue(option.Conditions.Value.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.Indentation > 0)
|
||||||
|
{
|
||||||
|
jWriter.WritePropertyName("Indentation");
|
||||||
|
jWriter.WriteValue(option.Indentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (option.SubGroupLabel.Length > 0)
|
||||||
|
{
|
||||||
|
jWriter.WritePropertyName("SubGroup");
|
||||||
|
jWriter.WriteValue(option.SubGroupLabel);
|
||||||
|
}
|
||||||
|
|
||||||
|
jWriter.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
jWriter.WriteEndArray();
|
||||||
|
|
||||||
|
jWriter.WritePropertyName("Containers");
|
||||||
|
jWriter.WriteStartArray();
|
||||||
|
foreach (var container in Containers)
|
||||||
|
{
|
||||||
|
jWriter.WriteStartObject();
|
||||||
|
if (container.Name.Length > 0)
|
||||||
|
{
|
||||||
|
jWriter.WritePropertyName("Name");
|
||||||
|
jWriter.WriteValue(container.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!container.Association.IsZero)
|
||||||
|
{
|
||||||
|
jWriter.WritePropertyName("AssociationMask");
|
||||||
|
jWriter.WriteValue(container.Association.Mask.Value);
|
||||||
|
|
||||||
|
jWriter.WritePropertyName("AssociationValue");
|
||||||
|
jWriter.WriteValue(container.Association.Value.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
SubMod.WriteModContainer(jWriter, serializer, container, basePath ?? Mod.ModPath);
|
||||||
|
jWriter.WriteEndObject();
|
||||||
|
}
|
||||||
|
|
||||||
|
jWriter.WriteEndArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public (int Redirections, int Swaps, int Manips) GetCounts()
|
||||||
|
=> ModGroup.GetCountsBase(this);
|
||||||
|
|
||||||
|
public static ComplexModGroup? Load(Mod mod, JObject json)
|
||||||
|
{
|
||||||
|
var ret = new ComplexModGroup(mod);
|
||||||
|
if (!ModSaveGroup.ReadJsonBase(json, ret))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var options = json["Options"];
|
||||||
|
if (options != null)
|
||||||
|
foreach (var child in options.Children())
|
||||||
|
{
|
||||||
|
if (ret.Options.Count == IModGroup.MaxComplexOptions)
|
||||||
|
{
|
||||||
|
Penumbra.Messager.NotificationMessage(
|
||||||
|
$"Complex Group {ret.Name} in {mod.Name} has more than {IModGroup.MaxComplexOptions} options, ignoring excessive options.",
|
||||||
|
NotificationType.Warning);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var subMod = new ComplexSubMod(ret, child);
|
||||||
|
ret.Options.Add(subMod);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fix up conditions: No condition on itself.
|
||||||
|
foreach (var (option, index) in ret.Options.WithIndex())
|
||||||
|
{
|
||||||
|
option.Conditions = option.Conditions.Limit(ret.Options.Count);
|
||||||
|
option.Conditions = new MaskedSetting(option.Conditions.Mask.SetBit(index, false), option.Conditions.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var containers = json["Containers"];
|
||||||
|
if (containers != null)
|
||||||
|
foreach (var child in containers.Children())
|
||||||
|
{
|
||||||
|
var container = new ComplexDataContainer(ret, child);
|
||||||
|
container.Association = container.Association.Limit(ret.Options.Count);
|
||||||
|
ret.Containers.Add(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
ret.DefaultSettings = ret.FixSetting(ret.DefaultSettings);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,11 +18,13 @@ public enum GroupDrawBehaviour
|
||||||
{
|
{
|
||||||
SingleSelection,
|
SingleSelection,
|
||||||
MultiSelection,
|
MultiSelection,
|
||||||
|
Complex,
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface IModGroup
|
public interface IModGroup
|
||||||
{
|
{
|
||||||
public const int MaxMultiOptions = 32;
|
public const int MaxMultiOptions = 32;
|
||||||
|
public const int MaxComplexOptions = MaxMultiOptions;
|
||||||
public const int MaxCombiningOptions = 8;
|
public const int MaxCombiningOptions = 8;
|
||||||
|
|
||||||
public Mod Mod { get; }
|
public Mod Mod { get; }
|
||||||
|
|
|
||||||
|
|
@ -234,9 +234,56 @@ public static class EquipmentSwap
|
||||||
mdl.ChildSwaps.Add(mtrl);
|
mdl.ChildSwaps.Add(mtrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FixAttributes(mdl, slotFrom, slotTo);
|
||||||
|
|
||||||
return mdl;
|
return mdl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void FixAttributes(FileSwap swap, EquipSlot slotFrom, EquipSlot slotTo)
|
||||||
|
{
|
||||||
|
if (slotFrom == slotTo)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var needle = slotTo switch
|
||||||
|
{
|
||||||
|
EquipSlot.Head => "atr_mv_",
|
||||||
|
EquipSlot.Ears => "atr_ev_",
|
||||||
|
EquipSlot.Neck => "atr_nv_",
|
||||||
|
EquipSlot.Wrists => "atr_wv_",
|
||||||
|
EquipSlot.RFinger or EquipSlot.LFinger => "atr_rv_",
|
||||||
|
_ => string.Empty,
|
||||||
|
};
|
||||||
|
|
||||||
|
var replacement = slotFrom switch
|
||||||
|
{
|
||||||
|
EquipSlot.Head => 'm',
|
||||||
|
EquipSlot.Ears => 'e',
|
||||||
|
EquipSlot.Neck => 'n',
|
||||||
|
EquipSlot.Wrists => 'w',
|
||||||
|
EquipSlot.RFinger or EquipSlot.LFinger => 'r',
|
||||||
|
_ => 'm',
|
||||||
|
};
|
||||||
|
|
||||||
|
var attributes = swap.AsMdl()!.Attributes;
|
||||||
|
for (var i = 0; i < attributes.Length; ++i)
|
||||||
|
{
|
||||||
|
if (FixAttribute(ref attributes[i], needle, replacement))
|
||||||
|
swap.DataWasChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe bool FixAttribute(ref string attribute, string from, char to)
|
||||||
|
{
|
||||||
|
if (!attribute.StartsWith(from) || attribute.Length != from.Length + 1 || attribute[^1] is < 'a' or > 'j')
|
||||||
|
return false;
|
||||||
|
|
||||||
|
Span<char> stack = stackalloc char[attribute.Length];
|
||||||
|
attribute.CopyTo(stack);
|
||||||
|
stack[4] = to;
|
||||||
|
attribute = new string(stack);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant)
|
private static void LookupItem(EquipItem i, out EquipSlot slot, out PrimaryId modelId, out Variant variant)
|
||||||
{
|
{
|
||||||
slot = i.Type.ToSlot();
|
slot = i.Type.ToSlot();
|
||||||
|
|
@ -399,7 +446,7 @@ public static class EquipmentSwap
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo);
|
var folderTo = GamePaths.Mtrl.GearFolder(slotTo, idTo, variantTo);
|
||||||
var pathTo = $"{folderTo}{fileName}";
|
var pathTo = $"{folderTo}{fileName}";
|
||||||
|
|
||||||
var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo);
|
var folderFrom = GamePaths.Mtrl.GearFolder(slotFrom, idFrom, variantTo);
|
||||||
var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);
|
var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ public enum ModDataChangeType : ushort
|
||||||
Image = 0x1000,
|
Image = 0x1000,
|
||||||
DefaultChangedItems = 0x2000,
|
DefaultChangedItems = 0x2000,
|
||||||
PreferredChangedItems = 0x4000,
|
PreferredChangedItems = 0x4000,
|
||||||
|
RequiredFeatures = 0x8000,
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService
|
public class ModDataEditor(SaveService saveService, CommunicatorService communicatorService, ItemData itemData) : IService
|
||||||
|
|
@ -35,7 +36,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
|
|
||||||
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
/// <summary> Create the file containing the meta information about a mod from scratch. </summary>
|
||||||
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
public void CreateMeta(DirectoryInfo directory, string? name, string? author, string? description, string? version,
|
||||||
string? website)
|
string? website, params string[] tags)
|
||||||
{
|
{
|
||||||
var mod = new Mod(directory);
|
var mod = new Mod(directory);
|
||||||
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
|
mod.Name = name.IsNullOrEmpty() ? mod.Name : new LowerString(name);
|
||||||
|
|
@ -43,6 +44,7 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
mod.Description = description ?? mod.Description;
|
mod.Description = description ?? mod.Description;
|
||||||
mod.Version = version ?? mod.Version;
|
mod.Version = version ?? mod.Version;
|
||||||
mod.Website = website ?? mod.Website;
|
mod.Website = website ?? mod.Website;
|
||||||
|
mod.ModTags = tags;
|
||||||
saveService.ImmediateSaveSync(new ModMeta(mod));
|
saveService.ImmediateSaveSync(new ModMeta(mod));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +97,16 @@ public class ModDataEditor(SaveService saveService, CommunicatorService communic
|
||||||
mod.Website = newWebsite;
|
mod.Website = newWebsite;
|
||||||
saveService.QueueSave(new ModMeta(mod));
|
saveService.QueueSave(new ModMeta(mod));
|
||||||
communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
|
communicatorService.ModDataChanged.Invoke(ModDataChangeType.Website, mod, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ChangeRequiredFeatures(Mod mod, FeatureFlags flags)
|
||||||
|
{
|
||||||
|
if (mod.RequiredFeatures == flags)
|
||||||
|
return;
|
||||||
|
|
||||||
|
mod.RequiredFeatures = flags;
|
||||||
|
saveService.QueueSave(new ModMeta(mod));
|
||||||
|
communicatorService.ModDataChanged.Invoke(ModDataChangeType.RequiredFeatures, mod, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeModTag(Mod mod, int tagIdx, string newTag)
|
public void ChangeModTag(Mod mod, int tagIdx, string newTag)
|
||||||
|
|
|
||||||
|
|
@ -37,11 +37,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
||||||
|
|
||||||
public struct ImportDate : ISortMode<Mod>
|
public struct ImportDate : ISortMode<Mod>
|
||||||
{
|
{
|
||||||
public string Name
|
public ReadOnlySpan<byte> Name
|
||||||
=> "Import Date (Older First)";
|
=> "Import Date (Older First)"u8;
|
||||||
|
|
||||||
public string Description
|
public ReadOnlySpan<byte> Description
|
||||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date.";
|
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their import date."u8;
|
||||||
|
|
||||||
public IEnumerable<IPath> GetChildren(Folder f)
|
public IEnumerable<IPath> GetChildren(Folder f)
|
||||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate));
|
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderBy(l => l.Value.ImportDate));
|
||||||
|
|
@ -49,11 +49,11 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
||||||
|
|
||||||
public struct InverseImportDate : ISortMode<Mod>
|
public struct InverseImportDate : ISortMode<Mod>
|
||||||
{
|
{
|
||||||
public string Name
|
public ReadOnlySpan<byte> Name
|
||||||
=> "Import Date (Newer First)";
|
=> "Import Date (Newer First)"u8;
|
||||||
|
|
||||||
public string Description
|
public ReadOnlySpan<byte> Description
|
||||||
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date.";
|
=> "In each folder, sort all subfolders lexicographically, then sort all leaves using their inverse import date."u8;
|
||||||
|
|
||||||
public IEnumerable<IPath> GetChildren(Folder f)
|
public IEnumerable<IPath> GetChildren(Folder f)
|
||||||
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate));
|
=> f.GetSubFolders().Cast<IPath>().Concat(f.GetLeaves().OrderByDescending(l => l.Value.ImportDate));
|
||||||
|
|
@ -80,7 +80,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
||||||
// Update sort order when defaulted mod names change.
|
// Update sort order when defaulted mod names change.
|
||||||
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
private void OnModDataChange(ModDataChangeType type, Mod mod, string? oldName)
|
||||||
{
|
{
|
||||||
if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !FindLeaf(mod, out var leaf))
|
if (!type.HasFlag(ModDataChangeType.Name) || oldName == null || !TryGetValue(mod, out var leaf))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var old = oldName.FixName();
|
var old = oldName.FixName();
|
||||||
|
|
@ -111,7 +111,7 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
||||||
CreateDuplicateLeaf(parent, mod.Name.Text, mod);
|
CreateDuplicateLeaf(parent, mod.Name.Text, mod);
|
||||||
break;
|
break;
|
||||||
case ModPathChangeType.Deleted:
|
case ModPathChangeType.Deleted:
|
||||||
if (FindLeaf(mod, out var leaf))
|
if (TryGetValue(mod, out var leaf))
|
||||||
Delete(leaf);
|
Delete(leaf);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -124,16 +124,6 @@ public sealed class ModFileSystem : FileSystem<Mod>, IDisposable, ISavable, ISer
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search the entire filesystem for the leaf corresponding to a mod.
|
|
||||||
public bool FindLeaf(Mod mod, [NotNullWhen(true)] out Leaf? leaf)
|
|
||||||
{
|
|
||||||
leaf = Root.GetAllDescendants(ISortMode<Mod>.Lexicographical)
|
|
||||||
.OfType<Leaf>()
|
|
||||||
.FirstOrDefault(l => l.Value == mod);
|
|
||||||
return leaf != null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// Used for saving and loading.
|
// Used for saving and loading.
|
||||||
private static string ModToIdentifier(Mod mod)
|
private static string ModToIdentifier(Mod mod)
|
||||||
=> mod.ModPath.Name;
|
=> mod.ModPath.Name;
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,6 @@ public class ModImportManager(ModManager modManager, Configuration config, ModEd
|
||||||
_import = null;
|
_import = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod)
|
public bool AddUnpackedMod([NotNullWhen(true)] out Mod? mod)
|
||||||
{
|
{
|
||||||
if (!_modsToAdd.TryDequeue(out var directory))
|
if (!_modsToAdd.TryDequeue(out var directory))
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using OtterGui.Services;
|
using OtterGui.Services;
|
||||||
using Penumbra.Communication;
|
using Penumbra.Communication;
|
||||||
|
using Penumbra.Interop;
|
||||||
using Penumbra.Mods.Editor;
|
using Penumbra.Mods.Editor;
|
||||||
using Penumbra.Mods.Manager.OptionEditor;
|
using Penumbra.Mods.Manager.OptionEditor;
|
||||||
using Penumbra.Services;
|
using Penumbra.Services;
|
||||||
|
|
@ -143,9 +144,10 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
_communicator.ModPathChanged.Invoke(ModPathChangeType.StartingReload, mod, mod.ModPath, mod.ModPath);
|
||||||
if (!Creator.ReloadMod(mod, true, false, out var metaChange))
|
if (!Creator.ReloadMod(mod, true, false, out var metaChange))
|
||||||
{
|
{
|
||||||
Penumbra.Log.Warning(mod.Name.Length == 0
|
if (mod.RequiredFeatures is not FeatureFlags.Invalid)
|
||||||
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
|
Penumbra.Log.Warning(mod.Name.Length == 0
|
||||||
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead.");
|
? $"Reloading mod {oldName} has failed, new name is empty. Removing from loaded mods instead."
|
||||||
|
: $"Reloading mod {oldName} failed, {mod.ModPath.FullName} does not exist anymore or it has invalid data. Removing from loaded mods instead.");
|
||||||
RemoveMod(mod);
|
RemoveMod(mod);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -251,12 +253,8 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
{
|
{
|
||||||
switch (type)
|
switch (type)
|
||||||
{
|
{
|
||||||
case ModPathChangeType.Added:
|
case ModPathChangeType.Added: SetNew(mod); break;
|
||||||
SetNew(mod);
|
case ModPathChangeType.Deleted: SetKnown(mod); break;
|
||||||
break;
|
|
||||||
case ModPathChangeType.Deleted:
|
|
||||||
SetKnown(mod);
|
|
||||||
break;
|
|
||||||
case ModPathChangeType.Moved:
|
case ModPathChangeType.Moved:
|
||||||
if (oldDirectory != null && newDirectory != null)
|
if (oldDirectory != null && newDirectory != null)
|
||||||
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
DataEditor.MoveDataFile(oldDirectory, newDirectory);
|
||||||
|
|
@ -306,6 +304,9 @@ public sealed class ModManager : ModStorage, IDisposable, IService
|
||||||
if (!firstTime && _config.ModDirectory != BasePath.FullName)
|
if (!firstTime && _config.ModDirectory != BasePath.FullName)
|
||||||
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
TriggerModDirectoryChange(BasePath.FullName, Valid);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (CloudApi.IsCloudSynced(BasePath.FullName))
|
||||||
|
Penumbra.Log.Warning($"Mod base directory {BasePath.FullName} is cloud-synced. This may cause issues.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void TriggerModDirectoryChange(string newPath, bool valid)
|
private void TriggerModDirectoryChange(string newPath, bool valid)
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,8 @@ public abstract class ModOptionEditor<TGroup, TOption>(
|
||||||
where TOption : class, IModOption
|
where TOption : class, IModOption
|
||||||
{
|
{
|
||||||
protected readonly CommunicatorService Communicator = communicator;
|
protected readonly CommunicatorService Communicator = communicator;
|
||||||
protected readonly SaveService SaveService = saveService;
|
protected readonly SaveService SaveService = saveService;
|
||||||
protected readonly Configuration Config = config;
|
protected readonly Configuration Config = config;
|
||||||
|
|
||||||
/// <summary> Add a new, empty option group of the given type and name. </summary>
|
/// <summary> Add a new, empty option group of the given type and name. </summary>
|
||||||
public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync)
|
public TGroup? AddModGroup(Mod mod, string newName, SaveType saveType = SaveType.ImmediateSync)
|
||||||
|
|
@ -25,7 +25,7 @@ public abstract class ModOptionEditor<TGroup, TOption>(
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
|
var maxPriority = mod.Groups.Count == 0 ? ModPriority.Default : mod.Groups.Max(o => o.Priority) + 1;
|
||||||
var group = CreateGroup(mod, newName, maxPriority);
|
var group = CreateGroup(mod, newName, maxPriority);
|
||||||
mod.Groups.Add(group);
|
mod.Groups.Add(group);
|
||||||
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
SaveService.Save(saveType, new ModSaveGroup(group, Config.ReplaceNonAsciiOnImport));
|
||||||
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1);
|
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.GroupAdded, mod, group, null, null, -1);
|
||||||
|
|
@ -92,8 +92,8 @@ public abstract class ModOptionEditor<TGroup, TOption>(
|
||||||
/// <summary> Delete the given option from the given group. </summary>
|
/// <summary> Delete the given option from the given group. </summary>
|
||||||
public void DeleteOption(TOption option)
|
public void DeleteOption(TOption option)
|
||||||
{
|
{
|
||||||
var mod = option.Mod;
|
var mod = option.Mod;
|
||||||
var group = option.Group;
|
var group = option.Group;
|
||||||
var optionIdx = option.GetIndex();
|
var optionIdx = option.GetIndex();
|
||||||
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1);
|
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.PrepareChange, mod, group, option, null, -1);
|
||||||
RemoveOption((TGroup)group, optionIdx);
|
RemoveOption((TGroup)group, optionIdx);
|
||||||
|
|
@ -104,7 +104,7 @@ public abstract class ModOptionEditor<TGroup, TOption>(
|
||||||
/// <summary> Move an option inside the given option group. </summary>
|
/// <summary> Move an option inside the given option group. </summary>
|
||||||
public void MoveOption(TOption option, int optionIdxTo)
|
public void MoveOption(TOption option, int optionIdxTo)
|
||||||
{
|
{
|
||||||
var idx = option.GetIndex();
|
var idx = option.GetIndex();
|
||||||
var group = (TGroup)option.Group;
|
var group = (TGroup)option.Group;
|
||||||
if (!MoveOption(group, idx, optionIdxTo))
|
if (!MoveOption(group, idx, optionIdxTo))
|
||||||
return;
|
return;
|
||||||
|
|
@ -113,10 +113,10 @@ public abstract class ModOptionEditor<TGroup, TOption>(
|
||||||
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx);
|
Communicator.ModOptionChanged.Invoke(ModOptionChangeType.OptionMoved, group.Mod, group, option, null, idx);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync);
|
protected abstract TGroup CreateGroup(Mod mod, string newName, ModPriority priority, SaveType saveType = SaveType.ImmediateSync);
|
||||||
protected abstract TOption? CloneOption(TGroup group, IModOption option);
|
protected abstract TOption? CloneOption(TGroup group, IModOption option);
|
||||||
protected abstract void RemoveOption(TGroup group, int optionIndex);
|
protected abstract void RemoveOption(TGroup group, int optionIndex);
|
||||||
protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo);
|
protected abstract bool MoveOption(TGroup group, int optionIdxFrom, int optionIdxTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class ModOptionChangeTypeExtension
|
public static class ModOptionChangeTypeExtension
|
||||||
|
|
@ -132,22 +132,22 @@ public static class ModOptionChangeTypeExtension
|
||||||
{
|
{
|
||||||
(requiresSaving, requiresReloading, wasPrepared) = type switch
|
(requiresSaving, requiresReloading, wasPrepared) = type switch
|
||||||
{
|
{
|
||||||
ModOptionChangeType.GroupRenamed => (true, false, false),
|
ModOptionChangeType.GroupRenamed => (true, false, false),
|
||||||
ModOptionChangeType.GroupAdded => (true, false, false),
|
ModOptionChangeType.GroupAdded => (true, false, false),
|
||||||
ModOptionChangeType.GroupDeleted => (true, true, false),
|
ModOptionChangeType.GroupDeleted => (true, true, false),
|
||||||
ModOptionChangeType.GroupMoved => (true, false, false),
|
ModOptionChangeType.GroupMoved => (true, false, false),
|
||||||
ModOptionChangeType.GroupTypeChanged => (true, true, true),
|
ModOptionChangeType.GroupTypeChanged => (true, true, true),
|
||||||
ModOptionChangeType.PriorityChanged => (true, true, true),
|
ModOptionChangeType.PriorityChanged => (true, true, true),
|
||||||
ModOptionChangeType.OptionAdded => (true, true, true),
|
ModOptionChangeType.OptionAdded => (true, true, true),
|
||||||
ModOptionChangeType.OptionDeleted => (true, true, false),
|
ModOptionChangeType.OptionDeleted => (true, true, false),
|
||||||
ModOptionChangeType.OptionMoved => (true, false, false),
|
ModOptionChangeType.OptionMoved => (true, false, false),
|
||||||
ModOptionChangeType.OptionFilesChanged => (false, true, false),
|
ModOptionChangeType.OptionFilesChanged => (false, true, false),
|
||||||
ModOptionChangeType.OptionFilesAdded => (false, true, true),
|
ModOptionChangeType.OptionFilesAdded => (false, true, true),
|
||||||
ModOptionChangeType.OptionSwapsChanged => (false, true, false),
|
ModOptionChangeType.OptionSwapsChanged => (false, true, false),
|
||||||
ModOptionChangeType.OptionMetaChanged => (false, true, false),
|
ModOptionChangeType.OptionMetaChanged => (false, true, false),
|
||||||
ModOptionChangeType.DisplayChange => (false, false, false),
|
ModOptionChangeType.DisplayChange => (false, false, false),
|
||||||
ModOptionChangeType.DefaultOptionChanged => (true, false, false),
|
ModOptionChangeType.DefaultOptionChanged => (true, false, false),
|
||||||
_ => (false, false, false),
|
_ => (false, false, false),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
using OtterGui;
|
|
||||||
using OtterGui.Classes;
|
using OtterGui.Classes;
|
||||||
using OtterGui.Extensions;
|
using OtterGui.Extensions;
|
||||||
using Penumbra.GameData.Data;
|
using Penumbra.GameData.Data;
|
||||||
|
|
@ -12,6 +11,16 @@ using Penumbra.String.Classes;
|
||||||
|
|
||||||
namespace Penumbra.Mods;
|
namespace Penumbra.Mods;
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum FeatureFlags : ulong
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Atch = 1ul << 0,
|
||||||
|
Shp = 1ul << 1,
|
||||||
|
Atr = 1ul << 2,
|
||||||
|
Invalid = 1ul << 62,
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class Mod : IMod
|
public sealed class Mod : IMod
|
||||||
{
|
{
|
||||||
public static readonly TemporaryMod ForcedFiles = new()
|
public static readonly TemporaryMod ForcedFiles = new()
|
||||||
|
|
@ -57,6 +66,7 @@ public sealed class Mod : IMod
|
||||||
public string Image { get; internal set; } = string.Empty;
|
public string Image { get; internal set; } = string.Empty;
|
||||||
public IReadOnlyList<string> ModTags { get; internal set; } = [];
|
public IReadOnlyList<string> ModTags { get; internal set; } = [];
|
||||||
public HashSet<CustomItemId> DefaultPreferredItems { get; internal set; } = [];
|
public HashSet<CustomItemId> DefaultPreferredItems { get; internal set; } = [];
|
||||||
|
public FeatureFlags RequiredFeatures { get; internal set; } = 0;
|
||||||
|
|
||||||
|
|
||||||
// Local Data
|
// Local Data
|
||||||
|
|
@ -70,6 +80,23 @@ public sealed class Mod : IMod
|
||||||
public readonly DefaultSubMod Default;
|
public readonly DefaultSubMod Default;
|
||||||
public readonly List<IModGroup> Groups = [];
|
public readonly List<IModGroup> Groups = [];
|
||||||
|
|
||||||
|
/// <summary> Compute the required feature flags for this mod. </summary>
|
||||||
|
public FeatureFlags ComputeRequiredFeatures()
|
||||||
|
{
|
||||||
|
var flags = FeatureFlags.None;
|
||||||
|
foreach (var option in AllDataContainers)
|
||||||
|
{
|
||||||
|
if (option.Manipulations.Atch.Count > 0)
|
||||||
|
flags |= FeatureFlags.Atch;
|
||||||
|
if (option.Manipulations.Atr.Count > 0)
|
||||||
|
flags |= FeatureFlags.Atr;
|
||||||
|
if (option.Manipulations.Shp.Count > 0)
|
||||||
|
flags |= FeatureFlags.Shp;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
public AppliedModData GetData(ModSettings? settings = null)
|
public AppliedModData GetData(ModSettings? settings = null)
|
||||||
{
|
{
|
||||||
if (settings is not { Enabled: true })
|
if (settings is not { Enabled: true })
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,16 @@ public partial class ModCreator(
|
||||||
MetaFileManager metaFileManager,
|
MetaFileManager metaFileManager,
|
||||||
GamePathParser gamePathParser) : IService
|
GamePathParser gamePathParser) : IService
|
||||||
{
|
{
|
||||||
public readonly Configuration Config = config;
|
public const FeatureFlags SupportedFeatures = FeatureFlags.Atch | FeatureFlags.Shp | FeatureFlags.Atr;
|
||||||
|
public readonly Configuration Config = config;
|
||||||
|
|
||||||
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
|
/// <summary> Creates directory and files necessary for a new mod without adding it to the manager. </summary>
|
||||||
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null)
|
public DirectoryInfo? CreateEmptyMod(DirectoryInfo basePath, string newName, string description = "", string? author = null, params string[] tags)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
var newDir = CreateModFolder(basePath, newName, Config.ReplaceNonAsciiOnImport, true);
|
||||||
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty);
|
dataEditor.CreateMeta(newDir, newName, author ?? Config.DefaultModAuthor, description, "1.0", string.Empty, tags);
|
||||||
CreateDefaultFiles(newDir);
|
CreateDefaultFiles(newDir);
|
||||||
return newDir;
|
return newDir;
|
||||||
}
|
}
|
||||||
|
|
@ -74,7 +75,7 @@ public partial class ModCreator(
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
modDataChange = ModMeta.Load(dataEditor, this, mod);
|
modDataChange = ModMeta.Load(dataEditor, this, mod);
|
||||||
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0)
|
if (modDataChange.HasFlag(ModDataChangeType.Deletion) || mod.Name.Length == 0 || mod.RequiredFeatures is FeatureFlags.Invalid)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
modDataChange |= ModLocalData.Load(dataEditor, mod);
|
modDataChange |= ModLocalData.Load(dataEditor, mod);
|
||||||
|
|
@ -82,9 +83,9 @@ public partial class ModCreator(
|
||||||
LoadAllGroups(mod);
|
LoadAllGroups(mod);
|
||||||
if (incorporateMetaChanges)
|
if (incorporateMetaChanges)
|
||||||
IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges);
|
IncorporateAllMetaChanges(mod, true, deleteDefaultMetaChanges);
|
||||||
else if (deleteDefaultMetaChanges)
|
else if (deleteDefaultMetaChanges)
|
||||||
ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false);
|
ModMetaEditor.DeleteDefaultValues(mod, metaFileManager, saveService, false);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
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