Meta stuff is terrible.

This commit is contained in:
Ottermandias 2023-04-16 13:18:43 +02:00
parent 0186f176d0
commit 1d82e882ed
35 changed files with 1265 additions and 1247 deletions

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -11,33 +13,41 @@ namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable public struct CmpCache : IDisposable
{ {
private CmpFile? _cmpFile = null; private CmpFile? _cmpFile = null;
private readonly List< RspManipulation > _cmpManipulations = new(); private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache() public CmpCache()
{} { }
public void SetFiles(CollectionCacheManager manager) public void SetFiles(MetaFileManager manager)
=> manager.SetFile( _cmpFile, MetaIndex.HumanCmp ); => manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp ); => manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public bool ApplyMod( CollectionCacheManager manager, RspManipulation manip ) public void Reset()
{ {
_cmpManipulations.AddOrReplace( manip ); if (_cmpFile == null)
_cmpFile ??= new CmpFile(); return;
return manip.Apply( _cmpFile );
_cmpFile.Reset(_cmpManipulations.Select(m => (m.SubRace, m.Attribute)));
_cmpManipulations.Clear();
} }
public bool RevertMod( CollectionCacheManager manager, RspManipulation manip ) public bool ApplyMod(MetaFileManager manager, RspManipulation manip)
{
_cmpManipulations.AddOrReplace(manip);
_cmpFile ??= new CmpFile(manager);
return manip.Apply(_cmpFile);
}
public bool RevertMod(MetaFileManager manager, RspManipulation manip)
{ {
if (!_cmpManipulations.Remove(manip)) if (!_cmpManipulations.Remove(manip))
return false; return false;
var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute ); var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation( manip.SubRace, manip.Attribute, def ); manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply( _cmpFile! ); return manip.Apply(_cmpFile!);
} }
public void Dispose() public void Dispose()

View file

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.String.Classes; using Penumbra.String.Classes;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
@ -51,7 +52,7 @@ public class CollectionCache : IDisposable
{ {
_manager = manager; _manager = manager;
_collection = collection; _collection = collection;
MetaManipulations = new MetaCache(_collection); MetaManipulations = new MetaCache(manager.MetaFileManager, _collection);
} }
public void Dispose() public void Dispose()
@ -59,6 +60,9 @@ public class CollectionCache : IDisposable
MetaManipulations.Dispose(); MetaManipulations.Dispose();
} }
~CollectionCache()
=> MetaManipulations.Dispose();
// Resolve a given game path according to this collection. // Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath) public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
{ {
@ -115,6 +119,17 @@ public class CollectionCache : IDisposable
return ret; return ret;
} }
/// <summary> Force a file to be resolved to a specific path regardless of conflicts. </summary>
internal void ForceFile(Utf8GamePath path, FullPath fullPath)
{
if (CheckFullPath(path, fullPath))
ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath);
}
/// <summary> Force a file resolve to be removed. </summary>
internal void RemoveFile(Utf8GamePath path)
=> ResolvedFiles.Remove(path);
public void ReloadMod(IMod mod, bool addMetaChanges) public void ReloadMod(IMod mod, bool addMetaChanges)
{ {
RemoveMod(mod, addMetaChanges); RemoveMod(mod, addMetaChanges);
@ -234,7 +249,7 @@ public class CollectionCache : IDisposable
// Inside the same mod, conflicts are not recorded. // Inside the same mod, conflicts are not recorded.
private void AddFile(Utf8GamePath path, FullPath file, IMod mod) private void AddFile(Utf8GamePath path, FullPath file, IMod mod)
{ {
if (!ModCollection.CheckFullPath(path, file)) if (!CheckFullPath(path, file))
return; return;
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file))) if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@ -387,4 +402,14 @@ public class CollectionCache : IDisposable
Penumbra.Log.Error($"Unknown Error:\n{e}"); Penumbra.Log.Error($"Unknown Error:\n{e}");
} }
} }
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
{
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
return true;
Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}");
return false;
}
} }

View file

@ -8,7 +8,7 @@ using OtterGui.Classes;
using Penumbra.Api; using Penumbra.Api;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Interop.Services; using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Services; using Penumbra.Services;
@ -18,16 +18,12 @@ namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable public class CollectionCacheManager : IDisposable
{ {
private readonly FrameworkManager _framework; private readonly FrameworkManager _framework;
private readonly ActiveCollections _active;
private readonly CommunicatorService _communicator; private readonly CommunicatorService _communicator;
private readonly TempModManager _tempMods; private readonly TempModManager _tempMods;
private readonly ModStorage _modStorage; private readonly ModStorage _modStorage;
private readonly ModCacheManager _modCaches; private readonly ActiveCollections _active;
private readonly Configuration _config;
internal readonly ValidityChecker ValidityChecker; internal readonly MetaFileManager MetaFileManager;
internal readonly CharacterUtility CharacterUtility;
internal readonly ResidentResourceManager ResidentResources;
private readonly Dictionary<ModCollection, CollectionCache> _caches = new(); private readonly Dictionary<ModCollection, CollectionCache> _caches = new();
@ -37,20 +33,15 @@ public class CollectionCacheManager : IDisposable
public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active
=> _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value)); => _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value));
public CollectionCacheManager(FrameworkManager framework, ActiveCollections active, CommunicatorService communicator, public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator,
CharacterUtility characterUtility, TempModManager tempMods, ModStorage modStorage, Configuration config, TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active)
ResidentResourceManager residentResources, ModCacheManager modCaches, ValidityChecker validityChecker)
{ {
_framework = framework; _framework = framework;
_active = active;
_communicator = communicator; _communicator = communicator;
CharacterUtility = characterUtility;
_tempMods = tempMods; _tempMods = tempMods;
_modStorage = modStorage; _modStorage = modStorage;
_config = config; MetaFileManager = metaFileManager;
ResidentResources = residentResources; _active = active;
_modCaches = modCaches;
ValidityChecker = validityChecker;
_communicator.CollectionChange.Subscribe(OnCollectionChange); _communicator.CollectionChange.Subscribe(OnCollectionChange);
_communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100); _communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100);
@ -61,8 +52,8 @@ public class CollectionCacheManager : IDisposable
_communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange); _communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange);
CreateNecessaryCaches(); CreateNecessaryCaches();
if (!CharacterUtility.Ready) if (!MetaFileManager.CharacterUtility.Ready)
CharacterUtility.LoadingFinished += IncrementCounters; MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters;
} }
public void Dispose() public void Dispose()
@ -74,7 +65,7 @@ public class CollectionCacheManager : IDisposable
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange); _communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange); _communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange); _communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
CharacterUtility.LoadingFinished -= IncrementCounters; MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
} }
/// <summary> Only creates a new cache, does not update an existing one. </summary> /// <summary> Only creates a new cache, does not update an existing one. </summary>
@ -98,9 +89,6 @@ public class CollectionCacheManager : IDisposable
=> _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name, => _framework.RegisterImportant(nameof(CalculateEffectiveFileList) + collection.Name,
() => CalculateEffectiveFileListInternal(collection)); () => CalculateEffectiveFileListInternal(collection));
public bool IsDefault(ModCollection collection)
=> _active.Default == collection;
private void CalculateEffectiveFileListInternal(ModCollection collection) private void CalculateEffectiveFileListInternal(ModCollection collection)
{ {
// Skip the empty collection. // Skip the empty collection.
@ -139,11 +127,7 @@ public class CollectionCacheManager : IDisposable
++collection.ChangeCounter; ++collection.ChangeCounter;
if (_active.Default != collection || !CharacterUtility.Ready || !_config.EnableMods) MetaFileManager.ApplyDefaultFiles(collection);
return;
ResidentResources.Reload();
cache.MetaManipulations.SetFiles();
} }
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName) private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName)
@ -238,7 +222,7 @@ public class CollectionCacheManager : IDisposable
{ {
foreach (var (collection, _) in _caches) foreach (var (collection, _) in _caches)
++collection.ChangeCounter; ++collection.ChangeCounter;
CharacterUtility.LoadingFinished -= IncrementCounters; MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
} }
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _) private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _)

View file

@ -1,11 +1,13 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using OtterGui; using OtterGui;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -19,23 +21,29 @@ public readonly struct EqdpCache : IDisposable
public EqdpCache() public EqdpCache()
{ } { }
public void SetFiles(CollectionCacheManager manager) public void SetFiles(MetaFileManager manager)
{ {
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i) for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++i)
manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]); manager.SetFile(_eqdpFiles[i], CharacterUtilityData.EqdpIndices[i]);
} }
public CharacterUtility.MetaList.MetaReverter? TemporarilySetEqdpFile(CollectionCacheManager manager, GenderRace genderRace, bool accessory) public void SetFile(MetaFileManager manager, MetaIndex index)
{ {
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory); var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if ((int)idx == -1) if (i != -1)
return null; manager.SetFile(_eqdpFiles[i], index);
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
return i != -1 ? manager.TemporarilySetFile(_eqdpFiles[i], idx) : null;
} }
public void Reset(CollectionCacheManager manager) public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, GenderRace genderRace, bool accessory)
{
var idx = CharacterUtilityData.EqdpIdx(genderRace, accessory);
Debug.Assert(idx >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
Debug.Assert(i >= 0, $"Invalid Gender, Race or Accessory for EQDP file {genderRace}, {accessory}.");
return manager.TemporarilySetFile(_eqdpFiles[i], idx);
}
public void Reset()
{ {
foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>()) foreach (var file in _eqdpFiles.OfType<ExpandedEqdpFile>())
{ {
@ -46,20 +54,20 @@ public readonly struct EqdpCache : IDisposable
_eqdpManipulations.Clear(); _eqdpManipulations.Clear();
} }
public bool ApplyMod(CollectionCacheManager manager, EqdpManipulation manip) public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
{ {
_eqdpManipulations.AddOrReplace(manip); _eqdpManipulations.AddOrReplace(manip);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??= var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())] ??=
new ExpandedEqdpFile(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar new ExpandedEqdpFile(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory()); // TODO: female Hrothgar
return manip.Apply(file); return manip.Apply(file);
} }
public bool RevertMod(CollectionCacheManager manager, EqdpManipulation manip) public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
{ {
if (!_eqdpManipulations.Remove(manip)) if (!_eqdpManipulations.Remove(manip))
return false; return false;
var def = ExpandedEqdpFile.GetDefault(Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId); var def = ExpandedEqdpFile.GetDefault(manager, Names.CombinedRace(manip.Gender, manip.Race), manip.Slot.IsAccessory(), manip.SetId);
var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!; var file = _eqdpFiles[Array.IndexOf(CharacterUtilityData.EqdpIndices, manip.FileIndex())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId); manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file); return manip.Apply(file);

View file

@ -1,8 +1,10 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -16,29 +18,38 @@ public struct EqpCache : IDisposable
public EqpCache() public EqpCache()
{} {}
public void SetFiles(CollectionCacheManager manager) public void SetFiles(MetaFileManager manager)
=> manager.SetFile( _eqpFile, MetaIndex.Eqp ); => manager.SetFile( _eqpFile, MetaIndex.Eqp );
public static void ResetFiles(CollectionCacheManager manager) public static void ResetFiles(MetaFileManager manager)
=> manager.SetFile( null, MetaIndex.Eqp ); => manager.SetFile( null, MetaIndex.Eqp );
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp ); => manager.TemporarilySetFile( _eqpFile, MetaIndex.Eqp );
public bool ApplyMod( CollectionCacheManager manager, EqpManipulation manip ) public void Reset()
{
if (_eqpFile == null)
return;
_eqpFile.Reset(_eqpManipulations.Select(m => (int)m.SetId));
_eqpManipulations.Clear();
}
public bool ApplyMod( MetaFileManager manager, EqpManipulation manip )
{ {
_eqpManipulations.AddOrReplace( manip ); _eqpManipulations.AddOrReplace( manip );
_eqpFile ??= new ExpandedEqpFile(); _eqpFile ??= new ExpandedEqpFile(manager);
return manip.Apply( _eqpFile ); return manip.Apply( _eqpFile );
} }
public bool RevertMod( CollectionCacheManager manager, EqpManipulation manip ) public bool RevertMod( MetaFileManager manager, EqpManipulation manip )
{ {
var idx = _eqpManipulations.FindIndex( manip.Equals ); var idx = _eqpManipulations.FindIndex( manip.Equals );
if (idx < 0) if (idx < 0)
return false; return false;
var def = ExpandedEqpFile.GetDefault( manip.SetId ); var def = ExpandedEqpFile.GetDefault( manager, manip.SetId );
manip = new EqpManipulation( def, manip.Slot, manip.SetId ); manip = new EqpManipulation( def, manip.Slot, manip.SetId );
return manip.Apply( _eqpFile! ); return manip.Apply( _eqpFile! );

View file

@ -4,6 +4,7 @@ using OtterGui.Filesystem;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -16,31 +17,50 @@ public struct EstCache : IDisposable
private EstFile? _estBodyFile = null; private EstFile? _estBodyFile = null;
private EstFile? _estHeadFile = null; private EstFile? _estHeadFile = null;
private readonly List< EstManipulation > _estManipulations = new(); private readonly List<EstManipulation> _estManipulations = new();
public EstCache() public EstCache()
{} { }
public void SetFiles(CollectionCacheManager manager) public void SetFiles(MetaFileManager manager)
{ {
manager.SetFile( _estFaceFile, MetaIndex.FaceEst ); manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
manager.SetFile( _estHairFile, MetaIndex.HairEst ); manager.SetFile(_estHairFile, MetaIndex.HairEst);
manager.SetFile( _estBodyFile, MetaIndex.BodyEst ); manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
manager.SetFile( _estHeadFile, MetaIndex.HeadEst ); manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
} }
public CharacterUtility.MetaList.MetaReverter? TemporarilySetFiles(CollectionCacheManager manager, EstManipulation.EstType type) public void SetFile(MetaFileManager manager, MetaIndex index)
{
switch (index)
{
case MetaIndex.FaceEst:
manager.SetFile(_estFaceFile, MetaIndex.FaceEst);
break;
case MetaIndex.HairEst:
manager.SetFile(_estHairFile, MetaIndex.HairEst);
break;
case MetaIndex.BodyEst:
manager.SetFile(_estBodyFile, MetaIndex.BodyEst);
break;
case MetaIndex.HeadEst:
manager.SetFile(_estHeadFile, MetaIndex.HeadEst);
break;
}
}
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager, EstManipulation.EstType type)
{ {
var (file, idx) = type switch var (file, idx) = type switch
{ {
EstManipulation.EstType.Face => ( _estFaceFile, MetaIndex.FaceEst ), EstManipulation.EstType.Face => (_estFaceFile, MetaIndex.FaceEst),
EstManipulation.EstType.Hair => ( _estHairFile, MetaIndex.HairEst ), EstManipulation.EstType.Hair => (_estHairFile, MetaIndex.HairEst),
EstManipulation.EstType.Body => ( _estBodyFile, MetaIndex.BodyEst ), EstManipulation.EstType.Body => (_estBodyFile, MetaIndex.BodyEst),
EstManipulation.EstType.Head => ( _estHeadFile, MetaIndex.HeadEst ), EstManipulation.EstType.Head => (_estHeadFile, MetaIndex.HeadEst),
_ => ( null, 0 ), _ => (null, 0),
}; };
return idx != 0 ? manager.TemporarilySetFile( file, idx ) : null; return manager.TemporarilySetFile(file, idx);
} }
public void Reset() public void Reset()
@ -52,27 +72,27 @@ public struct EstCache : IDisposable
_estManipulations.Clear(); _estManipulations.Clear();
} }
public bool ApplyMod( CollectionCacheManager manager, EstManipulation m ) public bool ApplyMod(MetaFileManager manager, EstManipulation m)
{ {
_estManipulations.AddOrReplace( m ); _estManipulations.AddOrReplace(m);
var file = m.Slot switch var file = m.Slot switch
{ {
EstManipulation.EstType.Hair => _estHairFile ??= new EstFile( EstManipulation.EstType.Hair ), EstManipulation.EstType.Hair => _estHairFile ??= new EstFile(manager, EstManipulation.EstType.Hair),
EstManipulation.EstType.Face => _estFaceFile ??= new EstFile( EstManipulation.EstType.Face ), EstManipulation.EstType.Face => _estFaceFile ??= new EstFile(manager, EstManipulation.EstType.Face),
EstManipulation.EstType.Body => _estBodyFile ??= new EstFile( EstManipulation.EstType.Body ), EstManipulation.EstType.Body => _estBodyFile ??= new EstFile(manager, EstManipulation.EstType.Body),
EstManipulation.EstType.Head => _estHeadFile ??= new EstFile( EstManipulation.EstType.Head ), EstManipulation.EstType.Head => _estHeadFile ??= new EstFile(manager, EstManipulation.EstType.Head),
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
return m.Apply( file ); return m.Apply(file);
} }
public bool RevertMod( CollectionCacheManager manager, EstManipulation m ) public bool RevertMod(MetaFileManager manager, EstManipulation m)
{ {
if (!_estManipulations.Remove(m)) if (!_estManipulations.Remove(m))
return false; return false;
var def = EstFile.GetDefault( m.Slot, Names.CombinedRace( m.Gender, m.Race ), m.SetId ); var def = EstFile.GetDefault(manager, m.Slot, Names.CombinedRace(m.Gender, m.Race), m.SetId);
var manip = new EstManipulation( m.Gender, m.Race, m.Slot, m.SetId, def ); var manip = new EstManipulation(m.Gender, m.Race, m.Slot, m.SetId, def);
var file = m.Slot switch var file = m.Slot switch
{ {
EstManipulation.EstType.Hair => _estHairFile!, EstManipulation.EstType.Hair => _estHairFile!,
@ -81,8 +101,7 @@ public struct EstCache : IDisposable
EstManipulation.EstType.Head => _estHeadFile!, EstManipulation.EstType.Head => _estHeadFile!,
_ => throw new ArgumentOutOfRangeException(), _ => throw new ArgumentOutOfRangeException(),
}; };
return manip.Apply( file ); return manip.Apply(file);
} }
public void Dispose() public void Dispose()

View file

@ -4,6 +4,7 @@ using System.Linq;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -17,13 +18,13 @@ public struct GmpCache : IDisposable
public GmpCache() public GmpCache()
{} {}
public void SetFiles(CollectionCacheManager manager) public void SetFiles(MetaFileManager manager)
=> manager.SetFile( _gmpFile, MetaIndex.Gmp ); => manager.SetFile( _gmpFile, MetaIndex.Gmp );
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager) public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp ); => manager.TemporarilySetFile( _gmpFile, MetaIndex.Gmp );
public void ResetGmp(CollectionCacheManager manager) public void Reset()
{ {
if( _gmpFile == null ) if( _gmpFile == null )
return; return;
@ -32,19 +33,19 @@ public struct GmpCache : IDisposable
_gmpManipulations.Clear(); _gmpManipulations.Clear();
} }
public bool ApplyMod( CollectionCacheManager manager, GmpManipulation manip ) public bool ApplyMod( MetaFileManager manager, GmpManipulation manip )
{ {
_gmpManipulations.AddOrReplace( manip ); _gmpManipulations.AddOrReplace( manip );
_gmpFile ??= new ExpandedGmpFile(); _gmpFile ??= new ExpandedGmpFile(manager);
return manip.Apply( _gmpFile ); return manip.Apply( _gmpFile );
} }
public bool RevertMod( CollectionCacheManager manager, GmpManipulation manip ) public bool RevertMod( MetaFileManager manager, GmpManipulation manip )
{ {
if (!_gmpManipulations.Remove(manip)) if (!_gmpManipulations.Remove(manip))
return false; return false;
var def = ExpandedGmpFile.GetDefault( manip.SetId ); var def = ExpandedGmpFile.GetDefault( manager, manip.SetId );
manip = new GmpManipulation( def, manip.SetId ); manip = new GmpManipulation( def, manip.SetId );
return manip.Apply( _gmpFile! ); return manip.Apply( _gmpFile! );
} }

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using OtterGui.Filesystem; using OtterGui.Filesystem;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -16,13 +17,13 @@ public readonly struct ImcCache : IDisposable
public ImcCache() public ImcCache()
{ } { }
public void SetFiles(CollectionCacheManager manager, ModCollection collection) public void SetFiles(ModCollection collection)
{ {
foreach( var path in _imcFiles.Keys ) foreach( var path in _imcFiles.Keys )
collection._cache!.ForceFile( path, CreateImcPath( collection, path ) ); collection._cache!.ForceFile( path, CreateImcPath( collection, path ) );
} }
public void Reset(CollectionCacheManager manager, ModCollection collection) public void Reset(ModCollection collection)
{ {
foreach( var (path, file) in _imcFiles ) foreach( var (path, file) in _imcFiles )
{ {
@ -33,7 +34,7 @@ public readonly struct ImcCache : IDisposable
_imcManipulations.Clear(); _imcManipulations.Clear();
} }
public bool ApplyMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation manip ) public bool ApplyMod( MetaFileManager manager, ModCollection collection, ImcManipulation manip )
{ {
if( !manip.Valid ) if( !manip.Valid )
{ {
@ -46,7 +47,7 @@ public readonly struct ImcCache : IDisposable
{ {
if( !_imcFiles.TryGetValue( path, out var file ) ) if( !_imcFiles.TryGetValue( path, out var file ) )
{ {
file = new ImcFile( manip ); file = new ImcFile( manager, manip );
} }
if( !manip.Apply( file ) ) if( !manip.Apply( file ) )
@ -73,7 +74,7 @@ public readonly struct ImcCache : IDisposable
return false; return false;
} }
public bool RevertMod( CollectionCacheManager manager, ModCollection collection, ImcManipulation m ) public bool RevertMod( MetaFileManager manager, ModCollection collection, ImcManipulation m )
{ {
if( !m.Valid || !_imcManipulations.Remove( m ) ) if( !m.Valid || !_imcManipulations.Remove( m ) )
{ {
@ -86,7 +87,7 @@ public readonly struct ImcCache : IDisposable
return false; return false;
} }
var def = ImcFile.GetDefault( path, m.EquipSlot, m.Variant, out _ ); var def = ImcFile.GetDefault( manager, path, m.EquipSlot, m.Variant, out _ );
var manip = m.Copy( def ); var manip = m.Copy( def );
if( !manip.Apply( file ) ) if( !manip.Apply( file ) )
return false; return false;

View file

@ -2,19 +2,19 @@ using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using Penumbra.GameData.Enums;
using OtterGui;
using Penumbra.Interop.Services; using Penumbra.Interop.Services;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta.Files; using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.String.Classes;
namespace Penumbra.Collections.Cache; namespace Penumbra.Collections.Cache;
public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>> public class MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation, IMod>>
{ {
private readonly CollectionCacheManager _manager; private readonly MetaFileManager _manager;
private readonly ModCollection _collection; private readonly ModCollection _collection;
private readonly Dictionary<MetaManipulation, IMod> _manipulations = new(); private readonly Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new(); private EqpCache _eqpCache = new();
@ -22,7 +22,7 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
private EstCache _estCache = new(); private EstCache _estCache = new();
private GmpCache _gmpCache = new(); private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new(); private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache; private readonly ImcCache _imcCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod) public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
=> _manipulations.TryGetValue(manip, out mod); => _manipulations.TryGetValue(manip, out mod);
@ -39,10 +39,10 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator(); => GetEnumerator();
public MetaCache(CollectionCacheManager manager, ModCollection collection) public MetaCache(MetaFileManager manager, ModCollection collection)
{ {
_manager = manager; _manager = manager;
_imcCache = new ImcCache(collection); _collection = collection;
if (!_manager.CharacterUtility.Ready) if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations; _manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
} }
@ -54,17 +54,17 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
_estCache.SetFiles(_manager); _estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager); _gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager); _cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_manager, _collection); _imcCache.SetFiles(_collection);
} }
public void Reset() public void Reset()
{ {
_eqpCache.Reset(_manager); _eqpCache.Reset();
_eqdpCache.Reset(_manager); _eqdpCache.Reset();
_estCache.Reset(_manager); _estCache.Reset();
_gmpCache.Reset(_manager); _gmpCache.Reset();
_cmpCache.Reset(_manager); _cmpCache.Reset();
_imcCache.Reset(_manager, _collection); _imcCache.Reset(_collection);
_manipulations.Clear(); _manipulations.Clear();
} }
@ -80,6 +80,9 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
_manipulations.Clear(); _manipulations.Clear();
} }
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod) public bool ApplyMod(MetaManipulation manip, IMod mod)
{ {
if (_manipulations.ContainsKey(manip)) if (_manipulations.ContainsKey(manip))
@ -126,10 +129,60 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
}; };
} }
// Use this when CharacterUtility becomes ready. /// <summary> Set a single file. </summary>
public void SetFile(MetaIndex metaIndex)
{
switch (metaIndex)
{
case MetaIndex.Eqp:
_eqpCache.SetFiles(_manager);
break;
case MetaIndex.Gmp:
_gmpCache.SetFiles(_manager);
break;
case MetaIndex.HumanCmp:
_cmpCache.SetFiles(_manager);
break;
case MetaIndex.FaceEst:
case MetaIndex.HairEst:
case MetaIndex.HeadEst:
case MetaIndex.BodyEst:
_estCache.SetFile(_manager, metaIndex);
break;
default:
_eqdpCache.SetFile(_manager, metaIndex);
break;
}
}
/// <summary> Set the currently relevant IMC files for the collection cache. </summary>
public void SetImcFiles()
=> _imcCache.SetFiles(_collection);
public CharacterUtility.MetaList.MetaReverter TemporarilySetEqpFile()
=> _eqpCache.TemporarilySetFiles(_manager);
public CharacterUtility.MetaList.MetaReverter TemporarilySetEqdpFile(GenderRace genderRace, bool accessory)
=> _eqdpCache.TemporarilySetFiles(_manager, genderRace, accessory);
public CharacterUtility.MetaList.MetaReverter TemporarilySetGmpFile()
=> _gmpCache.TemporarilySetFiles(_manager);
public CharacterUtility.MetaList.MetaReverter TemporarilySetCmpFile()
=> _cmpCache.TemporarilySetFiles(_manager);
public CharacterUtility.MetaList.MetaReverter TemporarilySetEstFile(EstManipulation.EstType type)
=> _estCache.TemporarilySetFiles(_manager, type);
/// <summary> Try to obtain a manipulated IMC file. </summary>
public bool GetImcFile(Utf8GamePath path, [NotNullWhen(true)] out Meta.Files.ImcFile? file)
=> _imcCache.GetImcFile(path, out file);
/// <summary> Use this when CharacterUtility becomes ready. </summary>
private void ApplyStoredManipulations() private void ApplyStoredManipulations()
{ {
if (!Penumbra.CharacterUtility.Ready) if (!_manager.CharacterUtility.Ready)
return; return;
var loaded = 0; var loaded = 0;
@ -150,28 +203,8 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
: 0; : 0;
} }
if (_manager.IsDefault(_collection)) _manager.ApplyDefaultFiles(_collection);
{
SetFiles();
_manager.ResidentResources.Reload();
}
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations; _manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations."); Penumbra.Log.Debug($"{_collection.AnonymizedName}: Loaded {loaded} delayed meta manipulations.");
} }
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public unsafe void SetFile(MetaBaseFile? file, MetaIndex metaIndex)
{
if (file == null)
_manager.CharacterUtility.ResetResource(metaIndex);
else
_manager.CharacterUtility.SetResource(metaIndex, (IntPtr)file.Data, file.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public unsafe CharacterUtility.MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex)
=> file == null
? _manager.CharacterUtility.TemporarilyResetResource(metaIndex)
: _manager.CharacterUtility.TemporarilySetResource(metaIndex, (IntPtr)file.Data, file.Length);
} }

View file

@ -47,27 +47,6 @@ public partial class ModCollection
public FullPath? ResolvePath(Utf8GamePath path) public FullPath? ResolvePath(Utf8GamePath path)
=> _cache?.ResolvePath(path); => _cache?.ResolvePath(path);
// Force a file to be resolved to a specific path regardless of conflicts.
internal void ForceFile(Utf8GamePath path, FullPath fullPath)
{
if (CheckFullPath(path, fullPath))
_cache!.ResolvedFiles[path] = new ModPath(Mod.ForcedFiles, fullPath);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static bool CheckFullPath(Utf8GamePath path, FullPath fullPath)
{
if (fullPath.InternalName.Length < Utf8GamePath.MaxGamePathLength)
return true;
Penumbra.Log.Error($"The redirected path is too long to add the redirection\n\t{path}\n\t--> {fullPath}");
return false;
}
// Force a file resolve to be removed.
internal void RemoveFile(Utf8GamePath path)
=> _cache!.ResolvedFiles.Remove(path);
// Obtain data from the cache. // Obtain data from the cache.
internal MetaCache? MetaCache internal MetaCache? MetaCache
=> _cache?.MetaManipulations; => _cache?.MetaManipulations;
@ -135,3 +114,8 @@ public partial class ModCollection
=> _cache?.MetaManipulations.TemporarilySetEstFile(type) => _cache?.MetaManipulations.TemporarilySetEstFile(type)
?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type); ?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type);
} }
public static class CollectionCacheExtensions
{
}

View file

@ -12,148 +12,128 @@ namespace Penumbra.Import;
public partial class TexToolsMeta public partial class TexToolsMeta
{ {
// Deserialize and check Eqp Entries and add them to the list if they are non-default. // Deserialize and check Eqp Entries and add them to the list if they are non-default.
private void DeserializeEqpEntry( MetaFileInfo metaFileInfo, byte[]? data ) private void DeserializeEqpEntry(MetaFileInfo metaFileInfo, byte[]? data)
{ {
// Eqp can only be valid for equipment. // Eqp can only be valid for equipment.
if( data == null || !metaFileInfo.EquipSlot.IsEquipment() ) if (data == null || !metaFileInfo.EquipSlot.IsEquipment())
{
return; return;
}
var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data ); var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data);
var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot,
var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId ); metaFileInfo.PrimaryId);
if( _keepDefault || def.Entry != manip.Entry ) var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId);
{ if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add( manip ); MetaManipulations.Add(manip);
}
} }
// Deserialize and check Eqdp Entries and add them to the list if they are non-default. // Deserialize and check Eqdp Entries and add them to the list if they are non-default.
private void DeserializeEqdpEntries( MetaFileInfo metaFileInfo, byte[]? data ) private void DeserializeEqdpEntries(MetaFileInfo metaFileInfo, byte[]? data)
{
if( data == null )
{ {
if (data == null)
return; return;
}
var num = data.Length / 5; var num = data.Length / 5;
using var reader = new BinaryReader( new MemoryStream( data ) ); using var reader = new BinaryReader(new MemoryStream(data));
for( var i = 0; i < num; ++i ) for (var i = 0; i < num; ++i)
{ {
// Use the SE gender/race code. // Use the SE gender/race code.
var gr = ( GenderRace )reader.ReadUInt32(); var gr = (GenderRace)reader.ReadUInt32();
var byteValue = reader.ReadByte(); var byteValue = reader.ReadByte();
if( !gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory() ) if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory())
{
continue; continue;
}
var value = Eqdp.FromSlotAndBits( metaFileInfo.EquipSlot, ( byteValue & 1 ) == 1, ( byteValue & 2 ) == 2 ); var value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2);
var def = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId ), var def = new EqdpManipulation(
ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId),
metaFileInfo.EquipSlot, metaFileInfo.EquipSlot,
gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
var manip = new EqdpManipulation( value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId ); var manip = new EqdpManipulation(value, metaFileInfo.EquipSlot, gr.Split().Item1, gr.Split().Item2, metaFileInfo.PrimaryId);
if( _keepDefault || def.Entry != manip.Entry ) if (_keepDefault || def.Entry != manip.Entry)
{ MetaManipulations.Add(manip);
MetaManipulations.Add( manip );
}
} }
} }
// Deserialize and check Gmp Entries and add them to the list if they are non-default. // Deserialize and check Gmp Entries and add them to the list if they are non-default.
private void DeserializeGmpEntry( MetaFileInfo metaFileInfo, byte[]? data ) private void DeserializeGmpEntry(MetaFileInfo metaFileInfo, byte[]? data)
{
if( data == null )
{ {
if (data == null)
return; return;
}
using var reader = new BinaryReader( new MemoryStream( data ) ); using var reader = new BinaryReader(new MemoryStream(data));
var value = ( GmpEntry )reader.ReadUInt32(); var value = (GmpEntry)reader.ReadUInt32();
value.UnknownTotal = reader.ReadByte(); value.UnknownTotal = reader.ReadByte();
var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId ); var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
if( _keepDefault || value != def ) if (_keepDefault || value != def)
{ MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId));
MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) );
}
} }
// Deserialize and check Est Entries and add them to the list if they are non-default. // Deserialize and check Est Entries and add them to the list if they are non-default.
private void DeserializeEstEntries( MetaFileInfo metaFileInfo, byte[]? data ) private void DeserializeEstEntries(MetaFileInfo metaFileInfo, byte[]? data)
{
if( data == null )
{ {
if (data == null)
return; return;
}
var num = data.Length / 6; var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) ); using var reader = new BinaryReader(new MemoryStream(data));
for( var i = 0; i < num; ++i ) for (var i = 0; i < num; ++i)
{ {
var gr = ( GenderRace )reader.ReadUInt16(); var gr = (GenderRace)reader.ReadUInt16();
var id = reader.ReadUInt16(); var id = reader.ReadUInt16();
var value = reader.ReadUInt16(); var value = reader.ReadUInt16();
var type = ( metaFileInfo.SecondaryType, metaFileInfo.EquipSlot ) switch var type = (metaFileInfo.SecondaryType, metaFileInfo.EquipSlot) switch
{ {
(BodySlot.Face, _) => EstManipulation.EstType.Face, (BodySlot.Face, _) => EstManipulation.EstType.Face,
(BodySlot.Hair, _) => EstManipulation.EstType.Hair, (BodySlot.Hair, _) => EstManipulation.EstType.Hair,
(_, EquipSlot.Head) => EstManipulation.EstType.Head, (_, EquipSlot.Head) => EstManipulation.EstType.Head,
(_, EquipSlot.Body) => EstManipulation.EstType.Body, (_, EquipSlot.Body) => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0, _ => (EstManipulation.EstType)0,
}; };
if( !gr.IsValid() || type == 0 ) if (!gr.IsValid() || type == 0)
{
continue; continue;
}
var def = EstFile.GetDefault( type, gr, id ); var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
if( _keepDefault || def != value ) if (_keepDefault || def != value)
{ MetaManipulations.Add(new EstManipulation(gr.Split().Item1, gr.Split().Item2, type, id, value));
MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) );
}
} }
} }
// Deserialize and check IMC Entries and add them to the list if they are non-default. // Deserialize and check IMC Entries and add them to the list if they are non-default.
// This requires requesting a file from Lumina, which may fail due to TexTools corruption or just not existing. // This requires requesting a file from Lumina, which may fail due to TexTools corruption or just not existing.
// TexTools creates IMC files for off-hand weapon models which may not exist in the game files. // TexTools creates IMC files for off-hand weapon models which may not exist in the game files.
private void DeserializeImcEntries( MetaFileInfo metaFileInfo, byte[]? data ) private void DeserializeImcEntries(MetaFileInfo metaFileInfo, byte[]? data)
{
if( data == null )
{ {
if (data == null)
return; return;
}
var num = data.Length / 6; var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) ); using var reader = new BinaryReader(new MemoryStream(data));
var values = reader.ReadStructures< ImcEntry >( num ); var values = reader.ReadStructures<ImcEntry>(num);
ushort i = 0; ushort i = 0;
try try
{ {
var manip = new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot, var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId,
new ImcEntry() ); metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot,
var def = new ImcFile( manip ); new ImcEntry());
var partIdx = ImcFile.PartIndex( manip.EquipSlot ); // Gets turned to unknown for things without equip, and unknown turns to 0. var def = new ImcFile(_metaFileManager, manip);
foreach( var value in values ) var partIdx = ImcFile.PartIndex(manip.EquipSlot); // Gets turned to unknown for things without equip, and unknown turns to 0.
foreach (var value in values)
{ {
if( _keepDefault || !value.Equals( def.GetEntry( partIdx, i ) ) ) if (_keepDefault || !value.Equals(def.GetEntry(partIdx, i)))
{ {
var imc = new ImcManipulation( manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot, value ); var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot,
if( imc.Valid ) value);
{ if (imc.Valid)
MetaManipulations.Add( imc ); MetaManipulations.Add(imc);
}
} }
++i; ++i;
} }
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Warning( Penumbra.Log.Warning(
$"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n" $"Could not compute IMC manipulation for {metaFileInfo.PrimaryType} {metaFileInfo.PrimaryId}. This is in all likelihood due to TexTools corrupting your index files.\n"
+ $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}" ); + $"If the following error looks like Lumina is having trouble to read an IMC file, please do a do-over in TexTools:\n{e}");
} }
} }
} }

View file

@ -5,6 +5,7 @@ using System.Linq;
using System.Text; using System.Text;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -12,7 +13,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta public partial class TexToolsMeta
{ {
public static Dictionary< string, byte[] > ConvertToTexTools( IEnumerable< MetaManipulation > manips ) public static Dictionary< string, byte[] > ConvertToTexTools( MetaFileManager manager, IEnumerable< MetaManipulation > manips )
{ {
var ret = new Dictionary< string, byte[] >(); var ret = new Dictionary< string, byte[] >();
foreach( var group in manips.GroupBy( ManipToPath ) ) foreach( var group in manips.GroupBy( ManipToPath ) )
@ -23,8 +24,8 @@ public partial class TexToolsMeta
} }
var bytes = group.Key.EndsWith( ".rgsp" ) var bytes = group.Key.EndsWith( ".rgsp" )
? WriteRgspFile( group.Key, group ) ? WriteRgspFile( manager, group.Key, group )
: WriteMetaFile( group.Key, group ); : WriteMetaFile( manager, group.Key, group );
if( bytes.Length == 0 ) if( bytes.Length == 0 )
{ {
continue; continue;
@ -36,7 +37,7 @@ public partial class TexToolsMeta
return ret; return ret;
} }
private static byte[] WriteRgspFile( string path, IEnumerable< MetaManipulation > manips ) private static byte[] WriteRgspFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips )
{ {
var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp ); var list = manips.GroupBy( m => m.Rsp.Attribute ).ToDictionary( m => m.Key, m => m.Last().Rsp );
using var m = new MemoryStream( 45 ); using var m = new MemoryStream( 45 );
@ -54,7 +55,7 @@ public partial class TexToolsMeta
{ {
foreach( var attribute in attributes ) foreach( var attribute in attributes )
{ {
var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( race, attribute ); var value = list.TryGetValue( attribute, out var tmp ) ? tmp.Entry : CmpFile.GetDefault( manager, race, attribute );
b.Write( value ); b.Write( value );
} }
} }
@ -72,7 +73,7 @@ public partial class TexToolsMeta
return m.GetBuffer(); return m.GetBuffer();
} }
private static byte[] WriteMetaFile( string path, IEnumerable< MetaManipulation > manips ) private static byte[] WriteMetaFile( MetaFileManager manager, string path, IEnumerable< MetaManipulation > manips )
{ {
var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) ); var filteredManips = manips.GroupBy( m => m.ManipulationType ).ToDictionary( p => p.Key, p => p.Select( x => x ) );
@ -103,7 +104,7 @@ public partial class TexToolsMeta
b.Write( ( uint )header ); b.Write( ( uint )header );
b.Write( offset ); b.Write( offset );
var size = WriteData( b, offset, header, data ); var size = WriteData( manager, b, offset, header, data );
b.Write( size ); b.Write( size );
offset += size; offset += size;
} }
@ -111,7 +112,7 @@ public partial class TexToolsMeta
return m.ToArray(); return m.ToArray();
} }
private static uint WriteData( BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips ) private static uint WriteData( MetaFileManager manager, BinaryWriter b, uint offset, MetaManipulation.Type type, IEnumerable< MetaManipulation > manips )
{ {
var oldPos = b.BaseStream.Position; var oldPos = b.BaseStream.Position;
b.Seek( ( int )offset, SeekOrigin.Begin ); b.Seek( ( int )offset, SeekOrigin.Begin );
@ -120,7 +121,7 @@ public partial class TexToolsMeta
{ {
case MetaManipulation.Type.Imc: case MetaManipulation.Type.Imc:
var allManips = manips.ToList(); var allManips = manips.ToList();
var baseFile = new ImcFile( allManips[ 0 ].Imc ); var baseFile = new ImcFile( manager, allManips[ 0 ].Imc );
foreach( var manip in allManips ) foreach( var manip in allManips )
{ {
manip.Imc.Apply( baseFile ); manip.Imc.Apply( baseFile );

View file

@ -1,6 +1,7 @@
using System; using System;
using System.IO; using System.IO;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
@ -9,7 +10,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta public partial class TexToolsMeta
{ {
// Parse a single rgsp file. // Parse a single rgsp file.
public static TexToolsMeta FromRgspFile( string filePath, byte[] data, bool keepDefault ) public static TexToolsMeta FromRgspFile( MetaFileManager manager, string filePath, byte[] data, bool keepDefault )
{ {
if( data.Length != 45 && data.Length != 42 ) if( data.Length != 45 && data.Length != 42 )
{ {
@ -25,7 +26,7 @@ public partial class TexToolsMeta
var flag = br.ReadByte(); var flag = br.ReadByte();
var version = flag != 255 ? ( uint )1 : br.ReadUInt16(); var version = flag != 255 ? ( uint )1 : br.ReadUInt16();
var ret = new TexToolsMeta( filePath, version ); var ret = new TexToolsMeta( manager, filePath, version );
// SubRace is offset by one due to Unknown. // SubRace is offset by one due to Unknown.
var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 ); var subRace = ( SubRace )( version == 1 ? flag + 1 : br.ReadByte() + 1 );
@ -46,7 +47,7 @@ public partial class TexToolsMeta
// Add the given values to the manipulations if they are not default. // Add the given values to the manipulations if they are not default.
void Add( RspAttribute attribute, float value ) void Add( RspAttribute attribute, float value )
{ {
var def = CmpFile.GetDefault( subRace, attribute ); var def = CmpFile.GetDefault( manager, subRace, attribute );
if( keepDefault || value != def ) if( keepDefault || value != def )
{ {
ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) ); ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) );

View file

@ -4,34 +4,37 @@ using System.IO;
using System.Text; using System.Text;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Import.Structs; using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
namespace Penumbra.Import; namespace Penumbra.Import;
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to /// <summary>TexTools provices custom generated *.meta files for its modpacks, that contain changes to
// - imc files /// - imc files
// - eqp files /// - eqp files
// - gmp files /// - gmp files
// - est files /// - est files
// - eqdp files /// - eqdp files
// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes. /// made by the mod. The filename determines to what the changes are applied, and the binary file itself contains changes.
// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json. /// We parse every *.meta file in a mod and combine all actual changes that do not keep data on default values and that can be applied to the game in a .json.
// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored. /// TexTools may also generate files that contain non-existing changes, e.g. *.imc files for weapon offhands, which will be ignored.
// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file. /// TexTools also provides .rgsp files, that contain changes to the racial scaling parameters in the human.cmp file.</summary>
public partial class TexToolsMeta public partial class TexToolsMeta
{ {
// An empty TexToolsMeta. /// <summary> An empty TexToolsMeta. </summary>
public static readonly TexToolsMeta Invalid = new(string.Empty, 0); public static readonly TexToolsMeta Invalid = new(null!, string.Empty, 0);
// The info class determines the files or table locations the changes need to apply to from the filename. // The info class determines the files or table locations the changes need to apply to from the filename.
public readonly uint Version; public readonly uint Version;
public readonly string FilePath; public readonly string FilePath;
public readonly List< MetaManipulation > MetaManipulations = new(); public readonly List< MetaManipulation > MetaManipulations = new();
private readonly bool _keepDefault = false; private readonly bool _keepDefault = false;
public TexToolsMeta( IGamePathParser parser, byte[] data, bool keepDefault ) private readonly MetaFileManager _metaFileManager;
public TexToolsMeta( MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault )
{ {
_metaFileManager = metaFileManager;
_keepDefault = keepDefault; _keepDefault = keepDefault;
try try
{ {
@ -80,8 +83,9 @@ public partial class TexToolsMeta
} }
} }
private TexToolsMeta( string filePath, uint version ) private TexToolsMeta( MetaFileManager metaFileManager, string filePath, uint version )
{ {
_metaFileManager = metaFileManager;
FilePath = filePath; FilePath = filePath;
Version = version; Version = version;
} }

View file

@ -1,37 +0,0 @@
using System;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using Penumbra.GameData;
namespace Penumbra.Interop.Services;
public unsafe class MetaFileManager
{
public MetaFileManager()
{
SignatureHelper.Initialise(this);
}
// Allocate in the games space for file storage.
// We only need this if using any meta file.
[Signature(Sigs.GetFileSpace)]
private readonly IntPtr _getFileSpaceAddress = IntPtr.Zero;
public IMemorySpace* GetFileSpace()
=> ((delegate* unmanaged<IMemorySpace*>)_getFileSpaceAddress)();
public void* AllocateFileMemory(ulong length, ulong alignment = 0)
=> GetFileSpace()->Malloc(length, alignment);
public void* AllocateFileMemory(int length, int alignment = 0)
=> AllocateFileMemory((ulong)length, (ulong)alignment);
public void* AllocateDefaultMemory(ulong length, ulong alignment = 0)
=> GetFileSpace()->Malloc(length, alignment);
public void* AllocateDefaultMemory(int length, int alignment = 0)
=> IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment);
public void Free(IntPtr ptr, int length)
=> IMemorySpace.Free((void*)ptr, (ulong)length);
}

View file

@ -8,46 +8,46 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
// The human.cmp file contains many character-relevant parameters like color sets. /// <summary>
// We only support manipulating the racial scaling parameters at the moment. /// The human.cmp file contains many character-relevant parameters like color sets.
/// We only support manipulating the racial scaling parameters at the moment.
/// </summary>
public sealed unsafe class CmpFile : MetaBaseFile public sealed unsafe class CmpFile : MetaBaseFile
{ {
public static readonly CharacterUtility.InternalIndex InternalIndex = public static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[ ( int )MetaIndex.HumanCmp ]; CharacterUtility.ReverseIndices[(int)MetaIndex.HumanCmp];
private const int RacialScalingStart = 0x2A800; private const int RacialScalingStart = 0x2A800;
public float this[ SubRace subRace, RspAttribute attribute ] public float this[SubRace subRace, RspAttribute attribute]
{ {
get => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4);
set => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ) = value; set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4) = value;
} }
public override void Reset() public override void Reset()
=> MemoryUtility.MemCpyUnchecked( Data, ( byte* )DefaultData.Data, DefaultData.Length ); => MemoryUtility.MemCpyUnchecked(Data, (byte*)DefaultData.Data, DefaultData.Length);
public void Reset( IEnumerable< (SubRace, RspAttribute) > entries ) public void Reset(IEnumerable<(SubRace, RspAttribute)> entries)
{ {
foreach( var (r, a) in entries ) foreach (var (r, a) in entries)
{ this[r, a] = GetDefault(Manager, r, a);
this[ r, a ] = GetDefault( r, a );
}
} }
public CmpFile() public CmpFile(MetaFileManager manager)
: base( MetaIndex.HumanCmp ) : base(manager, MetaIndex.HumanCmp)
{ {
AllocateData( DefaultData.Length ); AllocateData(DefaultData.Length);
Reset(); Reset();
} }
public static float GetDefault( SubRace subRace, RspAttribute attribute ) public static float GetDefault(MetaFileManager manager, SubRace subRace, RspAttribute attribute)
{ {
var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( InternalIndex ).Address; var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address;
return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ); return *(float*)(data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4);
} }
private static int ToRspIndex( SubRace subRace ) private static int ToRspIndex(SubRace subRace)
=> subRace switch => subRace switch
{ {
SubRace.Midlander => 0, SubRace.Midlander => 0,
@ -67,6 +67,6 @@ public sealed unsafe class CmpFile : MetaBaseFile
SubRace.Rava => 70, SubRace.Rava => 70,
SubRace.Veena => 71, SubRace.Veena => 71,
SubRace.Unknown => 0, SubRace.Unknown => 0,
_ => throw new ArgumentOutOfRangeException( nameof( subRace ), subRace, null ), _ => throw new ArgumentOutOfRangeException(nameof(subRace), subRace, null),
}; };
} }

View file

@ -8,14 +8,15 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
// EQDP file structure: /// <summary>
// [Identifier][BlockSize:ushort][BlockCount:ushort] /// EQDP file structure:
// BlockCount x [BlockHeader:ushort] /// [Identifier][BlockSize:ushort][BlockCount:ushort]
// Containing offsets for blocks, ushort.Max means collapsed. /// BlockCount x [BlockHeader:ushort]
// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2. /// Containing offsets for blocks, ushort.Max means collapsed.
// ExpandedBlockCount x [Entry] /// Offsets are based on the end of the header, so 0 means IdentifierSize + 4 + BlockCount x 2.
/// ExpandedBlockCount x [Entry]
// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it. /// Expanded Eqdp File just expands all blocks for easy read and write access to single entries and to keep the same memory for it.
/// </summary>
public sealed unsafe class ExpandedEqdpFile : MetaBaseFile public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
{ {
private const ushort BlockHeaderSize = 2; private const ushort BlockHeaderSize = 2;
@ -28,117 +29,103 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
public readonly int DataOffset; public readonly int DataOffset;
public ushort Identifier public ushort Identifier
=> *( ushort* )Data; => *(ushort*)Data;
public ushort BlockSize public ushort BlockSize
=> *( ushort* )( Data + 2 ); => *(ushort*)(Data + 2);
public ushort BlockCount public ushort BlockCount
=> *( ushort* )( Data + 4 ); => *(ushort*)(Data + 4);
public int Count public int Count
=> ( Length - DataOffset ) / EqdpEntrySize; => (Length - DataOffset) / EqdpEntrySize;
public EqdpEntry this[ int idx ] public EqdpEntry this[int idx]
{ {
get get
{ {
if( idx >= Count || idx < 0 ) if (idx >= Count || idx < 0)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
}
return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) ); return (EqdpEntry)(*(ushort*)(Data + DataOffset + EqdpEntrySize * idx));
} }
set set
{ {
if( idx >= Count || idx < 0 ) if (idx >= Count || idx < 0)
{
throw new IndexOutOfRangeException(); throw new IndexOutOfRangeException();
}
*( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) = ( ushort )value; *(ushort*)(Data + DataOffset + EqdpEntrySize * idx) = (ushort)value;
} }
} }
public override void Reset() public override void Reset()
{ {
var def = ( byte* )DefaultData.Data; var def = (byte*)DefaultData.Data;
MemoryUtility.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize ); MemoryUtility.MemCpyUnchecked(Data, def, IdentifierSize + PreambleSize);
var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize ); var controlPtr = (ushort*)(def + IdentifierSize + PreambleSize);
var dataBasePtr = controlPtr + BlockCount; var dataBasePtr = controlPtr + BlockCount;
var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount ); var myDataPtr = (ushort*)(Data + IdentifierSize + PreambleSize + 2 * BlockCount);
var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize ); var myControlPtr = (ushort*)(Data + IdentifierSize + PreambleSize);
for( var i = 0; i < BlockCount; ++i ) for (var i = 0; i < BlockCount; ++i)
{ {
if( controlPtr[ i ] == CollapsedBlock ) if (controlPtr[i] == CollapsedBlock)
{ MemoryUtility.MemSet(myDataPtr, 0, BlockSize * EqdpEntrySize);
MemoryUtility.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize );
}
else else
{ MemoryUtility.MemCpyUnchecked(myDataPtr, dataBasePtr + controlPtr[i], BlockSize * EqdpEntrySize);
MemoryUtility.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize );
}
myControlPtr[ i ] = ( ushort )( i * BlockSize ); myControlPtr[i] = (ushort)(i * BlockSize);
myDataPtr += BlockSize; myDataPtr += BlockSize;
} }
MemoryUtility.MemSet( myDataPtr, 0, Length - ( int )( ( byte* )myDataPtr - Data ) ); MemoryUtility.MemSet(myDataPtr, 0, Length - (int)((byte*)myDataPtr - Data));
} }
public void Reset( IEnumerable< int > entries ) public void Reset(IEnumerable<int> entries)
{ {
foreach( var entry in entries ) foreach (var entry in entries)
{ this[entry] = GetDefault(entry);
this[ entry ] = GetDefault( entry );
}
} }
public ExpandedEqdpFile( GenderRace raceCode, bool accessory ) public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory)
: base( CharacterUtilityData.EqdpIdx( raceCode, accessory ) ) : base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory))
{ {
var def = ( byte* )DefaultData.Data; var def = (byte*)DefaultData.Data;
var blockSize = *( ushort* )( def + IdentifierSize ); var blockSize = *(ushort*)(def + IdentifierSize);
var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 ); var totalBlockCount = *(ushort*)(def + IdentifierSize + 2);
var totalBlockSize = blockSize * EqdpEntrySize; var totalBlockSize = blockSize * EqdpEntrySize;
DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize; DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize;
var fullLength = DataOffset + totalBlockCount * totalBlockSize; var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += ( FileAlignment - ( fullLength & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 ); fullLength += (FileAlignment - (fullLength & (FileAlignment - 1))) & (FileAlignment - 1);
AllocateData( fullLength ); AllocateData(fullLength);
Reset(); Reset();
} }
public EqdpEntry GetDefault( int setIdx ) public EqdpEntry GetDefault(int setIdx)
=> GetDefault( Index, setIdx ); => GetDefault(Manager, Index, setIdx);
public static EqdpEntry GetDefault( CharacterUtility.InternalIndex idx, int setIdx ) public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, int setIdx)
=> GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( idx ).Address, setIdx ); => GetDefault((byte*)manager.CharacterUtility.DefaultResource(idx).Address, setIdx);
public static EqdpEntry GetDefault( byte* data, int setIdx ) public static EqdpEntry GetDefault(byte* data, int setIdx)
{ {
var blockSize = *( ushort* )( data + IdentifierSize ); var blockSize = *(ushort*)(data + IdentifierSize);
var totalBlockCount = *( ushort* )( data + IdentifierSize + 2 ); var totalBlockCount = *(ushort*)(data + IdentifierSize + 2);
var blockIdx = setIdx / blockSize; var blockIdx = setIdx / blockSize;
if( blockIdx >= totalBlockCount ) if (blockIdx >= totalBlockCount)
{
return 0; return 0;
}
var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ]; var block = ((ushort*)(data + IdentifierSize + PreambleSize))[blockIdx];
if( block == CollapsedBlock ) if (block == CollapsedBlock)
{
return 0; return 0;
var blockData = (ushort*)(data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2);
return (EqdpEntry)(*(blockData + setIdx % blockSize));
} }
var blockData = ( ushort* )( data + IdentifierSize + PreambleSize + totalBlockCount * 2 + block * 2 ); public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, int setIdx)
return ( EqdpEntry )( *( blockData + setIdx % blockSize ) ); => GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setIdx);
}
public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx )
=> GetDefault( CharacterUtility.ReverseIndices[ ( int )CharacterUtilityData.EqdpIdx( raceCode, accessory ) ], setIdx );
} }

View file

@ -9,11 +9,13 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
// EQP/GMP Structure: /// <summary>
// 64 x [Block collapsed or not bit] /// EQP/GMP Structure:
// 159 x [EquipmentParameter:ulong] /// 64 x [Block collapsed or not bit]
// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong] /// 159 x [EquipmentParameter:ulong]
// Item 0 does not exist and is sent to Item 1 instead. /// (CountSetBits(Block Collapsed or not) - 1) x 160 x [EquipmentParameter:ulong]
/// Item 0 does not exist and is sent to Item 1 instead.
/// </summary>
public unsafe class ExpandedEqpGmpBase : MetaBaseFile public unsafe class ExpandedEqpGmpBase : MetaBaseFile
{ {
protected const int BlockSize = 160; protected const int BlockSize = 160;
@ -24,19 +26,19 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
public const int Count = BlockSize * NumBlocks; public const int Count = BlockSize * NumBlocks;
public ulong ControlBlock public ulong ControlBlock
=> *( ulong* )Data; => *(ulong*)Data;
protected ulong GetInternal( int idx ) protected ulong GetInternal(int idx)
{ {
return idx switch return idx switch
{ {
>= Count => throw new IndexOutOfRangeException(), >= Count => throw new IndexOutOfRangeException(),
<= 1 => *( ( ulong* )Data + 1 ), <= 1 => *((ulong*)Data + 1),
_ => *( ( ulong* )Data + idx ), _ => *((ulong*)Data + idx),
}; };
} }
protected void SetInternal( int idx, ulong value ) protected void SetInternal(int idx, ulong value)
{ {
idx = idx switch idx = idx switch
{ {
@ -45,67 +47,62 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
_ => idx, _ => idx,
}; };
*( ( ulong* )Data + idx ) = value; *((ulong*)Data + idx) = value;
} }
protected virtual void SetEmptyBlock( int idx ) protected virtual void SetEmptyBlock(int idx)
{ {
MemoryUtility.MemSet( Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize ); MemoryUtility.MemSet(Data + idx * BlockSize * EntrySize, 0, BlockSize * EntrySize);
} }
public sealed override void Reset() public sealed override void Reset()
{ {
var ptr = ( byte* )DefaultData.Data; var ptr = (byte*)DefaultData.Data;
var controlBlock = *( ulong* )ptr; var controlBlock = *(ulong*)ptr;
var expandedBlocks = 0; var expandedBlocks = 0;
for( var i = 0; i < NumBlocks; ++i ) for (var i = 0; i < NumBlocks; ++i)
{ {
var collapsed = ( ( controlBlock >> i ) & 1 ) == 0; var collapsed = ((controlBlock >> i) & 1) == 0;
if( !collapsed ) if (!collapsed)
{ {
MemoryUtility.MemCpyUnchecked( Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize, BlockSize * EntrySize ); MemoryUtility.MemCpyUnchecked(Data + i * BlockSize * EntrySize, ptr + expandedBlocks * BlockSize * EntrySize,
BlockSize * EntrySize);
expandedBlocks++; expandedBlocks++;
} }
else else
{ {
SetEmptyBlock( i ); SetEmptyBlock(i);
} }
} }
*( ulong* )Data = ulong.MaxValue; *(ulong*)Data = ulong.MaxValue;
} }
public ExpandedEqpGmpBase( bool gmp ) public ExpandedEqpGmpBase(MetaFileManager manager, bool gmp)
: base( gmp ? MetaIndex.Gmp : MetaIndex.Eqp ) : base(manager, gmp ? MetaIndex.Gmp : MetaIndex.Eqp)
{ {
AllocateData( MaxSize ); AllocateData(MaxSize);
Reset(); Reset();
} }
protected static ulong GetDefaultInternal( CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def ) protected static ulong GetDefaultInternal(MetaFileManager manager, CharacterUtility.InternalIndex fileIndex, int setIdx, ulong def)
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResource(fileIndex).Address;
if( setIdx == 0 )
{ {
var data = (byte*)manager.CharacterUtility.DefaultResource(fileIndex).Address;
if (setIdx == 0)
setIdx = 1; setIdx = 1;
}
var blockIdx = setIdx / BlockSize; var blockIdx = setIdx / BlockSize;
if( blockIdx >= NumBlocks ) if (blockIdx >= NumBlocks)
{
return def; return def;
}
var control = *( ulong* )data; var control = *(ulong*)data;
var blockBit = 1ul << blockIdx; var blockBit = 1ul << blockIdx;
if( ( control & blockBit ) == 0 ) if ((control & blockBit) == 0)
{
return def; return def;
}
var count = BitOperations.PopCount( control & ( blockBit - 1 ) ); var count = BitOperations.PopCount(control & (blockBit - 1));
var idx = setIdx % BlockSize; var idx = setIdx % BlockSize;
var ptr = ( ulong* )data + BlockSize * count + idx; var ptr = (ulong*)data + BlockSize * count + idx;
return *ptr; return *ptr;
} }
} }
@ -113,44 +110,40 @@ public unsafe class ExpandedEqpGmpBase : MetaBaseFile
public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable<EqpEntry> public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable<EqpEntry>
{ {
public static readonly CharacterUtility.InternalIndex InternalIndex = public static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[ (int) MetaIndex.Eqp ]; CharacterUtility.ReverseIndices[(int)MetaIndex.Eqp];
public ExpandedEqpFile() public ExpandedEqpFile(MetaFileManager manager)
: base( false ) : base(manager, false)
{ } { }
public EqpEntry this[ int idx ] public EqpEntry this[int idx]
{ {
get => ( EqpEntry )GetInternal( idx ); get => (EqpEntry)GetInternal(idx);
set => SetInternal( idx, ( ulong )value ); set => SetInternal(idx, (ulong)value);
} }
public static EqpEntry GetDefault( int setIdx ) public static EqpEntry GetDefault(MetaFileManager manager, int setIdx)
=> ( EqpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )Eqp.DefaultEntry ); => (EqpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)Eqp.DefaultEntry);
protected override unsafe void SetEmptyBlock( int idx ) protected override unsafe void SetEmptyBlock(int idx)
{ {
var blockPtr = ( ulong* )( Data + idx * BlockSize * EntrySize ); var blockPtr = (ulong*)(Data + idx * BlockSize * EntrySize);
var endPtr = blockPtr + BlockSize; var endPtr = blockPtr + BlockSize;
for( var ptr = blockPtr; ptr < endPtr; ++ptr ) for (var ptr = blockPtr; ptr < endPtr; ++ptr)
{ *ptr = (ulong)Eqp.DefaultEntry;
*ptr = ( ulong )Eqp.DefaultEntry;
}
} }
public void Reset( IEnumerable< int > entries ) public void Reset(IEnumerable<int> entries)
{ {
foreach( var entry in entries ) foreach (var entry in entries)
{ this[entry] = GetDefault(Manager, entry);
this[ entry ] = GetDefault( entry );
}
} }
public IEnumerator< EqpEntry > GetEnumerator() public IEnumerator<EqpEntry> GetEnumerator()
{ {
for( var idx = 1; idx < Count; ++idx ) for (var idx = 1; idx < Count; ++idx)
yield return this[ idx ]; yield return this[idx];
} }
IEnumerator IEnumerable.GetEnumerator() IEnumerator IEnumerable.GetEnumerator()
@ -160,32 +153,30 @@ public sealed class ExpandedEqpFile : ExpandedEqpGmpBase, IEnumerable<EqpEntry>
public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable<GmpEntry> public sealed class ExpandedGmpFile : ExpandedEqpGmpBase, IEnumerable<GmpEntry>
{ {
public static readonly CharacterUtility.InternalIndex InternalIndex = public static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[( int )MetaIndex.Gmp]; CharacterUtility.ReverseIndices[(int)MetaIndex.Gmp];
public ExpandedGmpFile() public ExpandedGmpFile(MetaFileManager manager)
: base( true ) : base(manager, true)
{ } { }
public GmpEntry this[ int idx ] public GmpEntry this[int idx]
{ {
get => ( GmpEntry )GetInternal( idx ); get => (GmpEntry)GetInternal(idx);
set => SetInternal( idx, ( ulong )value ); set => SetInternal(idx, (ulong)value);
} }
public static GmpEntry GetDefault( int setIdx ) public static GmpEntry GetDefault(MetaFileManager manager, int setIdx)
=> ( GmpEntry )GetDefaultInternal( InternalIndex, setIdx, ( ulong )GmpEntry.Default ); => (GmpEntry)GetDefaultInternal(manager, InternalIndex, setIdx, (ulong)GmpEntry.Default);
public void Reset( IEnumerable< int > entries ) public void Reset(IEnumerable<int> entries)
{ {
foreach( var entry in entries ) foreach (var entry in entries)
{ this[entry] = GetDefault(Manager, entry);
this[ entry ] = GetDefault( entry );
}
} }
public IEnumerator<GmpEntry> GetEnumerator() public IEnumerator<GmpEntry> GetEnumerator()
{ {
for( var idx = 1; idx < Count; ++idx ) for (var idx = 1; idx < Count; ++idx)
yield return this[idx]; yield return this[idx];
} }

View file

@ -8,11 +8,13 @@ using Penumbra.String.Functions;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
// EST Structure: /// <summary>
// 1x [NumEntries : UInt32] /// EST Structure:
// Apparently entries need to be sorted. /// 1x [NumEntries : UInt32]
// #NumEntries x [SetId : UInt16] [RaceId : UInt16] /// Apparently entries need to be sorted.
// #NumEntries x [SkeletonId : UInt16] /// #NumEntries x [SetId : UInt16] [RaceId : UInt16]
/// #NumEntries x [SkeletonId : UInt16]
/// </summary>
public sealed unsafe class EstFile : MetaBaseFile public sealed unsafe class EstFile : MetaBaseFile
{ {
private const ushort EntryDescSize = 4; private const ushort EntryDescSize = 4;
@ -20,10 +22,10 @@ public sealed unsafe class EstFile : MetaBaseFile
private const int IncreaseSize = 512; private const int IncreaseSize = 512;
public int Count public int Count
=> *( int* )Data; => *(int*)Data;
private int Size private int Size
=> 4 + Count * ( EntryDescSize + EntrySize ); => 4 + Count * (EntryDescSize + EntrySize);
public enum EstEntryChange public enum EstEntryChange
{ {
@ -33,176 +35,154 @@ public sealed unsafe class EstFile : MetaBaseFile
Removed, Removed,
} }
public ushort this[ GenderRace genderRace, ushort setId ] public ushort this[GenderRace genderRace, ushort setId]
{ {
get get
{ {
var (idx, exists) = FindEntry( genderRace, setId ); var (idx, exists) = FindEntry(genderRace, setId);
if( !exists ) if (!exists)
{
return 0; return 0;
return *(ushort*)(Data + EntryDescSize * (Count + 1) + EntrySize * idx);
}
set => SetEntry(genderRace, setId, value);
} }
return *( ushort* )( Data + EntryDescSize * ( Count + 1 ) + EntrySize * idx ); private void InsertEntry(int idx, GenderRace genderRace, ushort setId, ushort skeletonId)
}
set => SetEntry( genderRace, setId, value );
}
private void InsertEntry( int idx, GenderRace genderRace, ushort setId, ushort skeletonId )
{ {
if( Length < Size + EntryDescSize + EntrySize ) if (Length < Size + EntryDescSize + EntrySize)
ResizeResources(Length + IncreaseSize);
var control = (Info*)(Data + 4);
var entries = (ushort*)(control + Count);
for (var i = Count - 1; i >= idx; --i)
entries[i + 3] = entries[i];
entries[idx + 2] = skeletonId;
for (var i = idx - 1; i >= 0; --i)
entries[i + 2] = entries[i];
for (var i = Count - 1; i >= idx; --i)
control[i + 1] = control[i];
control[idx] = new Info(genderRace, setId);
*(int*)Data = Count + 1;
}
private void RemoveEntry(int idx)
{ {
ResizeResources( Length + IncreaseSize ); var control = (Info*)(Data + 4);
var entries = (ushort*)(control + Count);
for (var i = idx; i < Count; ++i)
control[i] = control[i + 1];
for (var i = 0; i < idx; ++i)
entries[i - 2] = entries[i];
for (var i = idx; i < Count - 1; ++i)
entries[i - 2] = entries[i + 1];
entries[Count - 3] = 0;
entries[Count - 2] = 0;
entries[Count - 1] = 0;
*(int*)Data = Count - 1;
} }
var control = ( Info* )( Data + 4 ); [StructLayout(LayoutKind.Sequential, Size = 4)]
var entries = ( ushort* )( control + Count ); private struct Info : IComparable<Info>
for( var i = Count - 1; i >= idx; --i )
{
entries[ i + 3 ] = entries[ i ];
}
entries[ idx + 2 ] = skeletonId;
for( var i = idx - 1; i >= 0; --i )
{
entries[ i + 2 ] = entries[ i ];
}
for( var i = Count - 1; i >= idx; --i )
{
control[ i + 1 ] = control[ i ];
}
control[ idx ] = new Info( genderRace, setId );
*( int* )Data = Count + 1;
}
private void RemoveEntry( int idx )
{
var control = ( Info* )( Data + 4 );
var entries = ( ushort* )( control + Count );
for( var i = idx; i < Count; ++i )
{
control[ i ] = control[ i + 1 ];
}
for( var i = 0; i < idx; ++i )
{
entries[ i - 2 ] = entries[ i ];
}
for( var i = idx; i < Count - 1; ++i )
{
entries[ i - 2 ] = entries[ i + 1 ];
}
entries[ Count - 3 ] = 0;
entries[ Count - 2 ] = 0;
entries[ Count - 1 ] = 0;
*( int* )Data = Count - 1;
}
[StructLayout( LayoutKind.Sequential, Size = 4 )]
private struct Info : IComparable< Info >
{ {
public readonly ushort SetId; public readonly ushort SetId;
public readonly GenderRace GenderRace; public readonly GenderRace GenderRace;
public Info( GenderRace gr, ushort setId ) public Info(GenderRace gr, ushort setId)
{ {
GenderRace = gr; GenderRace = gr;
SetId = setId; SetId = setId;
} }
public int CompareTo( Info other ) public int CompareTo(Info other)
{ {
var genderRaceComparison = GenderRace.CompareTo( other.GenderRace ); var genderRaceComparison = GenderRace.CompareTo(other.GenderRace);
return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo( other.SetId ); return genderRaceComparison != 0 ? genderRaceComparison : SetId.CompareTo(other.SetId);
} }
} }
private static (int, bool) FindEntry( ReadOnlySpan< Info > data, GenderRace genderRace, ushort setId ) private static (int, bool) FindEntry(ReadOnlySpan<Info> data, GenderRace genderRace, ushort setId)
{ {
var idx = data.BinarySearch( new Info( genderRace, setId ) ); var idx = data.BinarySearch(new Info(genderRace, setId));
return idx < 0 ? ( ~idx, false ) : ( idx, true ); return idx < 0 ? (~idx, false) : (idx, true);
} }
private (int, bool) FindEntry( GenderRace genderRace, ushort setId ) private (int, bool) FindEntry(GenderRace genderRace, ushort setId)
{ {
var span = new ReadOnlySpan< Info >( Data + 4, Count ); var span = new ReadOnlySpan<Info>(Data + 4, Count);
return FindEntry( span, genderRace, setId ); return FindEntry(span, genderRace, setId);
} }
public EstEntryChange SetEntry( GenderRace genderRace, ushort setId, ushort skeletonId ) public EstEntryChange SetEntry(GenderRace genderRace, ushort setId, ushort skeletonId)
{ {
var (idx, exists) = FindEntry( genderRace, setId ); var (idx, exists) = FindEntry(genderRace, setId);
if( exists ) if (exists)
{
var value = *( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx );
if( value == skeletonId )
{ {
var value = *(ushort*)(Data + 4 * (Count + 1) + 2 * idx);
if (value == skeletonId)
return EstEntryChange.Unchanged; return EstEntryChange.Unchanged;
}
if( skeletonId == 0 ) if (skeletonId == 0)
{ {
RemoveEntry( idx ); RemoveEntry(idx);
return EstEntryChange.Removed; return EstEntryChange.Removed;
} }
*( ushort* )( Data + 4 * ( Count + 1 ) + 2 * idx ) = skeletonId; *(ushort*)(Data + 4 * (Count + 1) + 2 * idx) = skeletonId;
return EstEntryChange.Changed; return EstEntryChange.Changed;
} }
if( skeletonId == 0 ) if (skeletonId == 0)
{
return EstEntryChange.Unchanged; return EstEntryChange.Unchanged;
}
InsertEntry( idx, genderRace, setId, skeletonId ); InsertEntry(idx, genderRace, setId, skeletonId);
return EstEntryChange.Added; return EstEntryChange.Added;
} }
public override void Reset() public override void Reset()
{ {
var (d, length) = DefaultData; var (d, length) = DefaultData;
var data = ( byte* )d; var data = (byte*)d;
MemoryUtility.MemCpyUnchecked( Data, data, length ); MemoryUtility.MemCpyUnchecked(Data, data, length);
MemoryUtility.MemSet( Data + length, 0, Length - length ); MemoryUtility.MemSet(Data + length, 0, Length - length);
} }
public EstFile( EstManipulation.EstType estType ) public EstFile(MetaFileManager manager, EstManipulation.EstType estType)
: base( ( MetaIndex )estType ) : base(manager, (MetaIndex)estType)
{ {
var length = DefaultData.Length; var length = DefaultData.Length;
AllocateData( length + IncreaseSize ); AllocateData(length + IncreaseSize);
Reset(); Reset();
} }
public ushort GetDefault( GenderRace genderRace, ushort setId ) public ushort GetDefault(GenderRace genderRace, ushort setId)
=> GetDefault( Index, genderRace, setId ); => GetDefault(Manager, Index, genderRace, setId);
public static ushort GetDefault( CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId ) public static ushort GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex index, GenderRace genderRace, ushort setId)
{
var data = ( byte* )Penumbra.CharacterUtility.DefaultResource( index ).Address;
var count = *( int* )data;
var span = new ReadOnlySpan< Info >( data + 4, count );
var (idx, found) = FindEntry( span, genderRace, setId );
if( !found )
{ {
var data = (byte*)manager.CharacterUtility.DefaultResource(index).Address;
var count = *(int*)data;
var span = new ReadOnlySpan<Info>(data + 4, count);
var (idx, found) = FindEntry(span, genderRace, setId);
if (!found)
return 0; return 0;
return *(ushort*)(data + 4 + count * EntryDescSize + idx * EntrySize);
} }
return *( ushort* )( data + 4 + count * EntryDescSize + idx * EntrySize ); public static ushort GetDefault(MetaFileManager manager, MetaIndex metaIndex, GenderRace genderRace, ushort setId)
} => GetDefault(manager, CharacterUtility.ReverseIndices[(int)metaIndex], genderRace, setId);
public static ushort GetDefault( MetaIndex metaIndex, GenderRace genderRace, ushort setId ) public static ushort GetDefault(MetaFileManager manager, EstManipulation.EstType estType, GenderRace genderRace, ushort setId)
=> GetDefault( CharacterUtility.ReverseIndices[ ( int )metaIndex ], genderRace, setId ); => GetDefault(manager, (MetaIndex)estType, genderRace, setId);
public static ushort GetDefault( EstManipulation.EstType estType, GenderRace genderRace, ushort setId )
=> GetDefault( ( MetaIndex )estType, genderRace, setId );
} }

View file

@ -3,15 +3,16 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files; namespace Penumbra.Meta.Files;
/// <summary>
// EVP file structure: /// EVP file structure:
// [Identifier:3 bytes, EVP] /// [Identifier:3 bytes, EVP]
// [NumModels:ushort] /// [NumModels:ushort]
// NumModels x [ModelId:ushort] /// NumModels x [ModelId:ushort]
// Containing the relevant model IDs. Seems to be sorted. /// Containing the relevant model IDs. Seems to be sorted.
// NumModels x [DataArray]:512 Byte] /// NumModels x [DataArray]:512 Byte]
// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet. /// Containing Flags in each byte, 0x01 set for Body, 0x02 set for Helmet.
// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect. /// Each flag corresponds to a mount row from the Mounts table and determines whether the mount disables the effect.
/// </summary>
public unsafe class EvpFile : MetaBaseFile public unsafe class EvpFile : MetaBaseFile
{ {
public const int FlagArraySize = 512; public const int FlagArraySize = 512;
@ -26,45 +27,39 @@ public unsafe class EvpFile : MetaBaseFile
} }
public int NumModels public int NumModels
=> Data[ 3 ]; => Data[3];
public ReadOnlySpan< ushort > ModelSetIds public ReadOnlySpan<ushort> ModelSetIds
=> new(Data + 4, NumModels); => new(Data + 4, NumModels);
public ushort ModelSetId( int idx ) public ushort ModelSetId(int idx)
=> idx >= 0 && idx < NumModels ? ( ( ushort* )( Data + 4 ) )[ idx ] : ushort.MaxValue; => idx >= 0 && idx < NumModels ? ((ushort*)(Data + 4))[idx] : ushort.MaxValue;
public ReadOnlySpan< EvpFlag > Flags( int idx ) public ReadOnlySpan<EvpFlag> Flags(int idx)
=> new(Data + 4 + idx * FlagArraySize, FlagArraySize); => new(Data + 4 + idx * FlagArraySize, FlagArraySize);
public EvpFlag Flag( ushort modelSet, int arrayIndex ) public EvpFlag Flag(ushort modelSet, int arrayIndex)
{
if( arrayIndex is >= FlagArraySize or < 0 )
{ {
if (arrayIndex is >= FlagArraySize or < 0)
return EvpFlag.None; return EvpFlag.None;
}
var ids = ModelSetIds; var ids = ModelSetIds;
for( var i = 0; i < ids.Length; ++i ) for (var i = 0; i < ids.Length; ++i)
{
var model = ids[ i ];
if( model < modelSet )
{ {
var model = ids[i];
if (model < modelSet)
continue; continue;
}
if( model > modelSet ) if (model > modelSet)
{
break; break;
}
return Flags( i )[ arrayIndex ]; return Flags(i)[arrayIndex];
} }
return EvpFlag.None; return EvpFlag.None;
} }
public EvpFile() public EvpFile(MetaFileManager manager)
: base( ( MetaIndex )1 ) // TODO: Name : base(manager, (MetaIndex)1) // TODO: Name
{ } { }
} }

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Numerics; using System.Numerics;
using Newtonsoft.Json;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
@ -16,7 +15,7 @@ public class ImcException : Exception
public readonly ImcManipulation Manipulation; public readonly ImcManipulation Manipulation;
public readonly string GamePath; public readonly string GamePath;
public ImcException( ImcManipulation manip, Utf8GamePath path ) public ImcException(ImcManipulation manip, Utf8GamePath path)
{ {
Manipulation = manip; Manipulation = manip;
GamePath = path.ToString(); GamePath = path.ToString();
@ -34,44 +33,42 @@ public unsafe class ImcFile : MetaBaseFile
private const int PreambleSize = 4; private const int PreambleSize = 4;
public int ActualLength public int ActualLength
=> NumParts * sizeof( ImcEntry ) * ( Count + 1 ) + PreambleSize; => NumParts * sizeof(ImcEntry) * (Count + 1) + PreambleSize;
public int Count public int Count
=> CountInternal( Data ); => CountInternal(Data);
public readonly Utf8GamePath Path; public readonly Utf8GamePath Path;
public readonly int NumParts; public readonly int NumParts;
public ReadOnlySpan< ImcEntry > Span public ReadOnlySpan<ImcEntry> Span
=> new(( ImcEntry* )( Data + PreambleSize ), ( Length - PreambleSize ) / sizeof( ImcEntry )); => new((ImcEntry*)(Data + PreambleSize), (Length - PreambleSize) / sizeof(ImcEntry));
private static int CountInternal( byte* data ) private static int CountInternal(byte* data)
=> *( ushort* )data; => *(ushort*)data;
private static ushort PartMask( byte* data ) private static ushort PartMask(byte* data)
=> *( ushort* )( data + 2 ); => *(ushort*)(data + 2);
private static ImcEntry* VariantPtr( byte* data, int partIdx, int variantIdx ) private static ImcEntry* VariantPtr(byte* data, int partIdx, int variantIdx)
{ {
var flag = 1 << partIdx; var flag = 1 << partIdx;
if( ( PartMask( data ) & flag ) == 0 || variantIdx > CountInternal( data ) ) if ((PartMask(data) & flag) == 0 || variantIdx > CountInternal(data))
{
return null; return null;
}
var numParts = BitOperations.PopCount( PartMask( data ) ); var numParts = BitOperations.PopCount(PartMask(data));
var ptr = ( ImcEntry* )( data + PreambleSize ); var ptr = (ImcEntry*)(data + PreambleSize);
ptr += variantIdx * numParts + partIdx; ptr += variantIdx * numParts + partIdx;
return ptr; return ptr;
} }
public ImcEntry GetEntry( int partIdx, int variantIdx ) public ImcEntry GetEntry(int partIdx, int variantIdx)
{ {
var ptr = VariantPtr( Data, partIdx, variantIdx ); var ptr = VariantPtr(Data, partIdx, variantIdx);
return ptr == null ? new ImcEntry() : *ptr; return ptr == null ? new ImcEntry() : *ptr;
} }
public static int PartIndex( EquipSlot slot ) public static int PartIndex(EquipSlot slot)
=> slot switch => slot switch
{ {
EquipSlot.Head => 0, EquipSlot.Head => 0,
@ -87,52 +84,44 @@ public unsafe class ImcFile : MetaBaseFile
_ => 0, _ => 0,
}; };
public bool EnsureVariantCount( int numVariants ) public bool EnsureVariantCount(int numVariants)
{
if( numVariants <= Count )
{ {
if (numVariants <= Count)
return true; return true;
}
var oldCount = Count; var oldCount = Count;
*( ushort* )Data = ( ushort )numVariants; *(ushort*)Data = (ushort)numVariants;
if( ActualLength > Length ) if (ActualLength > Length)
{ {
var newLength = ( ( ( ActualLength - 1 ) >> 7 ) + 1 ) << 7; var newLength = (((ActualLength - 1) >> 7) + 1) << 7;
Penumbra.Log.Verbose( $"Resized IMC {Path} from {Length} to {newLength}." ); Penumbra.Log.Verbose($"Resized IMC {Path} from {Length} to {newLength}.");
ResizeResources( newLength ); ResizeResources(newLength);
} }
var defaultPtr = ( ImcEntry* )( Data + PreambleSize ); var defaultPtr = (ImcEntry*)(Data + PreambleSize);
for( var i = oldCount + 1; i < numVariants + 1; ++i ) for (var i = oldCount + 1; i < numVariants + 1; ++i)
{ MemoryUtility.MemCpyUnchecked(defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof(ImcEntry));
MemoryUtility.MemCpyUnchecked( defaultPtr + i * NumParts, defaultPtr, NumParts * sizeof( ImcEntry ) );
}
Penumbra.Log.Verbose( $"Expanded IMC {Path} from {oldCount} to {numVariants} variants." ); Penumbra.Log.Verbose($"Expanded IMC {Path} from {oldCount} to {numVariants} variants.");
return true; return true;
} }
public bool SetEntry( int partIdx, int variantIdx, ImcEntry entry ) public bool SetEntry(int partIdx, int variantIdx, ImcEntry entry)
{ {
if( partIdx >= NumParts ) if (partIdx >= NumParts)
return false;
EnsureVariantCount(variantIdx);
var variantPtr = VariantPtr(Data, partIdx, variantIdx);
if (variantPtr == null)
{ {
Penumbra.Log.Error("Error during expansion of imc file.");
return false; return false;
} }
EnsureVariantCount( variantIdx ); if (variantPtr->Equals(entry))
var variantPtr = VariantPtr( Data, partIdx, variantIdx );
if( variantPtr == null )
{
Penumbra.Log.Error( "Error during expansion of imc file." );
return false; return false;
}
if( variantPtr->Equals( entry ) )
{
return false;
}
*variantPtr = entry; *variantPtr = entry;
return true; return true;
@ -141,70 +130,64 @@ public unsafe class ImcFile : MetaBaseFile
public override void Reset() public override void Reset()
{ {
var file = DalamudServices.SGameData.GetFile( Path.ToString() ); var file = DalamudServices.SGameData.GetFile(Path.ToString());
fixed( byte* ptr = file!.Data ) fixed (byte* ptr = file!.Data)
{ {
MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); MemoryUtility.MemCpyUnchecked(Data, ptr, file.Data.Length);
MemoryUtility.MemSet( Data + file.Data.Length, 0, Length - file.Data.Length ); MemoryUtility.MemSet(Data + file.Data.Length, 0, Length - file.Data.Length);
} }
} }
public ImcFile( ImcManipulation manip ) public ImcFile(MetaFileManager manager, ImcManipulation manip)
: base( 0 ) : base(manager, 0)
{ {
Path = manip.GamePath(); Path = manip.GamePath();
var file = DalamudServices.SGameData.GetFile( Path.ToString() ); var file = manager.GameData.GetFile(Path.ToString());
if( file == null ) if (file == null)
{ throw new ImcException(manip, Path);
throw new ImcException( manip, Path );
}
fixed( byte* ptr = file.Data ) fixed (byte* ptr = file.Data)
{ {
NumParts = BitOperations.PopCount( *( ushort* )( ptr + 2 ) ); NumParts = BitOperations.PopCount(*(ushort*)(ptr + 2));
AllocateData( file.Data.Length ); AllocateData(file.Data.Length);
MemoryUtility.MemCpyUnchecked( Data, ptr, file.Data.Length ); MemoryUtility.MemCpyUnchecked(Data, ptr, file.Data.Length);
} }
} }
public static ImcEntry GetDefault( Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists ) public static ImcEntry GetDefault(MetaFileManager manager, Utf8GamePath path, EquipSlot slot, int variantIdx, out bool exists)
=> GetDefault( path.ToString(), slot, variantIdx, out exists ); => GetDefault(manager, path.ToString(), slot, variantIdx, out exists);
public static ImcEntry GetDefault( string path, EquipSlot slot, int variantIdx, out bool exists ) public static ImcEntry GetDefault(MetaFileManager manager, string path, EquipSlot slot, int variantIdx, out bool exists)
{ {
var file = DalamudServices.SGameData.GetFile( path ); var file = manager.GameData.GetFile(path);
exists = false; exists = false;
if( file == null ) if (file == null)
{
throw new Exception(); throw new Exception();
}
fixed( byte* ptr = file.Data ) fixed (byte* ptr = file.Data)
{
var entry = VariantPtr( ptr, PartIndex( slot ), variantIdx );
if( entry != null )
{ {
var entry = VariantPtr(ptr, PartIndex(slot), variantIdx);
if (entry == null)
return new ImcEntry();
exists = true; exists = true;
return *entry; return *entry;
} }
return new ImcEntry();
}
} }
public void Replace( ResourceHandle* resource ) public void Replace(ResourceHandle* resource)
{ {
var (data, length) = resource->GetData(); var (data, length) = resource->GetData();
var newData = Penumbra.MetaFileManager.AllocateDefaultMemory( ActualLength, 8 ); var newData = Penumbra.MetaFileManager.AllocateDefaultMemory(ActualLength, 8);
if( newData == null ) if (newData == null)
{ {
Penumbra.Log.Error( $"Could not replace loaded IMC data at 0x{( ulong )resource:X}, allocation failed." ); Penumbra.Log.Error($"Could not replace loaded IMC data at 0x{(ulong)resource:X}, allocation failed.");
return; return;
} }
MemoryUtility.MemCpyUnchecked( newData, Data, ActualLength ); MemoryUtility.MemCpyUnchecked(newData, Data, ActualLength);
Penumbra.MetaFileManager.Free( data, length ); Penumbra.MetaFileManager.Free(data, length);
resource->SetData( ( IntPtr )newData, ActualLength ); resource->SetData((IntPtr)newData, ActualLength);
} }
} }

View file

@ -8,75 +8,74 @@ namespace Penumbra.Meta.Files;
public unsafe class MetaBaseFile : IDisposable public unsafe class MetaBaseFile : IDisposable
{ {
protected readonly MetaFileManager Manager;
public byte* Data { get; private set; } public byte* Data { get; private set; }
public int Length { get; private set; } public int Length { get; private set; }
public CharacterUtility.InternalIndex Index { get; } public CharacterUtility.InternalIndex Index { get; }
public MetaBaseFile( MetaIndex idx ) public MetaBaseFile(MetaFileManager manager, MetaIndex idx)
=> Index = CharacterUtility.ReverseIndices[ ( int )idx ]; {
Manager = manager;
Index = CharacterUtility.ReverseIndices[(int)idx];
}
protected (IntPtr Data, int Length) DefaultData protected (IntPtr Data, int Length) DefaultData
=> Penumbra.CharacterUtility.DefaultResource( Index ); => Manager.CharacterUtility.DefaultResource(Index);
// Reset to default values. /// <summary> Reset to default values. </summary>
public virtual void Reset() public virtual void Reset()
{ } { }
// Obtain memory. /// <summary> Obtain memory. </summary>
protected void AllocateData( int length ) protected void AllocateData(int length)
{ {
Length = length; Length = length;
Data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( length ); Data = (byte*)Manager.AllocateFileMemory(length);
if( length > 0 ) if (length > 0)
{ GC.AddMemoryPressure(length);
GC.AddMemoryPressure( length );
}
} }
// Free memory. /// <summary> Free memory. </summary>
protected void ReleaseUnmanagedResources() protected void ReleaseUnmanagedResources()
{ {
var ptr = ( IntPtr )Data; var ptr = (IntPtr)Data;
MemoryHelper.GameFree( ref ptr, ( ulong )Length ); MemoryHelper.GameFree(ref ptr, (ulong)Length);
if( Length > 0 ) if (Length > 0)
{ GC.RemoveMemoryPressure(Length);
GC.RemoveMemoryPressure( Length );
}
Length = 0; Length = 0;
Data = null; Data = null;
} }
// Resize memory while retaining data. /// <summary> Resize memory while retaining data. </summary>
protected void ResizeResources( int newLength ) protected void ResizeResources(int newLength)
{
if( newLength == Length )
{ {
if (newLength == Length)
return; return;
}
var data = ( byte* )Penumbra.MetaFileManager.AllocateFileMemory( ( ulong )newLength ); var data = (byte*)Manager.AllocateFileMemory((ulong)newLength);
if( newLength > Length ) if (newLength > Length)
{ {
MemoryUtility.MemCpyUnchecked( data, Data, Length ); MemoryUtility.MemCpyUnchecked(data, Data, Length);
MemoryUtility.MemSet( data + Length, 0, newLength - Length ); MemoryUtility.MemSet(data + Length, 0, newLength - Length);
} }
else else
{ {
MemoryUtility.MemCpyUnchecked( data, Data, newLength ); MemoryUtility.MemCpyUnchecked(data, Data, newLength);
} }
ReleaseUnmanagedResources(); ReleaseUnmanagedResources();
GC.AddMemoryPressure( newLength ); GC.AddMemoryPressure(newLength);
Data = data; Data = data;
Length = newLength; Length = newLength;
} }
// Manually free memory. /// <summary> Manually free memory. </summary>
public void Dispose() public void Dispose()
{ {
ReleaseUnmanagedResources(); ReleaseUnmanagedResources();
GC.SuppressFinalize( this ); GC.SuppressFinalize(this);
} }
~MetaBaseFile() ~MetaBaseFile()

View file

@ -0,0 +1,85 @@
using System;
using System.Runtime.CompilerServices;
using Dalamud.Data;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.System.Memory;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.GameData;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using ResidentResourceManager = Penumbra.Interop.Services.ResidentResourceManager;
namespace Penumbra.Meta;
public unsafe class MetaFileManager
{
internal readonly Configuration Config;
internal readonly CharacterUtility CharacterUtility;
internal readonly ResidentResourceManager ResidentResources;
internal readonly DataManager GameData;
internal readonly ActiveCollections ActiveCollections;
internal readonly ValidityChecker ValidityChecker;
public MetaFileManager(CharacterUtility characterUtility, ResidentResourceManager residentResources, DataManager gameData,
ActiveCollections activeCollections, Configuration config, ValidityChecker validityChecker)
{
CharacterUtility = characterUtility;
ResidentResources = residentResources;
GameData = gameData;
ActiveCollections = activeCollections;
Config = config;
ValidityChecker = validityChecker;
SignatureHelper.Initialise(this);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public void SetFile(MetaBaseFile? file, MetaIndex metaIndex)
{
if (file == null)
CharacterUtility.ResetResource(metaIndex);
else
CharacterUtility.SetResource(metaIndex, (nint)file.Data, file.Length);
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CharacterUtility.MetaList.MetaReverter TemporarilySetFile(MetaBaseFile? file, MetaIndex metaIndex)
=> file == null
? CharacterUtility.TemporarilyResetResource(metaIndex)
: CharacterUtility.TemporarilySetResource(metaIndex, (nint)file.Data, file.Length);
public void ApplyDefaultFiles(ModCollection collection)
{
if (ActiveCollections.Default != collection || !CharacterUtility.Ready || !Config.EnableMods)
return;
ResidentResources.Reload();
collection._cache?.MetaManipulations.SetFiles();
}
/// <summary>
/// Allocate in the games space for file storage.
/// We only need this if using any meta file.
/// </summary>
[Signature(Sigs.GetFileSpace)]
private readonly nint _getFileSpaceAddress = nint.Zero;
public IMemorySpace* GetFileSpace()
=> ((delegate* unmanaged<IMemorySpace*>)_getFileSpaceAddress)();
public void* AllocateFileMemory(ulong length, ulong alignment = 0)
=> GetFileSpace()->Malloc(length, alignment);
public void* AllocateFileMemory(int length, int alignment = 0)
=> AllocateFileMemory((ulong)length, (ulong)alignment);
public void* AllocateDefaultMemory(ulong length, ulong alignment = 0)
=> GetFileSpace()->Malloc(length, alignment);
public void* AllocateDefaultMemory(int length, int alignment = 0)
=> IMemorySpace.GetDefaultSpace()->Malloc((ulong)length, (ulong)alignment);
public void Free(nint ptr, int length)
=> IMemorySpace.Free((void*)ptr, (ulong)length);
}

View file

@ -5,6 +5,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.String.Classes; using Penumbra.String.Classes;
namespace Penumbra.Mods.ItemSwap; namespace Penumbra.Mods.ItemSwap;
@ -12,7 +13,7 @@ namespace Penumbra.Mods.ItemSwap;
public static class CustomizationSwap public static class CustomizationSwap
{ {
/// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode. /// The .mdl file for customizations is unique per racecode, slot and id, thus the .mdl redirection itself is independent of the mode.
public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo ) public static FileSwap CreateMdl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo )
{ {
if( idFrom.Value > byte.MaxValue ) if( idFrom.Value > byte.MaxValue )
{ {
@ -22,7 +23,7 @@ public static class CustomizationSwap
var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() ); var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() );
var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() ); var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() );
var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); var mdl = FileSwap.CreateSwap( manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo );
var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1; var range = slot == BodySlot.Tail && race is GenderRace.HrothgarMale or GenderRace.HrothgarFemale or GenderRace.HrothgarMaleNpc or GenderRace.HrothgarMaleNpc ? 5 : 1;
foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() ) foreach( ref var materialFileName in mdl.AsMdl()!.Materials.AsSpan() )
@ -31,7 +32,7 @@ public static class CustomizationSwap
foreach( var variant in Enumerable.Range( 1, range ) ) foreach( var variant in Enumerable.Range( 1, range ) )
{ {
name = materialFileName; name = materialFileName;
var mtrl = CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged ); var mtrl = CreateMtrl( manager, redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged );
mdl.ChildSwaps.Add( mtrl ); mdl.ChildSwaps.Add( mtrl );
} }
@ -41,7 +42,7 @@ public static class CustomizationSwap
return mdl; return mdl;
} }
public static FileSwap CreateMtrl( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant, public static FileSwap CreateMtrl( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant,
ref string fileName, ref bool dataWasChanged ) ref string fileName, ref bool dataWasChanged )
{ {
variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant; variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant;
@ -62,20 +63,20 @@ public static class CustomizationSwap
dataWasChanged = true; dataWasChanged = true;
} }
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath ); var mtrl = FileSwap.CreateSwap( manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath );
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); var shpk = CreateShader( manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( shpk ); mtrl.ChildSwaps.Add( shpk );
foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() )
{ {
var tex = CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged ); var tex = CreateTex( manager, redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( tex ); mtrl.ChildSwaps.Add( tex );
} }
return mtrl; return mtrl;
} }
public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture, public static FileSwap CreateTex( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture,
ref bool dataWasChanged ) ref bool dataWasChanged )
{ {
var path = texture.Path; var path = texture.Path;
@ -99,13 +100,13 @@ public static class CustomizationSwap
dataWasChanged = true; dataWasChanged = true;
} }
return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); return FileSwap.CreateSwap( manager, ResourceType.Tex, redirections, newPath, path, path );
} }
public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) public static FileSwap CreateShader( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged )
{ {
var path = $"shader/sm5/shpk/{shaderName}"; var path = $"shader/sm5/shpk/{shaderName}";
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); return FileSwap.CreateSwap( manager, ResourceType.Shpk, redirections, path, path );
} }
} }

View file

@ -10,6 +10,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs; using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -18,41 +19,51 @@ namespace Penumbra.Mods.ItemSwap;
public static class EquipmentSwap public static class EquipmentSwap
{ {
private static EquipSlot[] ConvertSlots( EquipSlot slot, bool rFinger, bool lFinger ) private static EquipSlot[] ConvertSlots(EquipSlot slot, bool rFinger, bool lFinger)
{ {
if( slot != EquipSlot.RFinger ) if (slot != EquipSlot.RFinger)
return new[]
{ {
return new[] { slot }; slot,
} };
return rFinger return rFinger
? lFinger ? lFinger
? new[] { EquipSlot.RFinger, EquipSlot.LFinger } ? new[]
: new[] { EquipSlot.RFinger } {
EquipSlot.RFinger,
EquipSlot.LFinger,
}
: new[]
{
EquipSlot.RFinger,
}
: lFinger : lFinger
? new[] { EquipSlot.LFinger } ? new[]
: Array.Empty< EquipSlot >(); {
EquipSlot.LFinger,
}
: Array.Empty<EquipSlot>();
} }
public static Item[] CreateTypeSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo ) Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips,
{ EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo)
LookupItem( itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom );
LookupItem( itemTo, out var actualSlotTo, out var idTo, out var variantTo );
if( actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot() )
{ {
LookupItem(itemFrom, out var actualSlotFrom, out var idFrom, out var variantFrom);
LookupItem(itemTo, out var actualSlotTo, out var idTo, out var variantTo);
if (actualSlotFrom != slotFrom.ToSlot() || actualSlotTo != slotTo.ToSlot())
throw new ItemSwap.InvalidItemTypeException(); throw new ItemSwap.InvalidItemTypeException();
}
var ( imcFileFrom, variants, affectedItems ) = GetVariants( identifier, slotFrom, idFrom, idTo, variantFrom ); var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default ); var imcManip = new ImcManipulation(slotTo, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile( imcManip ); var imcFileTo = new ImcFile(manager, imcManip);
var skipFemale = false; var skipFemale = false;
var skipMale = false; var skipMale = false;
var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ) ) ).Imc.Entry.MaterialId; var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo))).Imc.Entry.MaterialId;
foreach( var gr in Enum.GetValues< GenderRace >() ) foreach (var gr in Enum.GetValues<GenderRace>())
{ {
switch( gr.Split().Item1 ) switch (gr.Split().Item1)
{ {
case Gender.Male when skipMale: continue; case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue; case Gender.Female when skipFemale: continue;
@ -60,22 +71,18 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue; case Gender.FemaleNpc when skipFemale: continue;
} }
if( CharacterUtilityData.EqdpIdx( gr, true ) < 0 ) if (CharacterUtilityData.EqdpIdx(gr, true) < 0)
{
continue; continue;
}
try try
{ {
var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo ); var eqdp = CreateEqdp(manager, redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo);
if( eqdp != null ) if (eqdp != null)
{ swaps.Add(eqdp);
swaps.Add( eqdp );
} }
} catch (ItemSwap.MissingFileException e)
catch( ItemSwap.MissingFileException e )
{ {
switch( gr ) switch (gr)
{ {
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
skipMale = true; skipMale = true;
@ -88,59 +95,54 @@ public static class EquipmentSwap
} }
} }
foreach( var variant in variants ) foreach (var variant in variants)
{ {
var imc = CreateImc( redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); var imc = CreateImc(manager, redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add( imc ); swaps.Add(imc);
} }
return affectedItems; return affectedItems;
} }
public static Item[] CreateItemSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom, public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
Item itemTo, bool rFinger = true, bool lFinger = true ) Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, Item itemFrom,
Item itemTo, bool rFinger = true, bool lFinger = true)
{ {
// Check actual ids, variants and slots. We only support using the same slot. // Check actual ids, variants and slots. We only support using the same slot.
LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom ); LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom);
LookupItem( itemTo, out var slotTo, out var idTo, out var variantTo ); LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo);
if( slotFrom != slotTo ) if (slotFrom != slotTo)
{
throw new ItemSwap.InvalidItemTypeException(); throw new ItemSwap.InvalidItemTypeException();
}
var eqp = CreateEqp( manips, slotFrom, idFrom, idTo ); var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo);
if( eqp != null ) if (eqp != null)
{ swaps.Add(eqp);
swaps.Add( eqp );
}
var gmp = CreateGmp( manips, slotFrom, idFrom, idTo ); var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo);
if( gmp != null ) if (gmp != null)
{ swaps.Add(gmp);
swaps.Add( gmp );
}
var affectedItems = Array.Empty< Item >(); var affectedItems = Array.Empty<Item>();
foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) ) foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger))
{ {
( var imcFileFrom, var variants, affectedItems ) = GetVariants( identifier, slot, idFrom, idTo, variantFrom ); (var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default ); var imcManip = new ImcManipulation(slot, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile( imcManip ); var imcFileTo = new ImcFile(manager, imcManip);
var isAccessory = slot.IsAccessory(); var isAccessory = slot.IsAccessory();
var estType = slot switch var estType = slot switch
{ {
EquipSlot.Head => EstManipulation.EstType.Head, EquipSlot.Head => EstManipulation.EstType.Head,
EquipSlot.Body => EstManipulation.EstType.Body, EquipSlot.Body => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0, _ => (EstManipulation.EstType)0,
}; };
var skipFemale = false; var skipFemale = false;
var skipMale = false; var skipMale = false;
var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ) ) ).Imc.Entry.MaterialId; var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId;
foreach( var gr in Enum.GetValues< GenderRace >() ) foreach (var gr in Enum.GetValues<GenderRace>())
{ {
switch( gr.Split().Item1 ) switch (gr.Split().Item1)
{ {
case Gender.Male when skipMale: continue; case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue; case Gender.Female when skipFemale: continue;
@ -148,30 +150,24 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue; case Gender.FemaleNpc when skipFemale: continue;
} }
if( CharacterUtilityData.EqdpIdx( gr, isAccessory ) < 0 ) if (CharacterUtilityData.EqdpIdx(gr, isAccessory) < 0)
{
continue; continue;
}
try try
{ {
var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo ); var eqdp = CreateEqdp(manager, redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo);
if( eqdp != null ) if (eqdp != null)
{ swaps.Add(eqdp);
swaps.Add( eqdp );
}
var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false; var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false;
var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl ); var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl);
if( est != null ) if (est != null)
{ swaps.Add(est);
swaps.Add( est );
} }
} catch (ItemSwap.MissingFileException e)
catch( ItemSwap.MissingFileException e )
{ {
switch( gr ) switch (gr)
{ {
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl: case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
skipMale = true; skipMale = true;
@ -184,33 +180,38 @@ public static class EquipmentSwap
} }
} }
foreach( var variant in variants ) foreach (var variant in variants)
{ {
var imc = CreateImc( redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo ); var imc = CreateImc(manager, redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add( imc ); swaps.Add(imc);
} }
} }
return affectedItems; return affectedItems;
} }
public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom, public static MetaSwap? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
SetId idTo, byte mtrlTo ) Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, GenderRace gr, SetId idFrom,
=> CreateEqdp( redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo ); SetId idTo, byte mtrlTo)
public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, => CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo);
SetId idTo, byte mtrlTo )
public static MetaSwap? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
Func<MetaManipulation, MetaManipulation> manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom,
SetId idTo, byte mtrlTo)
{ {
var (gender, race) = gr.Split(); var (gender, race) = gr.Split();
var eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotFrom.IsAccessory(), idFrom.Value ), slotFrom, gender, race, idFrom.Value ); var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom.Value), slotFrom, gender,
var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotTo.IsAccessory(), idTo.Value ), slotTo, gender, race, idTo.Value ); race, idFrom.Value);
var meta = new MetaSwap( manips, eqdpFrom, eqdpTo ); var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo.Value), slotTo, gender, race,
var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slotFrom ); idTo.Value);
if( ownMdl ) var meta = new MetaSwap(manips, eqdpFrom, eqdpTo);
var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits(slotFrom);
if (ownMdl)
{ {
var mdl = CreateMdl( redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo ); var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo);
meta.ChildSwaps.Add( mdl ); meta.ChildSwaps.Add(mdl);
} }
else if( !ownMtrl && meta.SwapAppliedIsDefault ) else if (!ownMtrl && meta.SwapAppliedIsDefault)
{ {
meta = null; meta = null;
} }
@ -218,97 +219,98 @@ public static class EquipmentSwap
return meta; return meta;
} }
public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) public static FileSwap CreateMdl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, GenderRace gr,
=> CreateMdl( redirections, slot, slot, gr, idFrom, idTo, mtrlTo ); SetId idFrom, SetId idTo, byte mtrlTo)
=> CreateMdl(manager, redirections, slot, slot, gr, idFrom, idTo, mtrlTo);
public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo ) public static FileSwap CreateMdl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo)
{ {
var mdlPathFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idFrom, gr, slotFrom ) : GamePaths.Equipment.Mdl.Path( idFrom, gr, slotFrom ); var mdlPathFrom = slotFrom.IsAccessory()
var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idTo, gr, slotTo ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slotTo ); ? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom)
var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo ); : GamePaths.Equipment.Mdl.Path(idFrom, gr, slotFrom);
var mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path(idTo, gr, slotTo) : GamePaths.Equipment.Mdl.Path(idTo, gr, slotTo);
var mdl = FileSwap.CreateSwap(manager, ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo);
foreach( ref var fileName in mdl.AsMdl()!.Materials.AsSpan() ) foreach (ref var fileName in mdl.AsMdl()!.Materials.AsSpan())
{ {
var mtrl = CreateMtrl( redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged ); var mtrl = CreateMtrl(manager, redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged);
if( mtrl != null ) if (mtrl != null)
{ mdl.ChildSwaps.Add(mtrl);
mdl.ChildSwaps.Add( mtrl );
}
} }
return mdl; return mdl;
} }
private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant ) private static void LookupItem(Item i, out EquipSlot slot, out SetId modelId, out byte variant)
{
slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
if( !slot.IsEquipmentPiece() )
{ {
slot = ((EquipSlot)i.EquipSlotCategory.Row).ToSlot();
if (!slot.IsEquipmentPiece())
throw new ItemSwap.InvalidItemTypeException(); throw new ItemSwap.InvalidItemTypeException();
modelId = ((Quad)i.ModelMain).A;
variant = (byte)((Quad)i.ModelMain).B;
} }
modelId = ( ( Quad )i.ModelMain ).A; private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom,
variant = ( byte )( ( Quad )i.ModelMain ).B; SetId idFrom, SetId idTo, byte variantFrom)
}
private static (ImcFile, byte[], Item[]) GetVariants( IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom )
{ {
var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default ); var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default);
var imc = new ImcFile( entry ); var imc = new ImcFile(manager, entry);
Item[] items; Item[] items;
byte[] variants; byte[] variants;
if( idFrom.Value == idTo.Value ) if (idFrom.Value == idTo.Value)
{ {
items = identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray(); items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray();
variants = new[] { variantFrom }; variants = new[]
{
variantFrom,
};
} }
else else
{ {
items = identifier.Identify( slotFrom.IsEquipment() items = identifier.Identify(slotFrom.IsEquipment()
? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)
: GamePaths.Accessory.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom ) ).Select( kvp => kvp.Value ).OfType< Item >().ToArray(); : GamePaths.Accessory.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)).Select(kvp => kvp.Value).OfType<Item>().ToArray();
variants = Enumerable.Range( 0, imc.Count + 1 ).Select( i => ( byte )i ).ToArray(); variants = Enumerable.Range(0, imc.Count + 1).Select(i => (byte)i).ToArray();
} }
return ( imc, variants, items ); return (imc, variants, items);
} }
public static MetaSwap? CreateGmp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) public static MetaSwap? CreateGmp(MetaFileManager manager, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, SetId idFrom,
{ SetId idTo)
if( slot is not EquipSlot.Head )
{ {
if (slot is not EquipSlot.Head)
return null; return null;
var manipFrom = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idFrom.Value), idFrom.Value);
var manipTo = new GmpManipulation(ExpandedGmpFile.GetDefault(manager, idTo.Value), idTo.Value);
return new MetaSwap(manips, manipFrom, manipTo);
} }
var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value ); public static MetaSwap CreateImc(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot,
var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value ); SetId idFrom, SetId idTo,
return new MetaSwap( manips, manipFrom, manipTo ); byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
} => CreateImc(manager, redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo);
public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo, public static MetaSwap CreateImc(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips,
byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo ) EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo,
=> CreateImc( redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo ); byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo)
public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo,
byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo )
{ {
var entryFrom = imcFileFrom.GetEntry( ImcFile.PartIndex( slotFrom ), variantFrom ); var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
var entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ); var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
var manipulationFrom = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, entryFrom ); var manipulationFrom = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, entryFrom);
var manipulationTo = new ImcManipulation( slotTo, variantTo, idTo.Value, entryTo ); var manipulationTo = new ImcManipulation(slotTo, variantTo, idTo.Value, entryTo);
var imc = new MetaSwap( manips, manipulationFrom, manipulationTo ); var imc = new MetaSwap(manips, manipulationFrom, manipulationTo);
var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId ); var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId);
if( decal != null ) if (decal != null)
{ imc.ChildSwaps.Add(decal);
imc.ChildSwaps.Add( decal );
}
var avfx = CreateAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId ); var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId);
if( avfx != null ) if (avfx != null)
{ imc.ChildSwaps.Add(avfx);
imc.ChildSwaps.Add( avfx );
}
// IMC also controls sound, Example: Dodore Doublet, but unknown what it does? // IMC also controls sound, Example: Dodore Doublet, but unknown what it does?
// IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does. // IMC also controls some material animation, Example: The Howling Spirit and The Wailing Spirit, but unknown what it does.
@ -316,134 +318,135 @@ public static class EquipmentSwap
} }
// Example: Crimson Standard Bracelet // Example: Crimson Standard Bracelet
public static FileSwap? CreateDecal( Func< Utf8GamePath, FullPath > redirections, byte decalId ) public static FileSwap? CreateDecal(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, byte decalId)
{
if( decalId == 0 )
{ {
if (decalId == 0)
return null; return null;
}
var decalPath = GamePaths.Equipment.Decal.Path( decalId ); var decalPath = GamePaths.Equipment.Decal.Path(decalId);
return FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath ); return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath);
} }
// Example: Abyssos Helm / Body // Example: Abyssos Helm / Body
public static FileSwap? CreateAvfx( Func< Utf8GamePath, FullPath > redirections, SetId idFrom, SetId idTo, byte vfxId ) public static FileSwap? CreateAvfx(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, SetId idFrom, SetId idTo, byte vfxId)
{
if( vfxId == 0 )
{ {
if (vfxId == 0)
return null; return null;
}
var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId ); var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom.Value, vfxId);
var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId ); var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo.Value, vfxId);
var avfx = FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo ); var avfx = FileSwap.CreateSwap(manager, ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo);
foreach( ref var filePath in avfx.AsAvfx()!.Textures.AsSpan() ) foreach (ref var filePath in avfx.AsAvfx()!.Textures.AsSpan())
{ {
var atex = CreateAtex( redirections, ref filePath, ref avfx.DataWasChanged ); var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged);
avfx.ChildSwaps.Add( atex ); avfx.ChildSwaps.Add(atex);
} }
return avfx; return avfx;
} }
public static MetaSwap? CreateEqp( Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo ) public static MetaSwap? CreateEqp(MetaFileManager manager, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, SetId idFrom,
{ SetId idTo)
if( slot.IsAccessory() )
{ {
if (slot.IsAccessory())
return null; return null;
var eqpValueFrom = ExpandedEqpFile.GetDefault(manager, idFrom.Value);
var eqpValueTo = ExpandedEqpFile.GetDefault(manager, idTo.Value);
var eqpFrom = new EqpManipulation(eqpValueFrom, slot, idFrom.Value);
var eqpTo = new EqpManipulation(eqpValueTo, slot, idFrom.Value);
return new MetaSwap(manips, eqpFrom, eqpTo);
} }
var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value ); public static FileSwap? CreateMtrl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, SetId idFrom,
var eqpValueTo = ExpandedEqpFile.GetDefault( idTo.Value ); SetId idTo, byte variantTo, ref string fileName,
var eqpFrom = new EqpManipulation( eqpValueFrom, slot, idFrom.Value ); ref bool dataWasChanged)
var eqpTo = new EqpManipulation( eqpValueTo, slot, idFrom.Value ); => CreateMtrl(manager, redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged);
return new MetaSwap( manips, eqpFrom, eqpTo );
}
public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName, public static FileSwap? CreateMtrl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slotFrom, EquipSlot slotTo,
ref bool dataWasChanged ) SetId idFrom, SetId idTo, byte variantTo, ref string fileName,
=> CreateMtrl( redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged ); ref bool dataWasChanged)
public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, byte variantTo, ref string fileName,
ref bool dataWasChanged )
{ {
var prefix = slotTo.IsAccessory() ? 'a' : 'e'; var prefix = slotTo.IsAccessory() ? 'a' : 'e';
if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) ) if (!fileName.Contains($"{prefix}{idTo.Value:D4}"))
{
return null; return null;
}
var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo ); var folderTo = slotTo.IsAccessory()
? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo)
: GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo);
var pathTo = $"{folderTo}{fileName}"; var pathTo = $"{folderTo}{fileName}";
var folderFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idFrom, variantTo ); var folderFrom = slotFrom.IsAccessory()
var newFileName = ItemSwap.ReplaceId( fileName, prefix, idTo, idFrom ); ? GamePaths.Accessory.Mtrl.FolderPath(idFrom, variantTo)
newFileName = ItemSwap.ReplaceSlot( newFileName, slotTo, slotFrom, slotTo != slotFrom ); : GamePaths.Equipment.Mtrl.FolderPath(idFrom, variantTo);
var newFileName = ItemSwap.ReplaceId(fileName, prefix, idTo, idFrom);
newFileName = ItemSwap.ReplaceSlot(newFileName, slotTo, slotFrom, slotTo != slotFrom);
var pathFrom = $"{folderFrom}{newFileName}"; var pathFrom = $"{folderFrom}{newFileName}";
if( newFileName != fileName ) if (newFileName != fileName)
{ {
fileName = newFileName; fileName = newFileName;
dataWasChanged = true; dataWasChanged = true;
} }
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo ); var mtrl = FileSwap.CreateSwap(manager, ResourceType.Mtrl, redirections, pathFrom, pathTo);
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged ); var shpk = CreateShader(manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged);
mtrl.ChildSwaps.Add( shpk ); mtrl.ChildSwaps.Add(shpk);
foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() ) foreach (ref var texture in mtrl.AsMtrl()!.Textures.AsSpan())
{ {
var tex = CreateTex( redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged ); var tex = CreateTex(manager, redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged);
mtrl.ChildSwaps.Add( tex ); mtrl.ChildSwaps.Add(tex);
} }
return mtrl; return mtrl;
} }
public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, SetId idFrom, SetId idTo,
=> CreateTex( redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged ); ref MtrlFile.Texture texture, ref bool dataWasChanged)
=> CreateTex(manager, redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged);
public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged ) public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, EquipSlot slotFrom, EquipSlot slotTo, SetId idFrom,
SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged)
{ {
var path = texture.Path; var path = texture.Path;
var addedDashes = false; var addedDashes = false;
if( texture.DX11 ) if (texture.DX11)
{ {
var fileName = Path.GetFileName( path ); var fileName = Path.GetFileName(path);
if( !fileName.StartsWith( "--" ) ) if (!fileName.StartsWith("--"))
{ {
path = path.Replace( fileName, $"--{fileName}" ); path = path.Replace(fileName, $"--{fileName}");
addedDashes = true; addedDashes = true;
} }
} }
var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom ); var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
newPath = ItemSwap.ReplaceSlot( newPath, slotTo, slotFrom, slotTo != slotFrom ); newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom);
newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" ); newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
if( newPath != path ) if (newPath != path)
{ {
texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath; texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
dataWasChanged = true; dataWasChanged = true;
} }
return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, path ); return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, newPath, path, path);
} }
public static FileSwap CreateShader( Func< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged ) public static FileSwap CreateShader(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string shaderName, ref bool dataWasChanged)
{ {
var path = $"shader/sm5/shpk/{shaderName}"; var path = $"shader/sm5/shpk/{shaderName}";
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path ); return FileSwap.CreateSwap(manager, ResourceType.Shpk, redirections, path, path);
} }
public static FileSwap CreateAtex( Func< Utf8GamePath, FullPath > redirections, ref string filePath, ref bool dataWasChanged ) public static FileSwap CreateAtex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, ref string filePath, ref bool dataWasChanged)
{ {
var oldPath = filePath; var oldPath = filePath;
filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}" ); filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
dataWasChanged = true; dataWasChanged = true;
return FileSwap.CreateSwap( ResourceType.Atex, redirections, filePath, oldPath, oldPath ); return FileSwap.CreateSwap(manager, ResourceType.Atex, redirections, filePath, oldPath, oldPath);
} }
} }

View file

@ -6,6 +6,7 @@ using Penumbra.GameData.Data;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Services; using Penumbra.Services;
@ -27,7 +28,7 @@ public static class ItemSwap
=> Type = type; => Type = type;
} }
private static bool LoadFile( FullPath path, out byte[] data ) private static bool LoadFile( MetaFileManager manager, FullPath path, out byte[] data )
{ {
if( path.FullName.Length > 0 ) if( path.FullName.Length > 0 )
{ {
@ -39,7 +40,7 @@ public static class ItemSwap
return true; return true;
} }
var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() ); var file = manager.GameData.GetFile( path.InternalName.ToString() );
if( file != null ) if( file != null )
{ {
data = file.Data; data = file.Data;
@ -61,18 +62,18 @@ public static class ItemSwap
public readonly byte[] Data; public readonly byte[] Data;
public bool Valid { get; } public bool Valid { get; }
public GenericFile( FullPath path ) public GenericFile( MetaFileManager manager, FullPath path )
=> Valid = LoadFile( path, out Data ); => Valid = LoadFile( manager, path, out Data );
public byte[] Write() public byte[] Write()
=> Data; => Data;
public static readonly GenericFile Invalid = new(FullPath.Empty); public static readonly GenericFile Invalid = new(null!, FullPath.Empty);
} }
public static bool LoadFile( FullPath path, [NotNullWhen( true )] out GenericFile? file ) public static bool LoadFile( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out GenericFile? file )
{ {
file = new GenericFile( path ); file = new GenericFile( manager, path );
if( file.Valid ) if( file.Valid )
{ {
return true; return true;
@ -82,11 +83,11 @@ public static class ItemSwap
return false; return false;
} }
public static bool LoadMdl( FullPath path, [NotNullWhen( true )] out MdlFile? file ) public static bool LoadMdl( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MdlFile? file )
{ {
try try
{ {
if( LoadFile( path, out byte[] data ) ) if( LoadFile( manager, path, out byte[] data ) )
{ {
file = new MdlFile( data ); file = new MdlFile( data );
return true; return true;
@ -101,11 +102,11 @@ public static class ItemSwap
return false; return false;
} }
public static bool LoadMtrl( FullPath path, [NotNullWhen( true )] out MtrlFile? file ) public static bool LoadMtrl(MetaFileManager manager, FullPath path, [NotNullWhen( true )] out MtrlFile? file )
{ {
try try
{ {
if( LoadFile( path, out byte[] data ) ) if( LoadFile( manager, path, out byte[] data ) )
{ {
file = new MtrlFile( data ); file = new MtrlFile( data );
return true; return true;
@ -120,11 +121,11 @@ public static class ItemSwap
return false; return false;
} }
public static bool LoadAvfx( FullPath path, [NotNullWhen( true )] out AvfxFile? file ) public static bool LoadAvfx( MetaFileManager manager, FullPath path, [NotNullWhen( true )] out AvfxFile? file )
{ {
try try
{ {
if( LoadFile( path, out byte[] data ) ) if( LoadFile( manager, path, out byte[] data ) )
{ {
file = new AvfxFile( data ); file = new AvfxFile( data );
return true; return true;
@ -140,20 +141,20 @@ public static class ItemSwap
} }
public static FileSwap CreatePhyb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) public static FileSwap CreatePhyb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
{ {
var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry ); var phybPath = GamePaths.Skeleton.Phyb.Path( race, EstManipulation.ToName( type ), estEntry );
return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath ); return FileSwap.CreateSwap( manager, ResourceType.Phyb, redirections, phybPath, phybPath );
} }
public static FileSwap CreateSklb( Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry ) public static FileSwap CreateSklb(MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, EstManipulation.EstType type, GenderRace race, ushort estEntry )
{ {
var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry ); var sklbPath = GamePaths.Skeleton.Sklb.Path( race, EstManipulation.ToName( type ), estEntry );
return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath ); return FileSwap.CreateSwap(manager, ResourceType.Sklb, redirections, sklbPath, sklbPath );
} }
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks> /// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
public static MetaSwap? CreateEst( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type, public static MetaSwap? CreateEst( MetaFileManager manager, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EstManipulation.EstType type,
GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl ) GenderRace genderRace, SetId idFrom, SetId idTo, bool ownMdl )
{ {
if( type == 0 ) if( type == 0 )
@ -162,15 +163,15 @@ public static class ItemSwap
} }
var (gender, race) = genderRace.Split(); var (gender, race) = genderRace.Split();
var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) ); var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( manager, type, genderRace, idFrom.Value ) );
var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.Value ) ); var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( manager, type, genderRace, idTo.Value ) );
var est = new MetaSwap( manips, fromDefault, toDefault ); var est = new MetaSwap( manips, fromDefault, toDefault );
if( ownMdl && est.SwapApplied.Est.Entry >= 2 ) if( ownMdl && est.SwapApplied.Est.Entry >= 2 )
{ {
var phyb = CreatePhyb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); var phyb = CreatePhyb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry );
est.ChildSwaps.Add( phyb ); est.ChildSwaps.Add( phyb );
var sklb = CreateSklb( redirections, type, genderRace, est.SwapApplied.Est.Entry ); var sklb = CreateSklb( manager, redirections, type, genderRace, est.SwapApplied.Est.Entry );
est.ChildSwaps.Add( sklb ); est.ChildSwaps.Add( sklb );
} }
else if( est.SwapAppliedIsDefault ) else if( est.SwapAppliedIsDefault )

View file

@ -9,12 +9,14 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using Penumbra.GameData; using Penumbra.GameData;
using Penumbra.Meta;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
namespace Penumbra.Mods.ItemSwap; namespace Penumbra.Mods.ItemSwap;
public class ItemSwapContainer public class ItemSwapContainer
{ {
private readonly MetaFileManager _manager;
private readonly IObjectIdentifier _identifier; private readonly IObjectIdentifier _identifier;
private Dictionary< Utf8GamePath, FullPath > _modRedirections = new(); private Dictionary< Utf8GamePath, FullPath > _modRedirections = new();
@ -112,8 +114,9 @@ public class ItemSwapContainer
} }
} }
public ItemSwapContainer(IObjectIdentifier identifier) public ItemSwapContainer(MetaFileManager manager, IObjectIdentifier identifier)
{ {
_manager = manager;
_identifier = identifier; _identifier = identifier;
LoadMod( null, null ); LoadMod( null, null );
} }
@ -133,7 +136,7 @@ public class ItemSwapContainer
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;
var ret = EquipmentSwap.CreateItemSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing ); var ret = EquipmentSwap.CreateItemSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), from, to, useRightRing, useLeftRing );
Loaded = true; Loaded = true;
return ret; return ret;
} }
@ -142,15 +145,15 @@ public class ItemSwapContainer
{ {
Swaps.Clear(); Swaps.Clear();
Loaded = false; Loaded = false;
var ret = EquipmentSwap.CreateTypeSwap( _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to ); var ret = EquipmentSwap.CreateTypeSwap( _manager, _identifier, Swaps, PathResolver( collection ), MetaResolver( collection ), slotFrom, from, slotTo, to );
Loaded = true; Loaded = true;
return ret; return ret;
} }
public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null ) public bool LoadCustomization( MetaFileManager manager, BodySlot slot, GenderRace race, SetId from, SetId to, ModCollection? collection = null )
{ {
var pathResolver = PathResolver( collection ); var pathResolver = PathResolver( collection );
var mdl = CustomizationSwap.CreateMdl( pathResolver, slot, race, from, to ); var mdl = CustomizationSwap.CreateMdl( manager, pathResolver, slot, race, from, to );
var type = slot switch var type = slot switch
{ {
BodySlot.Hair => EstManipulation.EstType.Hair, BodySlot.Hair => EstManipulation.EstType.Hair,
@ -159,7 +162,7 @@ public class ItemSwapContainer
}; };
var metaResolver = MetaResolver( collection ); var metaResolver = MetaResolver( collection );
var est = ItemSwap.CreateEst( pathResolver, metaResolver, type, race, from, to, true ); var est = ItemSwap.CreateEst( manager, pathResolver, metaResolver, type, race, from, to, true );
Swaps.Add( mdl ); Swaps.Add( mdl );
if( est != null ) if( est != null )

View file

@ -7,6 +7,7 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.Meta;
using static Penumbra.Mods.ItemSwap.ItemSwap; using static Penumbra.Mods.ItemSwap.ItemSwap;
using Penumbra.Services; using Penumbra.Services;
@ -15,10 +16,10 @@ namespace Penumbra.Mods.ItemSwap;
public class Swap public class Swap
{ {
/// <summary> Any further swaps belonging specifically to this tree of changes. </summary> /// <summary> Any further swaps belonging specifically to this tree of changes. </summary>
public readonly List< Swap > ChildSwaps = new(); public readonly List<Swap> ChildSwaps = new();
public IEnumerable< Swap > WithChildren() public IEnumerable<Swap> WithChildren()
=> ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this ); => ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this);
} }
public sealed class MetaSwap : Swap public sealed class MetaSwap : Swap
@ -47,15 +48,15 @@ public sealed class MetaSwap : Swap
/// <param name="manipulations">A function that converts the given manipulation to the modded one.</param> /// <param name="manipulations">A function that converts the given manipulation to the modded one.</param>
/// <param name="manipFrom">The original meta identifier with its default value.</param> /// <param name="manipFrom">The original meta identifier with its default value.</param>
/// <param name="manipTo">The target meta identifier with its default value.</param> /// <param name="manipTo">The target meta identifier with its default value.</param>
public MetaSwap( Func< MetaManipulation, MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo ) public MetaSwap(Func<MetaManipulation, MetaManipulation> manipulations, MetaManipulation manipFrom, MetaManipulation manipTo)
{ {
SwapFrom = manipFrom; SwapFrom = manipFrom;
SwapToDefault = manipTo; SwapToDefault = manipTo;
SwapToModded = manipulations( manipTo ); SwapToModded = manipulations(manipTo);
SwapToIsDefault = manipTo.EntryEquals( SwapToModded ); SwapToIsDefault = manipTo.EntryEquals(SwapToModded);
SwapApplied = SwapFrom.WithEntryOf( SwapToModded ); SwapApplied = SwapFrom.WithEntryOf(SwapToModded);
SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom ); SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom);
} }
} }
@ -95,8 +96,8 @@ public sealed class FileSwap : Swap
/// <summary> Whether SwapFromPreChangePath equals SwapFromRequest. </summary> /// <summary> Whether SwapFromPreChangePath equals SwapFromRequest. </summary>
public bool SwapFromChanged; public bool SwapFromChanged;
public string GetNewPath( string newMod ) public string GetNewPath(string newMod)
=> Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() ); => Path.Combine(newMod, new Utf8RelPath(SwapFromRequestPath).ToString());
public MdlFile? AsMdl() public MdlFile? AsMdl()
=> FileData as MdlFile; => FileData as MdlFile;
@ -116,8 +117,9 @@ public sealed class FileSwap : Swap
/// <param name="swapToRequest">The unmodded path to the file the game is supposed to load instead.</param> /// <param name="swapToRequest">The unmodded path to the file the game is supposed to load instead.</param>
/// <param name="swap">A full swap container with the actual file in memory.</param> /// <param name="swap">A full swap container with the actual file in memory.</param>
/// <returns>True if everything could be read correctly, false otherwise.</returns> /// <returns>True if everything could be read correctly, false otherwise.</returns>
public static FileSwap CreateSwap( ResourceType type, Func< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func<Utf8GamePath, FullPath> redirections,
string? swapFromPreChange = null ) string swapFromRequest, string swapToRequest,
string? swapFromPreChange = null)
{ {
var swap = new FileSwap var swap = new FileSwap
{ {
@ -131,49 +133,25 @@ public sealed class FileSwap : Swap
SwapToModded = FullPath.Empty, SwapToModded = FullPath.Empty,
}; };
if( swapFromRequest.Length == 0 if (swapFromRequest.Length == 0
|| swapToRequest.Length == 0 || swapToRequest.Length == 0
|| !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath ) || !Utf8GamePath.FromString(swapToRequest, out swap.SwapToRequestPath)
|| !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) ) || !Utf8GamePath.FromString(swapFromRequest, out swap.SwapFromRequestPath))
{ throw new Exception($"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\".");
throw new Exception( $"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"." );
}
swap.SwapToModded = redirections( swap.SwapToRequestPath ); swap.SwapToModded = redirections(swap.SwapToRequestPath);
swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() ); swap.SwapToModdedExistsInGame =
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path ); !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists(swap.SwapToModded.InternalName.ToString());
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals(swap.SwapFromRequestPath.Path);
swap.FileData = type switch swap.FileData = type switch
{ {
ResourceType.Mdl => LoadMdl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), ResourceType.Mdl => LoadMdl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
ResourceType.Mtrl => LoadMtrl( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), ResourceType.Mtrl => LoadMtrl(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
ResourceType.Avfx => LoadAvfx( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), ResourceType.Avfx => LoadAvfx(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
_ => LoadFile( swap.SwapToModded, out var f ) ? f : throw new MissingFileException( type, swap.SwapToModded ), _ => LoadFile(manager, swap.SwapToModded, out var f) ? f : throw new MissingFileException(type, swap.SwapToModded),
}; };
return swap; return swap;
} }
/// <summary>
/// Convert a single file redirection to use the file name and extension given by type and the files SHA256 hash, if possible.
/// </summary>
/// <param name="redirections">The set of redirections that need to be considered.</param>
/// <param name="path">The in- and output path for a file</param>
/// <param name="dataWasChanged">Will be set to true if <paramref name="path"/> was changed.</param>
/// <param name="swap">Will be updated.</param>
public static bool CreateShaRedirection( Func< Utf8GamePath, FullPath > redirections, ref string path, ref bool dataWasChanged, ref FileSwap swap )
{
var oldFilename = Path.GetFileName( path );
var hash = SHA256.HashData( swap.FileData.Write() );
var name =
$"{( oldFilename.StartsWith( "--" ) ? "--" : string.Empty )}{string.Join( null, hash.Select( c => c.ToString( "x2" ) ) )}.{swap.Type.ToString().ToLowerInvariant()}";
var newPath = path.Replace( oldFilename, name );
var newSwap = CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString() );
path = newPath;
dataWasChanged = true;
swap = newSwap;
return true;
}
} }

View file

@ -5,6 +5,7 @@ using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using OtterGui; using OtterGui;
using Penumbra.Api.Enums; using Penumbra.Api.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -109,11 +110,11 @@ public partial class Mod
} }
} }
public void WriteAllTexToolsMeta() public void WriteAllTexToolsMeta(MetaFileManager manager)
{ {
try try
{ {
_default.WriteTexToolsMeta(ModPath); _default.WriteTexToolsMeta(manager, ModPath);
foreach (var group in Groups) foreach (var group in Groups)
{ {
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name); var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
@ -126,7 +127,7 @@ public partial class Mod
if (!optionDir.Exists) if (!optionDir.Exists)
optionDir.Create(); optionDir.Create();
option.WriteTexToolsMeta(optionDir); option.WriteTexToolsMeta(manager, optionDir);
} }
} }
} }

View file

@ -5,6 +5,7 @@ using System.IO;
using System.Linq; using System.Linq;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using Penumbra.Import; using Penumbra.Import;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -20,12 +21,12 @@ namespace Penumbra.Mods;
/// Nothing is checked for existence or validity when loading. /// Nothing is checked for existence or validity when loading.
/// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides. /// Objects are also not checked for uniqueness, the first appearance of a game path or meta path decides.
/// </summary> /// </summary>
public sealed class SubMod : ISubMod public sealed class SubMod : ISubMod
{ {
public string Name { get; set; } = "Default"; public string Name { get; set; } = "Default";
public string FullName public string FullName
=> GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[ GroupIdx ].Name}: {Name}"; => GroupIdx < 0 ? "Default Option" : $"{ParentMod.Groups[GroupIdx].Name}: {Name}";
public string Description { get; set; } = string.Empty; public string Description { get; set; } = string.Empty;
@ -36,180 +37,165 @@ namespace Penumbra.Mods;
public bool IsDefault public bool IsDefault
=> GroupIdx < 0; => GroupIdx < 0;
public Dictionary< Utf8GamePath, FullPath > FileData = new(); public Dictionary<Utf8GamePath, FullPath> FileData = new();
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new(); public Dictionary<Utf8GamePath, FullPath> FileSwapData = new();
public HashSet< MetaManipulation > ManipulationData = new(); public HashSet<MetaManipulation> ManipulationData = new();
public SubMod( IMod parentMod ) public SubMod(IMod parentMod)
=> ParentMod = parentMod; => ParentMod = parentMod;
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
=> FileData; => FileData;
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
=> FileSwapData; => FileSwapData;
public IReadOnlySet< MetaManipulation > Manipulations public IReadOnlySet<MetaManipulation> Manipulations
=> ManipulationData; => ManipulationData;
public void SetPosition( int groupIdx, int optionIdx ) public void SetPosition(int groupIdx, int optionIdx)
{ {
GroupIdx = groupIdx; GroupIdx = groupIdx;
OptionIdx = optionIdx; OptionIdx = optionIdx;
} }
public void Load( DirectoryInfo basePath, JToken json, out int priority ) public void Load(DirectoryInfo basePath, JToken json, out int priority)
{ {
FileData.Clear(); FileData.Clear();
FileSwapData.Clear(); FileSwapData.Clear();
ManipulationData.Clear(); ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options. // Every option has a name, but priorities are only relevant for multi group options.
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty; Name = json[nameof(ISubMod.Name)]?.ToObject<string>() ?? string.Empty;
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty; Description = json[nameof(ISubMod.Description)]?.ToObject<string>() ?? string.Empty;
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0; priority = json[nameof(IModGroup.Priority)]?.ToObject<int>() ?? 0;
var files = ( JObject? )json[ nameof( Files ) ]; var files = (JObject?)json[nameof(Files)];
if( files != null ) if (files != null)
foreach (var property in files.Properties())
{ {
foreach( var property in files.Properties() ) if (Utf8GamePath.FromString(property.Name, out var p, true))
{ FileData.TryAdd(p, new FullPath(basePath, property.Value.ToObject<Utf8RelPath>()));
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileData.TryAdd( p, new FullPath( basePath, property.Value.ToObject< Utf8RelPath >() ) );
}
}
} }
var swaps = ( JObject? )json[ nameof( FileSwaps ) ]; var swaps = (JObject?)json[nameof(FileSwaps)];
if( swaps != null ) if (swaps != null)
foreach (var property in swaps.Properties())
{ {
foreach( var property in swaps.Properties() ) if (Utf8GamePath.FromString(property.Name, out var p, true))
{ FileSwapData.TryAdd(p, new FullPath(property.Value.ToObject<string>()!));
if( Utf8GamePath.FromString( property.Name, out var p, true ) )
{
FileSwapData.TryAdd( p, new FullPath( property.Value.ToObject< string >()! ) );
}
}
} }
var manips = json[ nameof( Manipulations ) ]; var manips = json[nameof(Manipulations)];
if( manips != null ) if (manips != null)
{ foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) ) .Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
{ ManipulationData.Add(s);
ManipulationData.Add( s );
}
}
} }
// If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod. // If .meta or .rgsp files are encountered, parse them and incorporate their meta changes into the mod.
// If delete is true, the files are deleted afterwards. // If delete is true, the files are deleted afterwards.
public (bool Changes, List< string > DeleteList) IncorporateMetaChanges( DirectoryInfo basePath, bool delete ) public (bool Changes, List<string> DeleteList) IncorporateMetaChanges(DirectoryInfo basePath, bool delete)
{ {
var deleteList = new List< string >(); var deleteList = new List<string>();
var oldSize = ManipulationData.Count; var oldSize = ManipulationData.Count;
var deleteString = delete ? "with deletion." : "without deletion."; var deleteString = delete ? "with deletion." : "without deletion.";
foreach( var (key, file) in Files.ToList() ) foreach (var (key, file) in Files.ToList())
{ {
var ext1 = key.Extension().AsciiToLower().ToString(); var ext1 = key.Extension().AsciiToLower().ToString();
var ext2 = file.Extension.ToLowerInvariant(); var ext2 = file.Extension.ToLowerInvariant();
try try
{ {
if( ext1 == ".meta" || ext2 == ".meta" ) if (ext1 == ".meta" || ext2 == ".meta")
{
FileData.Remove( key );
if( !file.Exists )
{ {
FileData.Remove(key);
if (!file.Exists)
continue; continue;
}
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName),
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" ); Penumbra.Config.KeepDefaultMetaChanges);
deleteList.Add( file.FullName ); Penumbra.Log.Verbose(
ManipulationData.UnionWith( meta.MetaManipulations ); $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith(meta.MetaManipulations);
} }
else if( ext1 == ".rgsp" || ext2 == ".rgsp" ) else if (ext1 == ".rgsp" || ext2 == ".rgsp")
{
FileData.Remove( key );
if( !file.Exists )
{ {
FileData.Remove(key);
if (!file.Exists)
continue; continue;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges ); var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" ); Penumbra.Config.KeepDefaultMetaChanges);
deleteList.Add( file.FullName ); Penumbra.Log.Verbose(
$"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}");
deleteList.Add(file.FullName);
ManipulationData.UnionWith( rgsp.MetaManipulations ); ManipulationData.UnionWith(rgsp.MetaManipulations);
} }
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}" ); Penumbra.Log.Error($"Could not incorporate meta changes in mod {basePath} from file {file.FullName}:\n{e}");
} }
} }
DeleteDeleteList( deleteList, delete ); DeleteDeleteList(deleteList, delete);
return ( oldSize < ManipulationData.Count, deleteList ); return (oldSize < ManipulationData.Count, deleteList);
} }
internal static void DeleteDeleteList( IEnumerable< string > deleteList, bool delete ) internal static void DeleteDeleteList(IEnumerable<string> deleteList, bool delete)
{
if( !delete )
{ {
if (!delete)
return; return;
}
foreach( var file in deleteList ) foreach (var file in deleteList)
{ {
try try
{ {
File.Delete( file ); File.Delete(file);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not delete incorporated meta file {file}:\n{e}" ); Penumbra.Log.Error($"Could not delete incorporated meta file {file}:\n{e}");
} }
} }
} }
public void WriteTexToolsMeta( DirectoryInfo basePath, bool test = false ) public void WriteTexToolsMeta(MetaFileManager manager, DirectoryInfo basePath, bool test = false)
{ {
var files = TexToolsMeta.ConvertToTexTools( Manipulations ); var files = TexToolsMeta.ConvertToTexTools(manager, Manipulations);
foreach( var (file, data) in files ) foreach (var (file, data) in files)
{ {
var path = Path.Combine( basePath.FullName, file ); var path = Path.Combine(basePath.FullName, file);
try try
{ {
Directory.CreateDirectory( Path.GetDirectoryName( path )! ); Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllBytes( path, data ); File.WriteAllBytes(path, data);
} }
catch( Exception e ) catch (Exception e)
{ {
Penumbra.Log.Error( $"Could not write meta file {path}:\n{e}" ); Penumbra.Log.Error($"Could not write meta file {path}:\n{e}");
} }
} }
if( test ) if (test)
{ TestMetaWriting(manager, files);
TestMetaWriting( files );
}
} }
[Conditional("DEBUG" )] [Conditional("DEBUG")]
private void TestMetaWriting( Dictionary< string, byte[] > files ) private void TestMetaWriting(MetaFileManager manager, Dictionary<string, byte[]> files)
{ {
var meta = new HashSet< MetaManipulation >( Manipulations.Count ); var meta = new HashSet<MetaManipulation>(Manipulations.Count);
foreach( var (file, data) in files ) foreach (var (file, data) in files)
{ {
try try
{ {
var x = file.EndsWith( "rgsp" ) var x = file.EndsWith("rgsp")
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges ) ? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges)
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges ); : new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges);
meta.UnionWith( x.MetaManipulations ); meta.UnionWith(x.MetaManipulations);
} }
catch catch
{ {
@ -217,27 +203,21 @@ namespace Penumbra.Mods;
} }
} }
if( !Manipulations.SetEquals( meta ) ) if (!Manipulations.SetEquals(meta))
{ {
Penumbra.Log.Information( "Meta Sets do not equal." ); Penumbra.Log.Information("Meta Sets do not equal.");
foreach( var (m1, m2) in Manipulations.Zip( meta ) ) foreach (var (m1, m2) in Manipulations.Zip(meta))
{ Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}");
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
}
foreach( var m in Manipulations.Skip( meta.Count ) ) foreach (var m in Manipulations.Skip(meta.Count))
{ Penumbra.Log.Information($"{m} {m.EntryToString()} ");
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach( var m in meta.Skip( Manipulations.Count ) ) foreach (var m in meta.Skip(Manipulations.Count))
{ Penumbra.Log.Information($"{m} {m.EntryToString()} ");
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
} }
else else
{ {
Penumbra.Log.Information( "Meta Sets are equal." ); Penumbra.Log.Information("Meta Sets are equal.");
} }
} }
} }

View file

@ -28,6 +28,7 @@ using Penumbra.Interop.Services;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Meta;
namespace Penumbra; namespace Penumbra;

View file

@ -23,6 +23,7 @@ using CharacterUtility = Penumbra.Interop.Services.CharacterUtility;
using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector; using ModFileSystemSelector = Penumbra.UI.ModsTab.ModFileSystemSelector;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.Collections.Cache; using Penumbra.Collections.Cache;
using Penumbra.Meta;
namespace Penumbra; namespace Penumbra;
@ -66,7 +67,6 @@ public class PenumbraNew
// Add Game Services // Add Game Services
services.AddSingleton<GameEventManager>() services.AddSingleton<GameEventManager>()
.AddSingleton<FrameworkManager>() .AddSingleton<FrameworkManager>()
.AddSingleton<MetaFileManager>()
.AddSingleton<CutsceneService>() .AddSingleton<CutsceneService>()
.AddSingleton<CharacterUtility>() .AddSingleton<CharacterUtility>()
.AddSingleton<ResourceManagerService>() .AddSingleton<ResourceManagerService>()
@ -78,10 +78,6 @@ public class PenumbraNew
.AddSingleton<FontReloader>() .AddSingleton<FontReloader>()
.AddSingleton<RedrawService>(); .AddSingleton<RedrawService>();
// Add PathResolver
services.AddSingleton<CutsceneService>()
.AddSingleton<IdentifiedCollectionCache>();
// Add Configuration // Add Configuration
services.AddTransient<ConfigMigrationService>() services.AddTransient<ConfigMigrationService>()
.AddSingleton<Configuration>(); .AddSingleton<Configuration>();
@ -109,7 +105,8 @@ public class PenumbraNew
// Add Resource services // Add Resource services
services.AddSingleton<ResourceLoader>() services.AddSingleton<ResourceLoader>()
.AddSingleton<ResourceWatcher>() .AddSingleton<ResourceWatcher>()
.AddSingleton<ResourceTreeFactory>(); .AddSingleton<ResourceTreeFactory>()
.AddSingleton<MetaFileManager>();
// Add Path Resolver // Add Path Resolver
services.AddSingleton<AnimationHookService>() services.AddSingleton<AnimationHookService>()

View file

@ -15,6 +15,7 @@ using Penumbra.Collections;
using Penumbra.Collections.Manager; using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums; using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs; using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.ItemSwap; using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
@ -30,16 +31,18 @@ public class ItemSwapTab : IDisposable, ITab
private readonly CollectionManager _collectionManager; private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager; private readonly ModManager _modManager;
private readonly Configuration _config; private readonly Configuration _config;
private readonly MetaFileManager _metaFileManager;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager, public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
ModManager modManager, Configuration config, IdentifierService identifier) ModManager modManager, Configuration config, IdentifierService identifier, MetaFileManager metaFileManager)
{ {
_communicator = communicator; _communicator = communicator;
_itemService = itemService; _itemService = itemService;
_collectionManager = collectionManager; _collectionManager = collectionManager;
_modManager = modManager; _modManager = modManager;
_config = config; _config = config;
_swapData = new ItemSwapContainer(identifier.AwaitedService); _metaFileManager = metaFileManager;
_swapData = new ItemSwapContainer(metaFileManager, identifier.AwaitedService);
_selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)> _selectors = new Dictionary<SwapType, (ItemSelector Source, ItemSelector Target, string TextFrom, string TextTo)>
{ {
@ -215,22 +218,22 @@ public class ItemSwapTab : IDisposable, ITab
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Hair when _targetId > 0 && _sourceId > 0: case SwapType.Hair when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, _swapData.LoadCustomization(_metaFileManager, BodySlot.Hair, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId, (SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Face when _targetId > 0 && _sourceId > 0: case SwapType.Face when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, _swapData.LoadCustomization(_metaFileManager, BodySlot.Face, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId, (SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Ears when _targetId > 0 && _sourceId > 0: case SwapType.Ears when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId, _swapData.LoadCustomization(_metaFileManager, BodySlot.Zear, Names.CombinedRace(_currentGender, ModelRace.Viera), (SetId)_sourceId,
(SetId)_targetId, (SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
case SwapType.Tail when _targetId > 0 && _sourceId > 0: case SwapType.Tail when _targetId > 0 && _sourceId > 0:
_swapData.LoadCustomization(BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId, _swapData.LoadCustomization(_metaFileManager, BodySlot.Tail, Names.CombinedRace(_currentGender, _currentRace), (SetId)_sourceId,
(SetId)_targetId, (SetId)_targetId,
_useCurrentCollection ? _collectionManager.Active.Current : null); _useCurrentCollection ? _collectionManager.Active.Current : null);
break; break;
@ -312,7 +315,8 @@ public class ItemSwapTab : IDisposable, ITab
optionCreated = true; optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName); optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
dirCreated = true; dirCreated = true;
if (!_swapData.WriteMod(_modManager, _mod, _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps, if (!_swapData.WriteMod(_modManager, _mod,
_useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps,
optionFolderName, optionFolderName,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1)) _mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
throw new Exception("Failure writing files for mod swap."); throw new Exception("Failure writing files for mod swap.");
@ -751,7 +755,6 @@ public class ItemSwapTab : IDisposable, ITab
private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited) private void OnSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool inherited)
{ {
if (collection != _collectionManager.Active.Current || mod != _mod) if (collection != _collectionManager.Active.Current || mod != _mod)
return; return;
@ -773,6 +776,7 @@ public class ItemSwapTab : IDisposable, ITab
{ {
if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod) if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod)
return; return;
_swapData.LoadMod(_mod, _modSettings); _swapData.LoadMod(_mod, _modSettings);
UpdateOption(); UpdateOption();
_dirty = true; _dirty = true;

View file

@ -9,6 +9,7 @@ using OtterGui.Raii;
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;
using Penumbra.Meta.Files; using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations; using Penumbra.Meta.Manipulations;
using Penumbra.Mods; using Penumbra.Mods;
@ -62,7 +63,7 @@ public partial class ModEditWindow
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine()); CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine());
ImGui.SameLine(); ImGui.SameLine();
if (ImGui.Button("Write as TexTools Files")) if (ImGui.Button("Write as TexTools Files"))
_mod!.WriteAllTexToolsMeta(); _mod!.WriteAllTexToolsMeta(_metaFileManager);
using var child = ImRaii.Child("##meta", -Vector2.One, true); using var child = ImRaii.Child("##meta", -Vector2.One, true);
if (!child) if (!child)
@ -77,9 +78,9 @@ public partial class ModEditWindow
} }
// The headers for the different meta changes all have basically the same structure for different types. /// <summary> The headers for the different meta changes all have basically the same structure for different types.</summary>
private void DrawEditHeader<T>(IReadOnlyCollection<T> items, string label, int numColumns, Action<T, ModEditor, Vector2> draw, private void DrawEditHeader<T>(IReadOnlyCollection<T> items, string label, int numColumns, Action<MetaFileManager, T, ModEditor, Vector2> draw,
Action<ModEditor, Vector2> drawNew) Action<MetaFileManager, ModEditor, Vector2> drawNew)
{ {
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV; const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
if (!ImGui.CollapsingHeader($"{items.Count} {label}")) if (!ImGui.CollapsingHeader($"{items.Count} {label}"))
@ -89,11 +90,11 @@ public partial class ModEditWindow
{ {
if (table) if (table)
{ {
drawNew(_editor, _iconSize); drawNew(_metaFileManager, _editor, _iconSize);
foreach (var (item, index) in items.ToArray().WithIndex()) foreach (var (item, index) in items.ToArray().WithIndex())
{ {
using var id = ImRaii.PushId(index); using var id = ImRaii.PushId(index);
draw(item, _editor, _iconSize); draw(_metaFileManager, item, _editor, _iconSize);
} }
} }
} }
@ -108,7 +109,7 @@ public partial class ModEditWindow
private static float IdWidth private static float IdWidth
=> 100 * UiHelpers.Scale; => 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize,
@ -116,20 +117,20 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new); var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedEqpFile.GetDefault(_new.SetId); var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry)); editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier // Identifier
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) if (IdInput("##eqpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), _new.Slot, setId); _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), _new.Slot, setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip); ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot)) if (Combos.EqpEquipSlot("##eqpSlot", 100, _new.Slot, out var slot))
_new = new EqpManipulation(ExpandedEqpFile.GetDefault(setId), slot, _new.SetId); _new = new EqpManipulation(ExpandedEqpFile.GetDefault(metaFileManager, setId), slot, _new.SetId);
ImGuiUtil.HoverTooltip(EquipSlotTooltip); ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -148,7 +149,7 @@ public partial class ModEditWindow
ImGui.NewLine(); ImGui.NewLine();
} }
public static void Draw(EqpManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, EqpManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -157,7 +158,7 @@ public partial class ModEditWindow
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString()); ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId); var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X); ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
@ -192,7 +193,7 @@ public partial class ModEditWindow
private static float IdWidth private static float IdWidth
=> 100 * UiHelpers.Scale; => 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize,
@ -204,7 +205,7 @@ public partial class ModEditWindow
var tt = canAdd ? "Stage this edit." : var tt = canAdd ? "Stage this edit." :
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used."; validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
var defaultEntry = validRaceCode var defaultEntry = validRaceCode
? ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId) ? ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId)
: 0; : 0;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry)); editor.MetaEditor.Add(_new.Copy(defaultEntry));
@ -213,7 +214,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) if (IdInput("##eqdpId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{ {
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId); var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), _new.Slot.IsAccessory(), setId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, _new.Race, setId);
} }
@ -222,7 +223,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.Race("##eqdpRace", _new.Race, out var race)) if (Combos.Race("##eqdpRace", _new.Race, out var race))
{ {
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId); var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, race), _new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId);
} }
@ -231,7 +232,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.Gender("##eqdpGender", _new.Gender, out var gender)) if (Combos.Gender("##eqdpGender", _new.Gender, out var gender))
{ {
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId); var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(gender, _new.Race), _new.Slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId); _new = new EqdpManipulation(newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId);
} }
@ -240,7 +241,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot)) if (Combos.EqdpEquipSlot("##eqdpSlot", _new.Slot, out var slot))
{ {
var newDefaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId); var newDefaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(_new.Gender, _new.Race), slot.IsAccessory(), _new.SetId);
_new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId); _new = new EqdpManipulation(newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId);
} }
@ -255,7 +256,7 @@ public partial class ModEditWindow
Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _); Checkmark("Model##eqdpCheck2", string.Empty, bit2, bit2, out _);
} }
public static void Draw(EqdpManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, EqdpManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -278,7 +279,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(EquipSlotTooltip); ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// Values // Values
var defaultEntry = ExpandedEqdpFile.GetDefault(Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId); var defaultEntry = ExpandedEqdpFile.GetDefault(metaFileManager, Names.CombinedRace(meta.Gender, meta.Race), meta.Slot.IsAccessory(), meta.SetId);
var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot); var (defaultBit1, defaultBit2) = defaultEntry.ToBits(meta.Slot);
var (bit1, bit2) = meta.Entry.ToBits(meta.Slot); var (bit1, bit2) = meta.Entry.ToBits(meta.Slot);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -301,12 +302,12 @@ public partial class ModEditWindow
private static float SmallIdWidth private static float SmallIdWidth
=> 45 * UiHelpers.Scale; => 45 * UiHelpers.Scale;
// Convert throwing to null-return if the file does not exist. /// <summary> Convert throwing to null-return if the file does not exist. </summary>
private static ImcEntry? GetDefault(ImcManipulation imc) private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc)
{ {
try try
{ {
return ImcFile.GetDefault(imc.GamePath(), imc.EquipSlot, imc.Variant, out _); return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _);
} }
catch catch
{ {
@ -314,13 +315,13 @@ public partial class ModEditWindow
} }
} }
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize,
editor.MetaEditor.Imc.Select(m => (MetaManipulation)m)); editor.MetaEditor.Imc.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var defaultEntry = GetDefault(_new); var defaultEntry = GetDefault(metaFileManager, _new);
var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new); var canAdd = defaultEntry != null && editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited."; var tt = canAdd ? "Stage this edit." : defaultEntry == null ? "This IMC file does not exist." : "This entry is already edited.";
defaultEntry ??= new ImcEntry(); defaultEntry ??= new ImcEntry();
@ -347,7 +348,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1)) if (IdInput("##imcId", IdWidth, _new.PrimaryId, out var setId, 0, ushort.MaxValue, _new.PrimaryId <= 1))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, setId, _new.SecondaryId, _new.Variant, _new.EquipSlot, _new.Entry)
.Copy(GetDefault(_new) .Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGuiUtil.HoverTooltip(PrimaryIdTooltip); ImGuiUtil.HoverTooltip(PrimaryIdTooltip);
@ -361,7 +362,7 @@ public partial class ModEditWindow
{ {
if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot)) if (Combos.EqpEquipSlot("##imcSlot", 100, _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new) .Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip); ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -370,7 +371,7 @@ public partial class ModEditWindow
{ {
if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot)) if (Combos.AccessorySlot("##imcSlot", _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new) .Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip); ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -379,7 +380,7 @@ public partial class ModEditWindow
{ {
if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false)) if (IdInput("##imcId2", 100 * UiHelpers.Scale, _new.SecondaryId, out var setId2, 0, ushort.MaxValue, false))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, setId2, _new.Variant, _new.EquipSlot, _new.Entry)
.Copy(GetDefault(_new) .Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGuiUtil.HoverTooltip(SecondaryIdTooltip); ImGuiUtil.HoverTooltip(SecondaryIdTooltip);
@ -388,7 +389,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false)) if (IdInput("##imcVariant", SmallIdWidth, _new.Variant, out var variant, 0, byte.MaxValue, false))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot, _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, variant, _new.EquipSlot,
_new.Entry).Copy(GetDefault(_new) _new.Entry).Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGui.TableNextColumn(); ImGui.TableNextColumn();
@ -396,7 +397,7 @@ public partial class ModEditWindow
{ {
if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot)) if (Combos.EqpEquipSlot("##imcSlot", 70, _new.EquipSlot, out var slot))
_new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry) _new = new ImcManipulation(_new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry)
.Copy(GetDefault(_new) .Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry()); ?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip); ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -438,7 +439,7 @@ public partial class ModEditWindow
ImGui.NewLine(); ImGui.NewLine();
} }
public static void Draw(ImcManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, ImcManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -479,7 +480,7 @@ public partial class ModEditWindow
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y)); new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var defaultEntry = GetDefault(meta) ?? new ImcEntry(); var defaultEntry = GetDefault(metaFileManager, meta) ?? new ImcEntry();
if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId, if (IntDragInput("##imcMaterialId", $"Material ID\nDefault Value: {defaultEntry.MaterialId}", SmallIdWidth, meta.Entry.MaterialId,
defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f)) defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId })); editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId }));
@ -530,7 +531,7 @@ public partial class ModEditWindow
private static float IdWidth private static float IdWidth
=> 100 * UiHelpers.Scale; => 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize,
@ -538,7 +539,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new); var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); var defaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry)); editor.MetaEditor.Add(_new.Copy(defaultEntry));
@ -546,7 +547,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) if (IdInput("##estId", IdWidth, _new.SetId, out var setId, 0, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
{ {
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId); var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, _new.Race), setId);
_new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry); _new = new EstManipulation(_new.Gender, _new.Race, _new.Slot, setId, newDefaultEntry);
} }
@ -555,7 +556,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.Race("##estRace", _new.Race, out var race)) if (Combos.Race("##estRace", _new.Race, out var race))
{ {
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId); var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(_new.Gender, race), _new.SetId);
_new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry); _new = new EstManipulation(_new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry);
} }
@ -564,7 +565,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.Gender("##estGender", _new.Gender, out var gender)) if (Combos.Gender("##estGender", _new.Gender, out var gender))
{ {
var newDefaultEntry = EstFile.GetDefault(_new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId); var newDefaultEntry = EstFile.GetDefault(metaFileManager, _new.Slot, Names.CombinedRace(gender, _new.Race), _new.SetId);
_new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry); _new = new EstManipulation(gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry);
} }
@ -573,7 +574,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.EstSlot("##estSlot", _new.Slot, out var slot)) if (Combos.EstSlot("##estSlot", _new.Slot, out var slot))
{ {
var newDefaultEntry = EstFile.GetDefault(slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId); var newDefaultEntry = EstFile.GetDefault(metaFileManager, slot, Names.CombinedRace(_new.Gender, _new.Race), _new.SetId);
_new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry); _new = new EstManipulation(_new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry);
} }
@ -585,7 +586,7 @@ public partial class ModEditWindow
IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f); IntDragInput("##estSkeleton", "Skeleton Index", IdWidth, _new.Entry, defaultEntry, out _, 0, ushort.MaxValue, 0.05f);
} }
public static void Draw(EstManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, EstManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -608,7 +609,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(EstTypeTooltip); ImGuiUtil.HoverTooltip(EstTypeTooltip);
// Values // Values
var defaultEntry = EstFile.GetDefault(meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId); var defaultEntry = EstFile.GetDefault(metaFileManager, meta.Slot, Names.CombinedRace(meta.Gender, meta.Race), meta.SetId);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry, if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
out var entry, 0, ushort.MaxValue, 0.05f)) out var entry, 0, ushort.MaxValue, 0.05f))
@ -629,7 +630,7 @@ public partial class ModEditWindow
private static float IdWidth private static float IdWidth
=> 100 * UiHelpers.Scale; => 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize,
@ -637,14 +638,14 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new); var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = ExpandedGmpFile.GetDefault(_new.SetId); var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, _new.SetId);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry)); editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier // Identifier
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1)) if (IdInput("##gmpId", IdWidth, _new.SetId, out var setId, 1, ExpandedEqpGmpBase.Count - 1, _new.SetId <= 1))
_new = new GmpManipulation(ExpandedGmpFile.GetDefault(setId), setId); _new = new GmpManipulation(ExpandedGmpFile.GetDefault(metaFileManager, setId), setId);
ImGuiUtil.HoverTooltip(ModelSetIdTooltip); ImGuiUtil.HoverTooltip(ModelSetIdTooltip);
@ -669,7 +670,7 @@ public partial class ModEditWindow
IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f); IntDragInput("##gmpUnkB", "Animation Type B?", UnkWidth, defaultEntry.UnknownB, defaultEntry.UnknownB, out _, 0, 15, 0f);
} }
public static void Draw(GmpManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, GmpManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -680,7 +681,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort); ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
// Values // Values
var defaultEntry = ExpandedGmpFile.GetDefault(meta.SetId); var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled)) if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled })); editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled }));
@ -723,7 +724,7 @@ public partial class ModEditWindow
private static float FloatWidth private static float FloatWidth
=> 150 * UiHelpers.Scale; => 150 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize) public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{ {
ImGui.TableNextColumn(); ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize, CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize,
@ -731,20 +732,20 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new); var canAdd = editor.MetaEditor.CanAdd(_new);
var tt = canAdd ? "Stage this edit." : "This entry is already edited."; var tt = canAdd ? "Stage this edit." : "This entry is already edited.";
var defaultEntry = CmpFile.GetDefault(_new.SubRace, _new.Attribute); var defaultEntry = CmpFile.GetDefault(metaFileManager, _new.SubRace, _new.Attribute);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true)) if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry)); editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier // Identifier
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace)) if (Combos.SubRace("##rspSubRace", _new.SubRace, out var subRace))
_new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(subRace, _new.Attribute)); _new = new RspManipulation(subRace, _new.Attribute, CmpFile.GetDefault(metaFileManager, subRace, _new.Attribute));
ImGuiUtil.HoverTooltip(RacialTribeTooltip); ImGuiUtil.HoverTooltip(RacialTribeTooltip);
ImGui.TableNextColumn(); ImGui.TableNextColumn();
if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute)) if (Combos.RspAttribute("##rspAttribute", _new.Attribute, out var attribute))
_new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(subRace, attribute)); _new = new RspManipulation(_new.SubRace, attribute, CmpFile.GetDefault(metaFileManager, subRace, attribute));
ImGuiUtil.HoverTooltip(ScalingTypeTooltip); ImGuiUtil.HoverTooltip(ScalingTypeTooltip);
@ -755,7 +756,7 @@ public partial class ModEditWindow
ImGui.DragFloat("##rspValue", ref defaultEntry, 0f); ImGui.DragFloat("##rspValue", ref defaultEntry, 0f);
} }
public static void Draw(RspManipulation meta, ModEditor editor, Vector2 iconSize) public static void Draw(MetaFileManager metaFileManager, RspManipulation meta, ModEditor editor, Vector2 iconSize)
{ {
DrawMetaButtons(meta, editor, iconSize); DrawMetaButtons(meta, editor, iconSize);
@ -771,7 +772,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn(); ImGui.TableNextColumn();
// Values // Values
var def = CmpFile.GetDefault(meta.SubRace, meta.Attribute); var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute);
var value = meta.Entry; var value = meta.Entry;
ImGui.SetNextItemWidth(FloatWidth); ImGui.SetNextItemWidth(FloatWidth);
using var color = ImRaii.PushColor(ImGuiCol.FrameBg, using var color = ImRaii.PushColor(ImGuiCol.FrameBg,

View file

@ -13,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files; using Penumbra.GameData.Files;
using Penumbra.Import.Textures; using Penumbra.Import.Textures;
using Penumbra.Interop.ResourceTree; using Penumbra.Interop.ResourceTree;
using Penumbra.Meta;
using Penumbra.Mods; using Penumbra.Mods;
using Penumbra.Mods.Manager; using Penumbra.Mods.Manager;
using Penumbra.String.Classes; using Penumbra.String.Classes;
@ -31,6 +32,7 @@ public partial class ModEditWindow : Window, IDisposable
private readonly Configuration _config; private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab; private readonly ItemSwapTab _itemSwapTab;
private readonly DataManager _gameData; private readonly DataManager _gameData;
private readonly MetaFileManager _metaFileManager;
private Mod? _mod; private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero; private Vector2 _iconSize = Vector2.Zero;
@ -493,7 +495,7 @@ public partial class ModEditWindow : Window, IDisposable
} }
public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData, public ModEditWindow(PerformanceTracker performance, FileDialogService fileDialog, ItemSwapTab itemSwapTab, DataManager gameData,
Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches) Configuration config, ModEditor editor, ResourceTreeFactory resourceTreeFactory, ModCacheManager modCaches, MetaFileManager metaFileManager)
: base(WindowBaseLabel) : base(WindowBaseLabel)
{ {
_performance = performance; _performance = performance;
@ -501,6 +503,7 @@ public partial class ModEditWindow : Window, IDisposable
_config = config; _config = config;
_editor = editor; _editor = editor;
_modCaches = modCaches; _modCaches = modCaches;
_metaFileManager = metaFileManager;
_gameData = gameData; _gameData = gameData;
_fileDialog = fileDialog; _fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl", _materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",