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.Collections.Generic;
using System.Linq;
using OtterGui.Filesystem;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -10,34 +12,42 @@ namespace Penumbra.Collections.Cache;
public struct CmpCache : IDisposable
{
private CmpFile? _cmpFile = null;
private readonly List< RspManipulation > _cmpManipulations = new();
private CmpFile? _cmpFile = null;
private readonly List<RspManipulation> _cmpManipulations = new();
public CmpCache()
{}
{ }
public void SetFiles(CollectionCacheManager manager)
=> manager.SetFile( _cmpFile, MetaIndex.HumanCmp );
public void SetFiles(MetaFileManager manager)
=> manager.SetFile(_cmpFile, MetaIndex.HumanCmp);
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(CollectionCacheManager manager)
=> manager.TemporarilySetFile( _cmpFile, MetaIndex.HumanCmp );
public CharacterUtility.MetaList.MetaReverter TemporarilySetFiles(MetaFileManager manager)
=> manager.TemporarilySetFile(_cmpFile, MetaIndex.HumanCmp);
public bool ApplyMod( CollectionCacheManager manager, RspManipulation manip )
public void Reset()
{
_cmpManipulations.AddOrReplace( manip );
_cmpFile ??= new CmpFile();
return manip.Apply( _cmpFile );
if (_cmpFile == null)
return;
_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))
return false;
var def = CmpFile.GetDefault( manip.SubRace, manip.Attribute );
manip = new RspManipulation( manip.SubRace, manip.Attribute, def );
return manip.Apply( _cmpFile! );
var def = CmpFile.GetDefault(manager, manip.SubRace, manip.Attribute);
manip = new RspManipulation(manip.SubRace, manip.Attribute, def);
return manip.Apply(_cmpFile!);
}
public void Dispose()
@ -46,4 +56,4 @@ public struct CmpCache : IDisposable
_cmpFile = null;
_cmpManipulations.Clear();
}
}
}

View file

@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
using Penumbra.Api.Enums;
using Penumbra.String.Classes;
using Penumbra.Mods.Manager;
@ -51,7 +52,7 @@ public class CollectionCache : IDisposable
{
_manager = manager;
_collection = collection;
MetaManipulations = new MetaCache(_collection);
MetaManipulations = new MetaCache(manager.MetaFileManager, _collection);
}
public void Dispose()
@ -59,6 +60,9 @@ public class CollectionCache : IDisposable
MetaManipulations.Dispose();
}
~CollectionCache()
=> MetaManipulations.Dispose();
// Resolve a given game path according to this collection.
public FullPath? ResolvePath(Utf8GamePath gameResourcePath)
{
@ -115,6 +119,17 @@ public class CollectionCache : IDisposable
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)
{
RemoveMod(mod, addMetaChanges);
@ -234,7 +249,7 @@ public class CollectionCache : IDisposable
// Inside the same mod, conflicts are not recorded.
private void AddFile(Utf8GamePath path, FullPath file, IMod mod)
{
if (!ModCollection.CheckFullPath(path, file))
if (!CheckFullPath(path, file))
return;
if (ResolvedFiles.TryAdd(path, new ModPath(mod, file)))
@ -387,4 +402,14 @@ public class CollectionCache : IDisposable
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.Enums;
using Penumbra.Collections.Manager;
using Penumbra.Interop.Services;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.Services;
@ -18,16 +18,12 @@ namespace Penumbra.Collections.Cache;
public class CollectionCacheManager : IDisposable
{
private readonly FrameworkManager _framework;
private readonly ActiveCollections _active;
private readonly CommunicatorService _communicator;
private readonly TempModManager _tempMods;
private readonly ModStorage _modStorage;
private readonly ModCacheManager _modCaches;
private readonly Configuration _config;
private readonly ActiveCollections _active;
internal readonly ValidityChecker ValidityChecker;
internal readonly CharacterUtility CharacterUtility;
internal readonly ResidentResourceManager ResidentResources;
internal readonly MetaFileManager MetaFileManager;
private readonly Dictionary<ModCollection, CollectionCache> _caches = new();
@ -37,20 +33,15 @@ public class CollectionCacheManager : IDisposable
public IEnumerable<(ModCollection Collection, CollectionCache Cache)> Active
=> _caches.Where(c => c.Key.Index > ModCollection.Empty.Index).Select(p => (p.Key, p.Value));
public CollectionCacheManager(FrameworkManager framework, ActiveCollections active, CommunicatorService communicator,
CharacterUtility characterUtility, TempModManager tempMods, ModStorage modStorage, Configuration config,
ResidentResourceManager residentResources, ModCacheManager modCaches, ValidityChecker validityChecker)
public CollectionCacheManager(FrameworkManager framework, CommunicatorService communicator,
TempModManager tempMods, ModStorage modStorage, MetaFileManager metaFileManager, ActiveCollections active)
{
_framework = framework;
_active = active;
_communicator = communicator;
CharacterUtility = characterUtility;
_tempMods = tempMods;
_modStorage = modStorage;
_config = config;
ResidentResources = residentResources;
_modCaches = modCaches;
ValidityChecker = validityChecker;
_framework = framework;
_communicator = communicator;
_tempMods = tempMods;
_modStorage = modStorage;
MetaFileManager = metaFileManager;
_active = active;
_communicator.CollectionChange.Subscribe(OnCollectionChange);
_communicator.ModPathChanged.Subscribe(OnModChangeAddition, -100);
@ -61,8 +52,8 @@ public class CollectionCacheManager : IDisposable
_communicator.CollectionInheritanceChanged.Subscribe(OnCollectionInheritanceChange);
CreateNecessaryCaches();
if (!CharacterUtility.Ready)
CharacterUtility.LoadingFinished += IncrementCounters;
if (!MetaFileManager.CharacterUtility.Ready)
MetaFileManager.CharacterUtility.LoadingFinished += IncrementCounters;
}
public void Dispose()
@ -74,7 +65,7 @@ public class CollectionCacheManager : IDisposable
_communicator.ModOptionChanged.Unsubscribe(OnModOptionChange);
_communicator.ModSettingChanged.Unsubscribe(OnModSettingChange);
_communicator.CollectionInheritanceChanged.Unsubscribe(OnCollectionInheritanceChange);
CharacterUtility.LoadingFinished -= IncrementCounters;
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
}
/// <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,
() => CalculateEffectiveFileListInternal(collection));
public bool IsDefault(ModCollection collection)
=> _active.Default == collection;
private void CalculateEffectiveFileListInternal(ModCollection collection)
{
// Skip the empty collection.
@ -139,11 +127,7 @@ public class CollectionCacheManager : IDisposable
++collection.ChangeCounter;
if (_active.Default != collection || !CharacterUtility.Ready || !_config.EnableMods)
return;
ResidentResources.Reload();
cache.MetaManipulations.SetFiles();
MetaFileManager.ApplyDefaultFiles(collection);
}
private void OnCollectionChange(CollectionType type, ModCollection? old, ModCollection? newCollection, string displayName)
@ -238,7 +222,7 @@ public class CollectionCacheManager : IDisposable
{
foreach (var (collection, _) in _caches)
++collection.ChangeCounter;
CharacterUtility.LoadingFinished -= IncrementCounters;
MetaFileManager.CharacterUtility.LoadingFinished -= IncrementCounters;
}
private void OnModSettingChange(ModCollection collection, ModSettingChange type, Mod? mod, int oldValue, int groupIdx, bool _)

View file

@ -1,11 +1,13 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using OtterGui;
using OtterGui.Filesystem;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -19,23 +21,29 @@ public readonly struct EqdpCache : IDisposable
public EqdpCache()
{ }
public void SetFiles(CollectionCacheManager manager)
public void SetFiles(MetaFileManager manager)
{
for (var i = 0; i < CharacterUtilityData.EqdpIndices.Length; ++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);
if ((int)idx == -1)
return null;
var i = CharacterUtilityData.EqdpIndices.IndexOf(idx);
return i != -1 ? manager.TemporarilySetFile(_eqdpFiles[i], idx) : null;
var i = CharacterUtilityData.EqdpIndices.IndexOf(index);
if (i != -1)
manager.SetFile(_eqdpFiles[i], index);
}
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>())
{
@ -46,20 +54,20 @@ public readonly struct EqdpCache : IDisposable
_eqdpManipulations.Clear();
}
public bool ApplyMod(CollectionCacheManager manager, EqdpManipulation manip)
public bool ApplyMod(MetaFileManager manager, EqdpManipulation manip)
{
_eqdpManipulations.AddOrReplace(manip);
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);
}
public bool RevertMod(CollectionCacheManager manager, EqdpManipulation manip)
public bool RevertMod(MetaFileManager manager, EqdpManipulation manip)
{
if (!_eqdpManipulations.Remove(manip))
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())]!;
manip = new EqdpManipulation(def, manip.Slot, manip.Gender, manip.Race, manip.SetId);
return manip.Apply(file);

View file

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

View file

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

View file

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

View file

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

View file

@ -2,19 +2,19 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.Interop.Services;
using Penumbra.Interop.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
using Penumbra.String.Classes;
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 Dictionary<MetaManipulation, IMod> _manipulations = new();
private EqpCache _eqpCache = new();
@ -22,7 +22,7 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
private EstCache _estCache = new();
private GmpCache _gmpCache = new();
private CmpCache _cmpCache = new();
private readonly ImcCache _imcCache;
private readonly ImcCache _imcCache = new();
public bool TryGetValue(MetaManipulation manip, [NotNullWhen(true)] out IMod? mod)
=> _manipulations.TryGetValue(manip, out mod);
@ -39,10 +39,10 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public MetaCache(CollectionCacheManager manager, ModCollection collection)
public MetaCache(MetaFileManager manager, ModCollection collection)
{
_manager = manager;
_imcCache = new ImcCache(collection);
_manager = manager;
_collection = collection;
if (!_manager.CharacterUtility.Ready)
_manager.CharacterUtility.LoadingFinished += ApplyStoredManipulations;
}
@ -54,17 +54,17 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
_estCache.SetFiles(_manager);
_gmpCache.SetFiles(_manager);
_cmpCache.SetFiles(_manager);
_imcCache.SetFiles(_manager, _collection);
_imcCache.SetFiles(_collection);
}
public void Reset()
{
_eqpCache.Reset(_manager);
_eqdpCache.Reset(_manager);
_estCache.Reset(_manager);
_gmpCache.Reset(_manager);
_cmpCache.Reset(_manager);
_imcCache.Reset(_manager, _collection);
_eqpCache.Reset();
_eqdpCache.Reset();
_estCache.Reset();
_gmpCache.Reset();
_cmpCache.Reset();
_imcCache.Reset(_collection);
_manipulations.Clear();
}
@ -80,6 +80,9 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
_manipulations.Clear();
}
~MetaCache()
=> Dispose();
public bool ApplyMod(MetaManipulation manip, IMod mod)
{
if (_manipulations.ContainsKey(manip))
@ -125,11 +128,61 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
_ => false,
};
}
/// <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);
// Use this when CharacterUtility becomes ready.
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()
{
if (!Penumbra.CharacterUtility.Ready)
if (!_manager.CharacterUtility.Ready)
return;
var loaded = 0;
@ -149,29 +202,9 @@ public struct MetaCache : IDisposable, IEnumerable<KeyValuePair<MetaManipulation
? 1
: 0;
}
if (_manager.IsDefault(_collection))
{
SetFiles();
_manager.ResidentResources.Reload();
}
_manager.ApplyDefaultFiles(_collection);
_manager.CharacterUtility.LoadingFinished -= ApplyStoredManipulations;
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)
=> _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.
internal MetaCache? MetaCache
=> _cache?.MetaManipulations;
@ -135,3 +114,8 @@ public partial class ModCollection
=> _cache?.MetaManipulations.TemporarilySetEstFile(type)
?? Penumbra.CharacterUtility.TemporarilyResetResource((MetaIndex)type);
}
public static class CollectionCacheExtensions
{
}

View file

@ -3,7 +3,7 @@ using System.IO;
using Lumina.Extensions;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Import.Structs;
using Penumbra.Import.Structs;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -12,148 +12,128 @@ namespace Penumbra.Import;
public partial class TexToolsMeta
{
// 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.
if( data == null || !metaFileInfo.EquipSlot.IsEquipment() )
{
if (data == null || !metaFileInfo.EquipSlot.IsEquipment())
return;
}
var value = Eqp.FromSlotAndBytes( metaFileInfo.EquipSlot, data );
var def = new EqpManipulation( ExpandedEqpFile.GetDefault( metaFileInfo.PrimaryId ), metaFileInfo.EquipSlot, metaFileInfo.PrimaryId );
var manip = new EqpManipulation( value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId );
if( _keepDefault || def.Entry != manip.Entry )
{
MetaManipulations.Add( manip );
}
var value = Eqp.FromSlotAndBytes(metaFileInfo.EquipSlot, data);
var def = new EqpManipulation(ExpandedEqpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId), metaFileInfo.EquipSlot,
metaFileInfo.PrimaryId);
var manip = new EqpManipulation(value, metaFileInfo.EquipSlot, metaFileInfo.PrimaryId);
if (_keepDefault || def.Entry != manip.Entry)
MetaManipulations.Add(manip);
}
// 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;
}
var num = data.Length / 5;
using var reader = new BinaryReader( new MemoryStream( data ) );
for( var i = 0; i < num; ++i )
using var reader = new BinaryReader(new MemoryStream(data));
for (var i = 0; i < num; ++i)
{
// Use the SE gender/race code.
var gr = ( GenderRace )reader.ReadUInt32();
var gr = (GenderRace)reader.ReadUInt32();
var byteValue = reader.ReadByte();
if( !gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory() )
{
if (!gr.IsValid() || !metaFileInfo.EquipSlot.IsEquipment() && !metaFileInfo.EquipSlot.IsAccessory())
continue;
}
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 value = Eqdp.FromSlotAndBits(metaFileInfo.EquipSlot, (byteValue & 1) == 1, (byteValue & 2) == 2);
var def = new EqdpManipulation(
ExpandedEqdpFile.GetDefault(_metaFileManager, gr, metaFileInfo.EquipSlot.IsAccessory(), metaFileInfo.PrimaryId),
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 )
{
MetaManipulations.Add( manip );
}
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)
MetaManipulations.Add(manip);
}
}
// 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;
}
using var reader = new BinaryReader( new MemoryStream( data ) );
var value = ( GmpEntry )reader.ReadUInt32();
using var reader = new BinaryReader(new MemoryStream(data));
var value = (GmpEntry)reader.ReadUInt32();
value.UnknownTotal = reader.ReadByte();
var def = ExpandedGmpFile.GetDefault( metaFileInfo.PrimaryId );
if( _keepDefault || value != def )
{
MetaManipulations.Add( new GmpManipulation( value, metaFileInfo.PrimaryId ) );
}
var def = ExpandedGmpFile.GetDefault(_metaFileManager, metaFileInfo.PrimaryId);
if (_keepDefault || value != def)
MetaManipulations.Add(new GmpManipulation(value, metaFileInfo.PrimaryId));
}
// 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;
}
var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) );
for( var i = 0; i < num; ++i )
using var reader = new BinaryReader(new MemoryStream(data));
for (var i = 0; i < num; ++i)
{
var gr = ( GenderRace )reader.ReadUInt16();
var gr = (GenderRace)reader.ReadUInt16();
var id = 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.Hair, _) => EstManipulation.EstType.Hair,
(_, EquipSlot.Head) => EstManipulation.EstType.Head,
(_, EquipSlot.Body) => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0,
_ => (EstManipulation.EstType)0,
};
if( !gr.IsValid() || type == 0 )
{
if (!gr.IsValid() || type == 0)
continue;
}
var def = EstFile.GetDefault( type, gr, id );
if( _keepDefault || def != value )
{
MetaManipulations.Add( new EstManipulation( gr.Split().Item1, gr.Split().Item2, type, id, value ) );
}
var def = EstFile.GetDefault(_metaFileManager, type, gr, id);
if (_keepDefault || def != 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.
// 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.
private void DeserializeImcEntries( MetaFileInfo metaFileInfo, byte[]? data )
private void DeserializeImcEntries(MetaFileInfo metaFileInfo, byte[]? data)
{
if( data == null )
{
if (data == null)
return;
}
var num = data.Length / 6;
using var reader = new BinaryReader( new MemoryStream( data ) );
var values = reader.ReadStructures< ImcEntry >( num );
using var reader = new BinaryReader(new MemoryStream(data));
var values = reader.ReadStructures<ImcEntry>(num);
ushort i = 0;
try
{
var manip = new ImcManipulation( metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId, metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot,
new ImcEntry() );
var def = new ImcFile( manip );
var partIdx = ImcFile.PartIndex( manip.EquipSlot ); // Gets turned to unknown for things without equip, and unknown turns to 0.
foreach( var value in values )
var manip = new ImcManipulation(metaFileInfo.PrimaryType, metaFileInfo.SecondaryType, metaFileInfo.PrimaryId,
metaFileInfo.SecondaryId, i, metaFileInfo.EquipSlot,
new ImcEntry());
var def = new ImcFile(_metaFileManager, manip);
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 );
if( imc.Valid )
{
MetaManipulations.Add( imc );
}
var imc = new ImcManipulation(manip.ObjectType, manip.BodySlot, manip.PrimaryId, manip.SecondaryId, i, manip.EquipSlot,
value);
if (imc.Valid)
MetaManipulations.Add(imc);
}
++i;
}
}
catch( Exception e )
catch (Exception e)
{
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"
+ $"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 Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -12,7 +13,7 @@ namespace Penumbra.Import;
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[] >();
foreach( var group in manips.GroupBy( ManipToPath ) )
@ -23,8 +24,8 @@ public partial class TexToolsMeta
}
var bytes = group.Key.EndsWith( ".rgsp" )
? WriteRgspFile( group.Key, group )
: WriteMetaFile( group.Key, group );
? WriteRgspFile( manager, group.Key, group )
: WriteMetaFile( manager, group.Key, group );
if( bytes.Length == 0 )
{
continue;
@ -36,7 +37,7 @@ public partial class TexToolsMeta
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 );
using var m = new MemoryStream( 45 );
@ -54,7 +55,7 @@ public partial class TexToolsMeta
{
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 );
}
}
@ -72,7 +73,7 @@ public partial class TexToolsMeta
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 ) );
@ -103,7 +104,7 @@ public partial class TexToolsMeta
b.Write( ( uint )header );
b.Write( offset );
var size = WriteData( b, offset, header, data );
var size = WriteData( manager, b, offset, header, data );
b.Write( size );
offset += size;
}
@ -111,7 +112,7 @@ public partial class TexToolsMeta
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;
b.Seek( ( int )offset, SeekOrigin.Begin );
@ -120,7 +121,7 @@ public partial class TexToolsMeta
{
case MetaManipulation.Type.Imc:
var allManips = manips.ToList();
var baseFile = new ImcFile( allManips[ 0 ].Imc );
var baseFile = new ImcFile( manager, allManips[ 0 ].Imc );
foreach( var manip in allManips )
{
manip.Imc.Apply( baseFile );

View file

@ -1,6 +1,7 @@
using System;
using System.IO;
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
@ -9,7 +10,7 @@ namespace Penumbra.Import;
public partial class TexToolsMeta
{
// 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 )
{
@ -25,7 +26,7 @@ public partial class TexToolsMeta
var flag = br.ReadByte();
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.
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.
void Add( RspAttribute attribute, float value )
{
var def = CmpFile.GetDefault( subRace, attribute );
var def = CmpFile.GetDefault( manager, subRace, attribute );
if( keepDefault || value != def )
{
ret.MetaManipulations.Add( new RspManipulation( subRace, attribute, value ) );

View file

@ -4,34 +4,37 @@ using System.IO;
using System.Text;
using Penumbra.GameData;
using Penumbra.Import.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
namespace Penumbra.Import;
// TexTools provices custom generated *.meta files for its modpacks, that contain changes to
// - imc files
// - eqp files
// - gmp files
// - est files
// - eqdp files
// 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.
// 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.
/// <summary>TexTools provices custom generated *.meta files for its modpacks, that contain changes to
/// - imc files
/// - eqp files
/// - gmp files
/// - est files
/// - eqdp files
/// 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.
/// 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.</summary>
public partial class TexToolsMeta
{
// An empty TexToolsMeta.
public static readonly TexToolsMeta Invalid = new(string.Empty, 0);
{
/// <summary> An empty TexToolsMeta. </summary>
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.
public readonly uint Version;
public readonly string FilePath;
public readonly List< MetaManipulation > MetaManipulations = new();
private readonly bool _keepDefault = false;
private readonly MetaFileManager _metaFileManager;
public TexToolsMeta( IGamePathParser parser, byte[] data, bool keepDefault )
{
public TexToolsMeta( MetaFileManager metaFileManager, IGamePathParser parser, byte[] data, bool keepDefault )
{
_metaFileManager = metaFileManager;
_keepDefault = keepDefault;
try
{
@ -80,10 +83,11 @@ public partial class TexToolsMeta
}
}
private TexToolsMeta( string filePath, uint version )
private TexToolsMeta( MetaFileManager metaFileManager, string filePath, uint version )
{
FilePath = filePath;
Version = version;
_metaFileManager = metaFileManager;
FilePath = filePath;
Version = version;
}
// Read a null terminated string from a binary reader.

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;
// The human.cmp file contains many character-relevant parameters like color sets.
// We only support manipulating the racial scaling parameters at the moment.
/// <summary>
/// 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 static readonly CharacterUtility.InternalIndex InternalIndex =
CharacterUtility.ReverseIndices[ ( int )MetaIndex.HumanCmp ];
CharacterUtility.ReverseIndices[(int)MetaIndex.HumanCmp];
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 );
set => *( float* )( Data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 ) = value;
get => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4);
set => *(float*)(Data + RacialScalingStart + ToRspIndex(subRace) * RspEntry.ByteSize + (int)attribute * 4) = value;
}
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 )
{
this[ r, a ] = GetDefault( r, a );
}
foreach (var (r, a) in entries)
this[r, a] = GetDefault(Manager, r, a);
}
public CmpFile()
: base( MetaIndex.HumanCmp )
public CmpFile(MetaFileManager manager)
: base(manager, MetaIndex.HumanCmp)
{
AllocateData( DefaultData.Length );
AllocateData(DefaultData.Length);
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;
return *( float* )( data + RacialScalingStart + ToRspIndex( subRace ) * RspEntry.ByteSize + ( int )attribute * 4 );
var data = (byte*)manager.CharacterUtility.DefaultResource(InternalIndex).Address;
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.Midlander => 0,
@ -67,6 +67,6 @@ public sealed unsafe class CmpFile : MetaBaseFile
SubRace.Rava => 70,
SubRace.Veena => 71,
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;
// EQDP file structure:
// [Identifier][BlockSize:ushort][BlockCount:ushort]
// BlockCount x [BlockHeader:ushort]
// Containing offsets for blocks, ushort.Max means collapsed.
// 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.
/// <summary>
/// EQDP file structure:
/// [Identifier][BlockSize:ushort][BlockCount:ushort]
/// BlockCount x [BlockHeader:ushort]
/// Containing offsets for blocks, ushort.Max means collapsed.
/// 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.
/// </summary>
public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
{
private const ushort BlockHeaderSize = 2;
@ -28,117 +29,103 @@ public sealed unsafe class ExpandedEqdpFile : MetaBaseFile
public readonly int DataOffset;
public ushort Identifier
=> *( ushort* )Data;
=> *(ushort*)Data;
public ushort BlockSize
=> *( ushort* )( Data + 2 );
=> *(ushort*)(Data + 2);
public ushort BlockCount
=> *( ushort* )( Data + 4 );
=> *(ushort*)(Data + 4);
public int Count
=> ( Length - DataOffset ) / EqdpEntrySize;
=> (Length - DataOffset) / EqdpEntrySize;
public EqdpEntry this[ int idx ]
public EqdpEntry this[int idx]
{
get
{
if( idx >= Count || idx < 0 )
{
if (idx >= Count || idx < 0)
throw new IndexOutOfRangeException();
}
return ( EqdpEntry )( *( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) );
return (EqdpEntry)(*(ushort*)(Data + DataOffset + EqdpEntrySize * idx));
}
set
{
if( idx >= Count || idx < 0 )
{
if (idx >= Count || idx < 0)
throw new IndexOutOfRangeException();
}
*( ushort* )( Data + DataOffset + EqdpEntrySize * idx ) = ( ushort )value;
*(ushort*)(Data + DataOffset + EqdpEntrySize * idx) = (ushort)value;
}
}
public override void Reset()
{
var def = ( byte* )DefaultData.Data;
MemoryUtility.MemCpyUnchecked( Data, def, IdentifierSize + PreambleSize );
var def = (byte*)DefaultData.Data;
MemoryUtility.MemCpyUnchecked(Data, def, IdentifierSize + PreambleSize);
var controlPtr = ( ushort* )( def + IdentifierSize + PreambleSize );
var controlPtr = (ushort*)(def + IdentifierSize + PreambleSize);
var dataBasePtr = controlPtr + BlockCount;
var myDataPtr = ( ushort* )( Data + IdentifierSize + PreambleSize + 2 * BlockCount );
var myControlPtr = ( ushort* )( Data + IdentifierSize + PreambleSize );
for( var i = 0; i < BlockCount; ++i )
var myDataPtr = (ushort*)(Data + IdentifierSize + PreambleSize + 2 * BlockCount);
var myControlPtr = (ushort*)(Data + IdentifierSize + PreambleSize);
for (var i = 0; i < BlockCount; ++i)
{
if( controlPtr[ i ] == CollapsedBlock )
{
MemoryUtility.MemSet( myDataPtr, 0, BlockSize * EqdpEntrySize );
}
if (controlPtr[i] == CollapsedBlock)
MemoryUtility.MemSet(myDataPtr, 0, BlockSize * EqdpEntrySize);
else
{
MemoryUtility.MemCpyUnchecked( myDataPtr, dataBasePtr + controlPtr[ i ], BlockSize * EqdpEntrySize );
}
MemoryUtility.MemCpyUnchecked(myDataPtr, dataBasePtr + controlPtr[i], BlockSize * EqdpEntrySize);
myControlPtr[ i ] = ( ushort )( i * BlockSize );
myDataPtr += BlockSize;
myControlPtr[i] = (ushort)(i * 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 )
{
this[ entry ] = GetDefault( entry );
}
foreach (var entry in entries)
this[entry] = GetDefault(entry);
}
public ExpandedEqdpFile( GenderRace raceCode, bool accessory )
: base( CharacterUtilityData.EqdpIdx( raceCode, accessory ) )
public ExpandedEqdpFile(MetaFileManager manager, GenderRace raceCode, bool accessory)
: base(manager, CharacterUtilityData.EqdpIdx(raceCode, accessory))
{
var def = ( byte* )DefaultData.Data;
var blockSize = *( ushort* )( def + IdentifierSize );
var totalBlockCount = *( ushort* )( def + IdentifierSize + 2 );
var def = (byte*)DefaultData.Data;
var blockSize = *(ushort*)(def + IdentifierSize);
var totalBlockCount = *(ushort*)(def + IdentifierSize + 2);
var totalBlockSize = blockSize * EqdpEntrySize;
DataOffset = IdentifierSize + PreambleSize + totalBlockCount * BlockHeaderSize;
var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += ( FileAlignment - ( fullLength & ( FileAlignment - 1 ) ) ) & ( FileAlignment - 1 );
AllocateData( fullLength );
var fullLength = DataOffset + totalBlockCount * totalBlockSize;
fullLength += (FileAlignment - (fullLength & (FileAlignment - 1))) & (FileAlignment - 1);
AllocateData(fullLength);
Reset();
}
public EqdpEntry GetDefault( int setIdx )
=> GetDefault( Index, setIdx );
public EqdpEntry GetDefault(int setIdx)
=> GetDefault(Manager, Index, setIdx);
public static EqdpEntry GetDefault( CharacterUtility.InternalIndex idx, int setIdx )
=> GetDefault( ( byte* )Penumbra.CharacterUtility.DefaultResource( idx ).Address, setIdx );
public static EqdpEntry GetDefault(MetaFileManager manager, CharacterUtility.InternalIndex idx, int 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 totalBlockCount = *( ushort* )( data + IdentifierSize + 2 );
var blockSize = *(ushort*)(data + IdentifierSize);
var totalBlockCount = *(ushort*)(data + IdentifierSize + 2);
var blockIdx = setIdx / blockSize;
if( blockIdx >= totalBlockCount )
{
if (blockIdx >= totalBlockCount)
return 0;
}
var block = ( ( ushort* )( data + IdentifierSize + PreambleSize ) )[ blockIdx ];
if( block == CollapsedBlock )
{
var block = ((ushort*)(data + IdentifierSize + PreambleSize))[blockIdx];
if (block == CollapsedBlock)
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);
return (EqdpEntry)(*(blockData + setIdx % blockSize));
}
public static EqdpEntry GetDefault( GenderRace raceCode, bool accessory, int setIdx )
=> GetDefault( CharacterUtility.ReverseIndices[ ( int )CharacterUtilityData.EqdpIdx( raceCode, accessory ) ], setIdx );
}
public static EqdpEntry GetDefault(MetaFileManager manager, GenderRace raceCode, bool accessory, int setIdx)
=> GetDefault(manager, CharacterUtility.ReverseIndices[(int)CharacterUtilityData.EqdpIdx(raceCode, accessory)], setIdx);
}

View file

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

View file

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

View file

@ -3,15 +3,16 @@ using Penumbra.Interop.Structs;
namespace Penumbra.Meta.Files;
// EVP file structure:
// [Identifier:3 bytes, EVP]
// [NumModels:ushort]
// NumModels x [ModelId:ushort]
// Containing the relevant model IDs. Seems to be sorted.
// NumModels x [DataArray]:512 Byte]
// 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.
/// <summary>
/// EVP file structure:
/// [Identifier:3 bytes, EVP]
/// [NumModels:ushort]
/// NumModels x [ModelId:ushort]
/// Containing the relevant model IDs. Seems to be sorted.
/// NumModels x [DataArray]:512 Byte]
/// 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.
/// </summary>
public unsafe class EvpFile : MetaBaseFile
{
public const int FlagArraySize = 512;
@ -26,45 +27,39 @@ public unsafe class EvpFile : MetaBaseFile
}
public int NumModels
=> Data[ 3 ];
=> Data[3];
public ReadOnlySpan< ushort > ModelSetIds
public ReadOnlySpan<ushort> ModelSetIds
=> new(Data + 4, NumModels);
public ushort ModelSetId( int idx )
=> idx >= 0 && idx < NumModels ? ( ( ushort* )( Data + 4 ) )[ idx ] : ushort.MaxValue;
public ushort ModelSetId(int idx)
=> 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);
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;
}
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;
}
if( model > modelSet )
{
if (model > modelSet)
break;
}
return Flags( i )[ arrayIndex ];
return Flags(i)[arrayIndex];
}
return EvpFlag.None;
}
public EvpFile()
: base( ( MetaIndex )1 ) // TODO: Name
public EvpFile(MetaFileManager manager)
: base(manager, (MetaIndex)1) // TODO: Name
{ }
}
}

View file

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

View file

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

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.Files;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.String.Classes;
namespace Penumbra.Mods.ItemSwap;
@ -12,7 +13,7 @@ namespace Penumbra.Mods.ItemSwap;
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.
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 )
{
@ -22,7 +23,7 @@ public static class CustomizationSwap
var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, 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;
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 ) )
{
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 );
}
@ -41,7 +42,7 @@ public static class CustomizationSwap
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 )
{
variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant;
@ -62,20 +63,20 @@ public static class CustomizationSwap
dataWasChanged = true;
}
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath );
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
var mtrl = FileSwap.CreateSwap( manager, ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, actualMtrlFromPath );
var shpk = CreateShader( manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( shpk );
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 );
}
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 )
{
var path = texture.Path;
@ -99,13 +100,13 @@ public static class CustomizationSwap
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}";
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.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
@ -18,41 +19,51 @@ namespace Penumbra.Mods.ItemSwap;
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 )
{
return new[] { slot };
}
if (slot != EquipSlot.RFinger)
return new[]
{
slot,
};
return rFinger
? lFinger
? new[] { EquipSlot.RFinger, EquipSlot.LFinger }
: new[] { EquipSlot.RFinger }
? new[]
{
EquipSlot.RFinger,
EquipSlot.LFinger,
}
: new[]
{
EquipSlot.RFinger,
}
: lFinger
? new[] { EquipSlot.LFinger }
: Array.Empty< EquipSlot >();
? new[]
{
EquipSlot.LFinger,
}
: Array.Empty<EquipSlot>();
}
public static Item[] CreateTypeSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips,
EquipSlot slotFrom, Item itemFrom, EquipSlot slotTo, Item itemTo )
public static Item[] CreateTypeSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
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();
}
var ( imcFileFrom, variants, affectedItems ) = GetVariants( identifier, slotFrom, idFrom, idTo, variantFrom );
var imcManip = new ImcManipulation( slotTo, variantTo, idTo.Value, default );
var imcFileTo = new ImcFile( imcManip );
var (imcFileFrom, variants, affectedItems) = GetVariants(manager, identifier, slotFrom, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation(slotTo, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile(manager, imcManip);
var skipFemale = false;
var skipMale = false;
var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo ) ) ).Imc.Entry.MaterialId;
foreach( var gr in Enum.GetValues< GenderRace >() )
var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo))).Imc.Entry.MaterialId;
foreach (var gr in Enum.GetValues<GenderRace>())
{
switch( gr.Split().Item1 )
switch (gr.Split().Item1)
{
case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue;
@ -60,22 +71,18 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue;
}
if( CharacterUtilityData.EqdpIdx( gr, true ) < 0 )
{
if (CharacterUtilityData.EqdpIdx(gr, true) < 0)
continue;
}
try
{
var eqdp = CreateEqdp( redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo );
if( eqdp != null )
{
swaps.Add( eqdp );
}
var eqdp = CreateEqdp(manager, redirections, manips, slotFrom, slotTo, gr, idFrom, idTo, mtrlVariantTo);
if (eqdp != null)
swaps.Add(eqdp);
}
catch( ItemSwap.MissingFileException e )
catch (ItemSwap.MissingFileException e)
{
switch( gr )
switch (gr)
{
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
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 );
swaps.Add( imc );
var imc = CreateImc(manager, redirections, manips, slotFrom, slotTo, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add(imc);
}
return affectedItems;
}
public static Item[] CreateItemSwap( IObjectIdentifier identifier, List< Swap > swaps, Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, Item itemFrom,
Item itemTo, bool rFinger = true, bool lFinger = true )
public static Item[] CreateItemSwap(MetaFileManager manager, IObjectIdentifier identifier, List<Swap> swaps,
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.
LookupItem( itemFrom, out var slotFrom, out var idFrom, out var variantFrom );
LookupItem( itemTo, out var slotTo, out var idTo, out var variantTo );
if( slotFrom != slotTo )
{
LookupItem(itemFrom, out var slotFrom, out var idFrom, out var variantFrom);
LookupItem(itemTo, out var slotTo, out var idTo, out var variantTo);
if (slotFrom != slotTo)
throw new ItemSwap.InvalidItemTypeException();
}
var eqp = CreateEqp( manips, slotFrom, idFrom, idTo );
if( eqp != null )
{
swaps.Add( eqp );
}
var eqp = CreateEqp(manager, manips, slotFrom, idFrom, idTo);
if (eqp != null)
swaps.Add(eqp);
var gmp = CreateGmp( manips, slotFrom, idFrom, idTo );
if( gmp != null )
{
swaps.Add( gmp );
}
var gmp = CreateGmp(manager, manips, slotFrom, idFrom, idTo);
if (gmp != null)
swaps.Add(gmp);
var affectedItems = Array.Empty< Item >();
foreach( var slot in ConvertSlots( slotFrom, rFinger, lFinger ) )
var affectedItems = Array.Empty<Item>();
foreach (var slot in ConvertSlots(slotFrom, rFinger, lFinger))
{
( var imcFileFrom, var variants, affectedItems ) = GetVariants( identifier, slot, idFrom, idTo, variantFrom );
var imcManip = new ImcManipulation( slot, variantTo, idTo.Value, default );
var imcFileTo = new ImcFile( imcManip );
(var imcFileFrom, var variants, affectedItems) = GetVariants(manager, identifier, slot, idFrom, idTo, variantFrom);
var imcManip = new ImcManipulation(slot, variantTo, idTo.Value, default);
var imcFileTo = new ImcFile(manager, imcManip);
var isAccessory = slot.IsAccessory();
var estType = slot switch
{
EquipSlot.Head => EstManipulation.EstType.Head,
EquipSlot.Body => EstManipulation.EstType.Body,
_ => ( EstManipulation.EstType )0,
_ => (EstManipulation.EstType)0,
};
var skipFemale = false;
var skipMale = false;
var mtrlVariantTo = manips( imcManip.Copy( imcFileTo.GetEntry( ImcFile.PartIndex( slot ), variantTo ) ) ).Imc.Entry.MaterialId;
foreach( var gr in Enum.GetValues< GenderRace >() )
var mtrlVariantTo = manips(imcManip.Copy(imcFileTo.GetEntry(ImcFile.PartIndex(slot), variantTo))).Imc.Entry.MaterialId;
foreach (var gr in Enum.GetValues<GenderRace>())
{
switch( gr.Split().Item1 )
switch (gr.Split().Item1)
{
case Gender.Male when skipMale: continue;
case Gender.Female when skipFemale: continue;
@ -148,30 +150,24 @@ public static class EquipmentSwap
case Gender.FemaleNpc when skipFemale: continue;
}
if( CharacterUtilityData.EqdpIdx( gr, isAccessory ) < 0 )
{
if (CharacterUtilityData.EqdpIdx(gr, isAccessory) < 0)
continue;
}
try
{
var eqdp = CreateEqdp( redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo );
if( eqdp != null )
{
swaps.Add( eqdp );
}
var eqdp = CreateEqdp(manager, redirections, manips, slot, gr, idFrom, idTo, mtrlVariantTo);
if (eqdp != null)
swaps.Add(eqdp);
var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits( slot ).Item2 ?? false;
var est = ItemSwap.CreateEst( redirections, manips, estType, gr, idFrom, idTo, ownMdl );
if( est != null )
{
swaps.Add( est );
}
var ownMdl = eqdp?.SwapApplied.Eqdp.Entry.ToBits(slot).Item2 ?? false;
var est = ItemSwap.CreateEst(manager, redirections, manips, estType, gr, idFrom, idTo, ownMdl);
if (est != null)
swaps.Add(est);
}
catch( ItemSwap.MissingFileException e )
catch (ItemSwap.MissingFileException e)
{
switch( gr )
switch (gr)
{
case GenderRace.MidlanderMale when e.Type == ResourceType.Mdl:
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 );
swaps.Add( imc );
var imc = CreateImc(manager, redirections, manips, slot, idFrom, idTo, variant, variantTo, imcFileFrom, imcFileTo);
swaps.Add(imc);
}
}
return affectedItems;
}
public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, GenderRace gr, SetId idFrom,
SetId idTo, byte mtrlTo )
=> CreateEqdp( redirections, manips, slot, slot, gr, idFrom, idTo, mtrlTo );
public static MetaSwap? CreateEqdp( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slotFrom, EquipSlot slotTo, GenderRace gr, SetId idFrom,
SetId idTo, byte mtrlTo )
public static MetaSwap? CreateEqdp(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections,
Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot, GenderRace gr, SetId idFrom,
SetId idTo, byte mtrlTo)
=> CreateEqdp(manager, redirections, manips, slot, slot, gr, idFrom, idTo, 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 eqdpFrom = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotFrom.IsAccessory(), idFrom.Value ), slotFrom, gender, race, idFrom.Value );
var eqdpTo = new EqdpManipulation( ExpandedEqdpFile.GetDefault( gr, slotTo.IsAccessory(), idTo.Value ), slotTo, gender, race, idTo.Value );
var meta = new MetaSwap( manips, eqdpFrom, eqdpTo );
var (ownMtrl, ownMdl) = meta.SwapApplied.Eqdp.Entry.ToBits( slotFrom );
if( ownMdl )
var eqdpFrom = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotFrom.IsAccessory(), idFrom.Value), slotFrom, gender,
race, idFrom.Value);
var eqdpTo = new EqdpManipulation(ExpandedEqdpFile.GetDefault(manager, gr, slotTo.IsAccessory(), idTo.Value), slotTo, gender, race,
idTo.Value);
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 );
meta.ChildSwaps.Add( mdl );
var mdl = CreateMdl(manager, redirections, slotFrom, slotTo, gr, idFrom, idTo, mtrlTo);
meta.ChildSwaps.Add(mdl);
}
else if( !ownMtrl && meta.SwapAppliedIsDefault )
else if (!ownMtrl && meta.SwapAppliedIsDefault)
{
meta = null;
}
@ -218,97 +219,98 @@ public static class EquipmentSwap
return meta;
}
public static FileSwap CreateMdl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, GenderRace gr, SetId idFrom, SetId idTo, byte mtrlTo )
=> CreateMdl( redirections, slot, slot, gr, idFrom, idTo, mtrlTo );
public static FileSwap CreateMdl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, GenderRace gr,
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 mdlPathTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mdl.Path( idTo, gr, slotTo ) : GamePaths.Equipment.Mdl.Path( idTo, gr, slotTo );
var mdl = FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo );
var mdlPathFrom = slotFrom.IsAccessory()
? GamePaths.Accessory.Mdl.Path(idFrom, gr, slotFrom)
: 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 );
if( mtrl != null )
{
mdl.ChildSwaps.Add( mtrl );
}
var mtrl = CreateMtrl(manager, redirections, slotFrom, slotTo, idFrom, idTo, mtrlTo, ref fileName, ref mdl.DataWasChanged);
if (mtrl != null)
mdl.ChildSwaps.Add(mtrl);
}
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();
}
modelId = ( ( Quad )i.ModelMain ).A;
variant = ( byte )( ( Quad )i.ModelMain ).B;
modelId = ((Quad)i.ModelMain).A;
variant = (byte)((Quad)i.ModelMain).B;
}
private static (ImcFile, byte[], Item[]) GetVariants( IObjectIdentifier identifier, EquipSlot slotFrom, SetId idFrom, SetId idTo, byte variantFrom )
private static (ImcFile, byte[], Item[]) GetVariants(MetaFileManager manager, IObjectIdentifier identifier, EquipSlot slotFrom,
SetId idFrom, SetId idTo, byte variantFrom)
{
var entry = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, default );
var imc = new ImcFile( entry );
var entry = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, default);
var imc = new ImcFile(manager, entry);
Item[] items;
byte[] variants;
if( idFrom.Value == idTo.Value )
if (idFrom.Value == idTo.Value)
{
items = identifier.Identify( idFrom, variantFrom, slotFrom ).ToArray();
variants = new[] { variantFrom };
items = identifier.Identify(idFrom, variantFrom, slotFrom).ToArray();
variants = new[]
{
variantFrom,
};
}
else
{
items = identifier.Identify( slotFrom.IsEquipment()
? GamePaths.Equipment.Mdl.Path( idFrom, GenderRace.MidlanderMale, slotFrom )
: 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();
items = identifier.Identify(slotFrom.IsEquipment()
? GamePaths.Equipment.Mdl.Path(idFrom, GenderRace.MidlanderMale, slotFrom)
: 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();
}
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;
}
var manipFrom = new GmpManipulation( ExpandedGmpFile.GetDefault( idFrom.Value ), idFrom.Value );
var manipTo = new GmpManipulation( ExpandedGmpFile.GetDefault( idTo.Value ), idTo.Value );
return new MetaSwap( manips, manipFrom, manipTo );
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);
}
public static MetaSwap CreateImc( Func< Utf8GamePath, FullPath > redirections, Func< MetaManipulation, MetaManipulation > manips, EquipSlot slot, SetId idFrom, SetId idTo,
byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo )
=> CreateImc( redirections, manips, slot, slot, idFrom, idTo, variantFrom, variantTo, imcFileFrom, imcFileTo );
public static MetaSwap CreateImc(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, Func<MetaManipulation, MetaManipulation> manips, EquipSlot slot,
SetId idFrom, SetId idTo,
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 slotFrom, EquipSlot slotTo, SetId idFrom, SetId idTo,
byte variantFrom, byte variantTo, ImcFile imcFileFrom, ImcFile imcFileTo )
public static MetaSwap CreateImc(MetaFileManager manager, 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 entryTo = imcFileTo.GetEntry( ImcFile.PartIndex( slotTo ), variantTo );
var manipulationFrom = new ImcManipulation( slotFrom, variantFrom, idFrom.Value, entryFrom );
var manipulationTo = new ImcManipulation( slotTo, variantTo, idTo.Value, entryTo );
var imc = new MetaSwap( manips, manipulationFrom, manipulationTo );
var entryFrom = imcFileFrom.GetEntry(ImcFile.PartIndex(slotFrom), variantFrom);
var entryTo = imcFileTo.GetEntry(ImcFile.PartIndex(slotTo), variantTo);
var manipulationFrom = new ImcManipulation(slotFrom, variantFrom, idFrom.Value, entryFrom);
var manipulationTo = new ImcManipulation(slotTo, variantTo, idTo.Value, entryTo);
var imc = new MetaSwap(manips, manipulationFrom, manipulationTo);
var decal = CreateDecal( redirections, imc.SwapToModded.Imc.Entry.DecalId );
if( decal != null )
{
imc.ChildSwaps.Add( decal );
}
var decal = CreateDecal(manager, redirections, imc.SwapToModded.Imc.Entry.DecalId);
if (decal != null)
imc.ChildSwaps.Add(decal);
var avfx = CreateAvfx( redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId );
if( avfx != null )
{
imc.ChildSwaps.Add( avfx );
}
var avfx = CreateAvfx(manager, redirections, idFrom, idTo, imc.SwapToModded.Imc.Entry.VfxId);
if (avfx != null)
imc.ChildSwaps.Add(avfx);
// 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.
@ -316,134 +318,135 @@ public static class EquipmentSwap
}
// 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;
}
var decalPath = GamePaths.Equipment.Decal.Path( decalId );
return FileSwap.CreateSwap( ResourceType.Tex, redirections, decalPath, decalPath );
var decalPath = GamePaths.Equipment.Decal.Path(decalId);
return FileSwap.CreateSwap(manager, ResourceType.Tex, redirections, decalPath, decalPath);
}
// 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;
}
var vfxPathFrom = GamePaths.Equipment.Avfx.Path( idFrom.Value, vfxId );
var vfxPathTo = GamePaths.Equipment.Avfx.Path( idTo.Value, vfxId );
var avfx = FileSwap.CreateSwap( ResourceType.Avfx, redirections, vfxPathFrom, vfxPathTo );
var vfxPathFrom = GamePaths.Equipment.Avfx.Path(idFrom.Value, vfxId);
var vfxPathTo = GamePaths.Equipment.Avfx.Path(idTo.Value, vfxId);
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 );
avfx.ChildSwaps.Add( atex );
var atex = CreateAtex(manager, redirections, ref filePath, ref avfx.DataWasChanged);
avfx.ChildSwaps.Add(atex);
}
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;
}
var eqpValueFrom = ExpandedEqpFile.GetDefault( idFrom.Value );
var eqpValueTo = ExpandedEqpFile.GetDefault( 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(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);
}
public static FileSwap? CreateMtrl( Func< Utf8GamePath, FullPath > redirections, EquipSlot slot, SetId idFrom, SetId idTo, byte variantTo, ref string fileName,
ref bool dataWasChanged )
=> CreateMtrl( redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref dataWasChanged );
public static FileSwap? CreateMtrl(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, EquipSlot slot, SetId idFrom,
SetId idTo, byte variantTo, ref string fileName,
ref bool dataWasChanged)
=> CreateMtrl(manager, redirections, slot, slot, idFrom, idTo, variantTo, ref fileName, ref 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 )
public static FileSwap? CreateMtrl(MetaFileManager manager, 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';
if( !fileName.Contains( $"{prefix}{idTo.Value:D4}" ) )
{
if (!fileName.Contains($"{prefix}{idTo.Value:D4}"))
return null;
}
var folderTo = slotTo.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idTo, variantTo ) : GamePaths.Equipment.Mtrl.FolderPath( idTo, variantTo );
var pathTo = $"{folderTo}{fileName}";
var folderTo = slotTo.IsAccessory()
? GamePaths.Accessory.Mtrl.FolderPath(idTo, variantTo)
: GamePaths.Equipment.Mtrl.FolderPath(idTo, variantTo);
var pathTo = $"{folderTo}{fileName}";
var folderFrom = slotFrom.IsAccessory() ? GamePaths.Accessory.Mtrl.FolderPath( idFrom, variantTo ) : 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 folderFrom = slotFrom.IsAccessory()
? GamePaths.Accessory.Mtrl.FolderPath(idFrom, variantTo)
: 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}";
if( newFileName != fileName )
if (newFileName != fileName)
{
fileName = newFileName;
dataWasChanged = true;
}
var mtrl = FileSwap.CreateSwap( ResourceType.Mtrl, redirections, pathFrom, pathTo );
var shpk = CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged );
mtrl.ChildSwaps.Add( shpk );
var mtrl = FileSwap.CreateSwap(manager, ResourceType.Mtrl, redirections, pathFrom, pathTo);
var shpk = CreateShader(manager, redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged);
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 );
mtrl.ChildSwaps.Add( tex );
var tex = CreateTex(manager, redirections, prefix, slotFrom, slotTo, idFrom, idTo, ref texture, ref mtrl.DataWasChanged);
mtrl.ChildSwaps.Add(tex);
}
return mtrl;
}
public static FileSwap CreateTex( Func< Utf8GamePath, FullPath > redirections, char prefix, SetId idFrom, SetId idTo, ref MtrlFile.Texture texture, ref bool dataWasChanged )
=> CreateTex( redirections, prefix, EquipSlot.Unknown, EquipSlot.Unknown, idFrom, idTo, ref texture, ref dataWasChanged );
public static FileSwap CreateTex(MetaFileManager manager, Func<Utf8GamePath, FullPath> redirections, char prefix, SetId idFrom, SetId idTo,
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 addedDashes = false;
if( texture.DX11 )
if (texture.DX11)
{
var fileName = Path.GetFileName( path );
if( !fileName.StartsWith( "--" ) )
var fileName = Path.GetFileName(path);
if (!fileName.StartsWith("--"))
{
path = path.Replace( fileName, $"--{fileName}" );
path = path.Replace(fileName, $"--{fileName}");
addedDashes = true;
}
}
var newPath = ItemSwap.ReplaceAnyId( path, prefix, idFrom );
newPath = ItemSwap.ReplaceSlot( newPath, slotTo, slotFrom, slotTo != slotFrom );
newPath = ItemSwap.AddSuffix( newPath, ".tex", $"_{Path.GetFileName( texture.Path ).GetStableHashCode():x8}" );
if( newPath != path )
var newPath = ItemSwap.ReplaceAnyId(path, prefix, idFrom);
newPath = ItemSwap.ReplaceSlot(newPath, slotTo, slotFrom, slotTo != slotFrom);
newPath = ItemSwap.AddSuffix(newPath, ".tex", $"_{Path.GetFileName(texture.Path).GetStableHashCode():x8}");
if (newPath != path)
{
texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath;
texture.Path = addedDashes ? newPath.Replace("--", string.Empty) : newPath;
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}";
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;
filePath = ItemSwap.AddSuffix( filePath, ".atex", $"_{Path.GetFileName( filePath ).GetStableHashCode():x8}" );
filePath = ItemSwap.AddSuffix(filePath, ".atex", $"_{Path.GetFileName(filePath).GetStableHashCode():x8}");
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.Files;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Services;
@ -27,7 +28,7 @@ public static class ItemSwap
=> 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 )
{
@ -39,7 +40,7 @@ public static class ItemSwap
return true;
}
var file = DalamudServices.SGameData.GetFile( path.InternalName.ToString() );
var file = manager.GameData.GetFile( path.InternalName.ToString() );
if( file != null )
{
data = file.Data;
@ -61,18 +62,18 @@ public static class ItemSwap
public readonly byte[] Data;
public bool Valid { get; }
public GenericFile( FullPath path )
=> Valid = LoadFile( path, out Data );
public GenericFile( MetaFileManager manager, FullPath path )
=> Valid = LoadFile( manager, path, out Data );
public byte[] Write()
=> 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 )
{
return true;
@ -82,11 +83,11 @@ public static class ItemSwap
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
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new MdlFile( data );
return true;
@ -101,11 +102,11 @@ public static class ItemSwap
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
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new MtrlFile( data );
return true;
@ -120,11 +121,11 @@ public static class ItemSwap
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
{
if( LoadFile( path, out byte[] data ) )
if( LoadFile( manager, path, out byte[] data ) )
{
file = new AvfxFile( data );
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 );
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 );
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>
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 )
{
if( type == 0 )
@ -162,15 +163,15 @@ public static class ItemSwap
}
var (gender, race) = genderRace.Split();
var fromDefault = new EstManipulation( gender, race, type, idFrom.Value, EstFile.GetDefault( type, genderRace, idFrom.Value ) );
var toDefault = new EstManipulation( gender, race, type, idTo.Value, EstFile.GetDefault( type, genderRace, idTo.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( manager, type, genderRace, idTo.Value ) );
var est = new MetaSwap( manips, fromDefault, toDefault );
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 );
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 );
}
else if( est.SwapAppliedIsDefault )

View file

@ -9,12 +9,14 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using Penumbra.GameData;
using Penumbra.Meta;
using Penumbra.Mods.Manager;
namespace Penumbra.Mods.ItemSwap;
public class ItemSwapContainer
{
private readonly MetaFileManager _manager;
private readonly IObjectIdentifier _identifier;
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;
LoadMod( null, null );
}
@ -133,7 +136,7 @@ public class ItemSwapContainer
{
Swaps.Clear();
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;
return ret;
}
@ -142,15 +145,15 @@ public class ItemSwapContainer
{
Swaps.Clear();
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;
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 mdl = CustomizationSwap.CreateMdl( pathResolver, slot, race, from, to );
var mdl = CustomizationSwap.CreateMdl( manager, pathResolver, slot, race, from, to );
var type = slot switch
{
BodySlot.Hair => EstManipulation.EstType.Hair,
@ -159,7 +162,7 @@ public class ItemSwapContainer
};
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 );
if( est != null )

View file

@ -7,18 +7,19 @@ using System.IO;
using System.Linq;
using System.Security.Cryptography;
using Penumbra.GameData.Enums;
using Penumbra.Meta;
using static Penumbra.Mods.ItemSwap.ItemSwap;
using Penumbra.Services;
using Penumbra.Services;
namespace Penumbra.Mods.ItemSwap;
public class Swap
{
/// <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()
=> ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this );
public IEnumerable<Swap> WithChildren()
=> ChildSwaps.SelectMany(c => c.WithChildren()).Prepend(this);
}
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="manipFrom">The original 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;
SwapToDefault = manipTo;
SwapToModded = manipulations( manipTo );
SwapToIsDefault = manipTo.EntryEquals( SwapToModded );
SwapApplied = SwapFrom.WithEntryOf( SwapToModded );
SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom );
SwapToModded = manipulations(manipTo);
SwapToIsDefault = manipTo.EntryEquals(SwapToModded);
SwapApplied = SwapFrom.WithEntryOf(SwapToModded);
SwapAppliedIsDefault = SwapApplied.EntryEquals(SwapFrom);
}
}
@ -95,8 +96,8 @@ public sealed class FileSwap : Swap
/// <summary> Whether SwapFromPreChangePath equals SwapFromRequest. </summary>
public bool SwapFromChanged;
public string GetNewPath( string newMod )
=> Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() );
public string GetNewPath(string newMod)
=> Path.Combine(newMod, new Utf8RelPath(SwapFromRequestPath).ToString());
public MdlFile? AsMdl()
=> 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="swap">A full swap container with the actual file in memory.</param>
/// <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,
string? swapFromPreChange = null )
public static FileSwap CreateSwap(MetaFileManager manager, ResourceType type, Func<Utf8GamePath, FullPath> redirections,
string swapFromRequest, string swapToRequest,
string? swapFromPreChange = null)
{
var swap = new FileSwap
{
@ -131,49 +133,25 @@ public sealed class FileSwap : Swap
SwapToModded = FullPath.Empty,
};
if( swapFromRequest.Length == 0
|| swapToRequest.Length == 0
|| !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath )
|| !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) )
{
throw new Exception( $"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\"." );
}
if (swapFromRequest.Length == 0
|| swapToRequest.Length == 0
|| !Utf8GamePath.FromString(swapToRequest, out swap.SwapToRequestPath)
|| !Utf8GamePath.FromString(swapFromRequest, out swap.SwapFromRequestPath))
throw new Exception($"Could not create UTF8 String for \"{swapFromRequest}\" or \"{swapToRequest}\".");
swap.SwapToModded = redirections( swap.SwapToRequestPath );
swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && DalamudServices.SGameData.FileExists( swap.SwapToModded.InternalName.ToString() );
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path );
swap.SwapToModded = redirections(swap.SwapToRequestPath);
swap.SwapToModdedExistsInGame =
!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
{
ResourceType.Mdl => LoadMdl( 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.Avfx => LoadAvfx( 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 ),
ResourceType.Mdl => LoadMdl(manager, 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(manager, 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;
}
/// <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 OtterGui;
using Penumbra.Api.Enums;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
@ -109,11 +110,11 @@ public partial class Mod
}
}
public void WriteAllTexToolsMeta()
public void WriteAllTexToolsMeta(MetaFileManager manager)
{
try
{
_default.WriteTexToolsMeta(ModPath);
_default.WriteTexToolsMeta(manager, ModPath);
foreach (var group in Groups)
{
var dir = ModCreator.NewOptionDirectory(ModPath, group.Name);
@ -126,7 +127,7 @@ public partial class Mod
if (!optionDir.Exists)
optionDir.Create();
option.WriteTexToolsMeta(optionDir);
option.WriteTexToolsMeta(manager, optionDir);
}
}
}

View file

@ -5,12 +5,13 @@ using System.IO;
using System.Linq;
using Newtonsoft.Json.Linq;
using Penumbra.Import;
using Penumbra.Meta;
using Penumbra.Meta.Manipulations;
using Penumbra.String.Classes;
namespace Penumbra.Mods;
/// <summary>
/// <summary>
/// A sub mod is a collection of
/// - file replacements
/// - file swaps
@ -18,14 +19,14 @@ namespace Penumbra.Mods;
/// that can be used either as an option or as the default data for a mod.
/// It can be loaded and reloaded from Json.
/// 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>
public sealed class SubMod : ISubMod
public sealed class SubMod : ISubMod
{
public string Name { get; set; } = "Default";
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;
@ -36,180 +37,165 @@ namespace Penumbra.Mods;
public bool IsDefault
=> GroupIdx < 0;
public Dictionary< Utf8GamePath, FullPath > FileData = new();
public Dictionary< Utf8GamePath, FullPath > FileSwapData = new();
public HashSet< MetaManipulation > ManipulationData = new();
public Dictionary<Utf8GamePath, FullPath> FileData = new();
public Dictionary<Utf8GamePath, FullPath> FileSwapData = new();
public HashSet<MetaManipulation> ManipulationData = new();
public SubMod( IMod parentMod )
public SubMod(IMod parentMod)
=> ParentMod = parentMod;
public IReadOnlyDictionary< Utf8GamePath, FullPath > Files
public IReadOnlyDictionary<Utf8GamePath, FullPath> Files
=> FileData;
public IReadOnlyDictionary< Utf8GamePath, FullPath > FileSwaps
public IReadOnlyDictionary<Utf8GamePath, FullPath> FileSwaps
=> FileSwapData;
public IReadOnlySet< MetaManipulation > Manipulations
public IReadOnlySet<MetaManipulation> Manipulations
=> ManipulationData;
public void SetPosition( int groupIdx, int optionIdx )
public void SetPosition(int groupIdx, int optionIdx)
{
GroupIdx = groupIdx;
OptionIdx = optionIdx;
}
public void Load( DirectoryInfo basePath, JToken json, out int priority )
public void Load(DirectoryInfo basePath, JToken json, out int priority)
{
FileData.Clear();
FileSwapData.Clear();
ManipulationData.Clear();
// Every option has a name, but priorities are only relevant for multi group options.
Name = json[ nameof( ISubMod.Name ) ]?.ToObject< string >() ?? string.Empty;
Description = json[ nameof( ISubMod.Description ) ]?.ToObject< string >() ?? string.Empty;
priority = json[ nameof( IModGroup.Priority ) ]?.ToObject< int >() ?? 0;
Name = json[nameof(ISubMod.Name)]?.ToObject<string>() ?? string.Empty;
Description = json[nameof(ISubMod.Description)]?.ToObject<string>() ?? string.Empty;
priority = json[nameof(IModGroup.Priority)]?.ToObject<int>() ?? 0;
var files = ( JObject? )json[ nameof( Files ) ];
if( files != null )
{
foreach( var property in files.Properties() )
var files = (JObject?)json[nameof(Files)];
if (files != null)
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 ) ];
if( swaps != null )
{
foreach( var property in swaps.Properties() )
var swaps = (JObject?)json[nameof(FileSwaps)];
if (swaps != null)
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 ) ];
if( manips != null )
{
foreach( var s in manips.Children().Select( c => c.ToObject< MetaManipulation >() ).Where( m => m.ManipulationType != MetaManipulation.Type.Unknown ) )
{
ManipulationData.Add( s );
}
}
var manips = json[nameof(Manipulations)];
if (manips != null)
foreach (var s in manips.Children().Select(c => c.ToObject<MetaManipulation>())
.Where(m => m.ManipulationType != MetaManipulation.Type.Unknown))
ManipulationData.Add(s);
}
// 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.
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 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 ext2 = file.Extension.ToLowerInvariant();
try
{
if( ext1 == ".meta" || ext2 == ".meta" )
if (ext1 == ".meta" || ext2 == ".meta")
{
FileData.Remove( key );
if( !file.Exists )
{
FileData.Remove(key);
if (!file.Exists)
continue;
}
var meta = new TexToolsMeta( Penumbra.GamePathParser, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as Metadata file of {meta.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
ManipulationData.UnionWith( meta.MetaManipulations );
var meta = new TexToolsMeta(Penumbra.MetaFileManager, Penumbra.GamePathParser, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
Penumbra.Log.Verbose(
$"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;
}
var rgsp = TexToolsMeta.FromRgspFile( file.FullName, File.ReadAllBytes( file.FullName ), Penumbra.Config.KeepDefaultMetaChanges );
Penumbra.Log.Verbose( $"Incorporating {file} as racial scaling file of {rgsp.MetaManipulations.Count} manipulations {deleteString}" );
deleteList.Add( file.FullName );
var rgsp = TexToolsMeta.FromRgspFile(Penumbra.MetaFileManager, file.FullName, File.ReadAllBytes(file.FullName),
Penumbra.Config.KeepDefaultMetaChanges);
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 );
return ( oldSize < ManipulationData.Count, deleteList );
DeleteDeleteList(deleteList, delete);
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;
}
foreach( var file in deleteList )
foreach (var file in deleteList)
{
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
{
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
File.WriteAllBytes( path, data );
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
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 )
{
TestMetaWriting( files );
}
if (test)
TestMetaWriting(manager, files);
}
[Conditional("DEBUG" )]
private void TestMetaWriting( Dictionary< string, byte[] > files )
[Conditional("DEBUG")]
private void TestMetaWriting(MetaFileManager manager, Dictionary<string, byte[]> files)
{
var meta = new HashSet< MetaManipulation >( Manipulations.Count );
foreach( var (file, data) in files )
var meta = new HashSet<MetaManipulation>(Manipulations.Count);
foreach (var (file, data) in files)
{
try
{
var x = file.EndsWith( "rgsp" )
? TexToolsMeta.FromRgspFile( file, data, Penumbra.Config.KeepDefaultMetaChanges )
: new TexToolsMeta( Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges );
meta.UnionWith( x.MetaManipulations );
var x = file.EndsWith("rgsp")
? TexToolsMeta.FromRgspFile(manager, file, data, Penumbra.Config.KeepDefaultMetaChanges)
: new TexToolsMeta(manager, Penumbra.GamePathParser, data, Penumbra.Config.KeepDefaultMetaChanges);
meta.UnionWith(x.MetaManipulations);
}
catch
{
@ -217,27 +203,21 @@ namespace Penumbra.Mods;
}
}
if( !Manipulations.SetEquals( meta ) )
if (!Manipulations.SetEquals(meta))
{
Penumbra.Log.Information( "Meta Sets do not equal." );
foreach( var (m1, m2) in Manipulations.Zip( meta ) )
{
Penumbra.Log.Information( $"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}" );
}
Penumbra.Log.Information("Meta Sets do not equal.");
foreach (var (m1, m2) in Manipulations.Zip(meta))
Penumbra.Log.Information($"{m1} {m1.EntryToString()} | {m2} {m2.EntryToString()}");
foreach( var m in Manipulations.Skip( meta.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach (var m in Manipulations.Skip(meta.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
foreach( var m in meta.Skip( Manipulations.Count ) )
{
Penumbra.Log.Information( $"{m} {m.EntryToString()} " );
}
foreach (var m in meta.Skip(Manipulations.Count))
Penumbra.Log.Information($"{m} {m.EntryToString()} ");
}
else
{
Penumbra.Log.Information( "Meta Sets are equal." );
Penumbra.Log.Information("Meta Sets are equal.");
}
}
}
}

View file

@ -28,7 +28,8 @@ using Penumbra.Interop.Services;
using Penumbra.Mods.Manager;
using Penumbra.Collections.Manager;
using Penumbra.Mods;
using Penumbra.Meta;
namespace Penumbra;
public class Penumbra : IDalamudPlugin

View file

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

View file

@ -12,9 +12,10 @@ using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.Api.Enums;
using Penumbra.Collections;
using Penumbra.Collections.Manager;
using Penumbra.Collections.Manager;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.ItemSwap;
using Penumbra.Mods.Manager;
@ -25,21 +26,23 @@ namespace Penumbra.UI.AdvancedWindow;
public class ItemSwapTab : IDisposable, ITab
{
private readonly CommunicatorService _communicator;
private readonly ItemService _itemService;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly Configuration _config;
private readonly CommunicatorService _communicator;
private readonly ItemService _itemService;
private readonly CollectionManager _collectionManager;
private readonly ModManager _modManager;
private readonly Configuration _config;
private readonly MetaFileManager _metaFileManager;
public ItemSwapTab(CommunicatorService communicator, ItemService itemService, CollectionManager collectionManager,
ModManager modManager, Configuration config, IdentifierService identifier)
ModManager modManager, Configuration config, IdentifierService identifier, MetaFileManager metaFileManager)
{
_communicator = communicator;
_itemService = itemService;
_collectionManager = collectionManager;
_modManager = modManager;
_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)>
{
@ -215,22 +218,22 @@ public class ItemSwapTab : IDisposable, ITab
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
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,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
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,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
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,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
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,
_useCurrentCollection ? _collectionManager.Active.Current : null);
break;
@ -312,7 +315,8 @@ public class ItemSwapTab : IDisposable, ITab
optionCreated = true;
optionFolderName = Directory.CreateDirectory(optionFolderName.FullName);
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,
_mod.Groups.IndexOf(_selectedGroup), _selectedGroup.Count - 1))
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)
{
if (collection != _collectionManager.Active.Current || mod != _mod)
return;
@ -762,7 +765,7 @@ public class ItemSwapTab : IDisposable, ITab
private void OnInheritanceChange(ModCollection collection, bool _)
{
if (collection != _collectionManager.Active.Current || _mod == null)
return;
return;
UpdateMod(_mod, collection[_mod.Index].Settings);
_swapData.LoadMod(_mod, _modSettings);
@ -772,8 +775,9 @@ public class ItemSwapTab : IDisposable, ITab
private void OnModOptionChange(ModOptionChangeType type, Mod mod, int a, int b, int c)
{
if (type is ModOptionChangeType.PrepareChange or ModOptionChangeType.GroupAdded or ModOptionChangeType.OptionAdded || mod != _mod)
return;
_swapData.LoadMod(_mod, _modSettings);
return;
_swapData.LoadMod(_mod, _modSettings);
UpdateOption();
_dirty = true;
}

View file

@ -9,6 +9,7 @@ using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.Interop.Structs;
using Penumbra.Meta;
using Penumbra.Meta.Files;
using Penumbra.Meta.Manipulations;
using Penumbra.Mods;
@ -62,7 +63,7 @@ public partial class ModEditWindow
CopyToClipboardButton("Copy all current manipulations to clipboard.", _iconSize, _editor.MetaEditor.Recombine());
ImGui.SameLine();
if (ImGui.Button("Write as TexTools Files"))
_mod!.WriteAllTexToolsMeta();
_mod!.WriteAllTexToolsMeta(_metaFileManager);
using var child = ImRaii.Child("##meta", -Vector2.One, true);
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.
private void DrawEditHeader<T>(IReadOnlyCollection<T> items, string label, int numColumns, Action<T, ModEditor, Vector2> draw,
Action<ModEditor, Vector2> drawNew)
/// <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<MetaFileManager, T, ModEditor, Vector2> draw,
Action<MetaFileManager, ModEditor, Vector2> drawNew)
{
const ImGuiTableFlags flags = ImGuiTableFlags.RowBg | ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.BordersInnerV;
if (!ImGui.CollapsingHeader($"{items.Count} {label}"))
@ -89,11 +90,11 @@ public partial class ModEditWindow
{
if (table)
{
drawNew(_editor, _iconSize);
drawNew(_metaFileManager, _editor, _iconSize);
foreach (var (item, index) in items.ToArray().WithIndex())
{
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
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQP manipulations to clipboard.", iconSize,
@ -116,20 +117,20 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
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))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
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);
ImGui.TableNextColumn();
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);
@ -148,7 +149,7 @@ public partial class ModEditWindow
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);
@ -157,7 +158,7 @@ public partial class ModEditWindow
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
ImGui.TextUnformatted(meta.SetId.ToString());
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
var defaultEntry = ExpandedEqpFile.GetDefault(meta.SetId);
var defaultEntry = ExpandedEqpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn();
ImGui.SetCursorPosX(ImGui.GetCursorPosX() + ImGui.GetStyle().FramePadding.X);
@ -192,7 +193,7 @@ public partial class ModEditWindow
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EQDP manipulations to clipboard.", iconSize,
@ -204,7 +205,7 @@ public partial class ModEditWindow
var tt = canAdd ? "Stage this edit." :
validRaceCode ? "This entry is already edited." : "This combination of race and gender can not be used.";
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;
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), iconSize, tt, !canAdd, true))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
@ -213,7 +214,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -222,7 +223,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -231,7 +232,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -240,7 +241,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -255,7 +256,7 @@ public partial class ModEditWindow
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);
@ -278,7 +279,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
// 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 (bit1, bit2) = meta.Entry.ToBits(meta.Slot);
ImGui.TableNextColumn();
@ -301,12 +302,12 @@ public partial class ModEditWindow
private static float SmallIdWidth
=> 45 * UiHelpers.Scale;
// Convert throwing to null-return if the file does not exist.
private static ImcEntry? GetDefault(ImcManipulation imc)
/// <summary> Convert throwing to null-return if the file does not exist. </summary>
private static ImcEntry? GetDefault(MetaFileManager metaFileManager, ImcManipulation imc)
{
try
{
return ImcFile.GetDefault(imc.GamePath(), imc.EquipSlot, imc.Variant, out _);
return ImcFile.GetDefault(metaFileManager, imc.GamePath(), imc.EquipSlot, imc.Variant, out _);
}
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();
CopyToClipboardButton("Copy all current IMC manipulations to clipboard.", iconSize,
editor.MetaEditor.Imc.Select(m => (MetaManipulation)m));
ImGui.TableNextColumn();
var defaultEntry = GetDefault(_new);
var defaultEntry = GetDefault(metaFileManager, _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.";
defaultEntry ??= new ImcEntry();
@ -347,7 +348,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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)
.Copy(GetDefault(_new)
.Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(PrimaryIdTooltip);
@ -361,7 +362,7 @@ public partial class ModEditWindow
{
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)
.Copy(GetDefault(_new)
.Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -370,7 +371,7 @@ public partial class ModEditWindow
{
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)
.Copy(GetDefault(_new)
.Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry());
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))
_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());
ImGuiUtil.HoverTooltip(SecondaryIdTooltip);
@ -388,7 +389,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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.Entry).Copy(GetDefault(_new)
_new.Entry).Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry());
ImGui.TableNextColumn();
@ -396,7 +397,7 @@ public partial class ModEditWindow
{
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)
.Copy(GetDefault(_new)
.Copy(GetDefault(metaFileManager, _new)
?? new ImcEntry());
ImGuiUtil.HoverTooltip(EquipSlotTooltip);
@ -438,7 +439,7 @@ public partial class ModEditWindow
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);
@ -479,7 +480,7 @@ public partial class ModEditWindow
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing,
new Vector2(3 * UiHelpers.Scale, ImGui.GetStyle().ItemSpacing.Y));
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,
defaultEntry.MaterialId, out var materialId, 1, byte.MaxValue, 0.01f))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { MaterialId = (byte)materialId }));
@ -530,7 +531,7 @@ public partial class ModEditWindow
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current EST manipulations to clipboard.", iconSize,
@ -538,7 +539,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
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))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
@ -546,7 +547,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -555,7 +556,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -564,7 +565,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -573,7 +574,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
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);
}
@ -585,7 +586,7 @@ public partial class ModEditWindow
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);
@ -608,7 +609,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(EstTypeTooltip);
// 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();
if (IntDragInput("##estSkeleton", $"Skeleton Index\nDefault Value: {defaultEntry}", IdWidth, meta.Entry, defaultEntry,
out var entry, 0, ushort.MaxValue, 0.05f))
@ -629,7 +630,7 @@ public partial class ModEditWindow
private static float IdWidth
=> 100 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current GMP manipulations to clipboard.", iconSize,
@ -637,14 +638,14 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
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))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
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);
@ -669,7 +670,7 @@ public partial class ModEditWindow
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);
@ -680,7 +681,7 @@ public partial class ModEditWindow
ImGuiUtil.HoverTooltip(ModelSetIdTooltipShort);
// Values
var defaultEntry = ExpandedGmpFile.GetDefault(meta.SetId);
var defaultEntry = ExpandedGmpFile.GetDefault(metaFileManager, meta.SetId);
ImGui.TableNextColumn();
if (Checkmark("##gmpEnabled", "Gimmick Enabled", meta.Entry.Enabled, defaultEntry.Enabled, out var enabled))
editor.MetaEditor.Change(meta.Copy(meta.Entry with { Enabled = enabled }));
@ -723,7 +724,7 @@ public partial class ModEditWindow
private static float FloatWidth
=> 150 * UiHelpers.Scale;
public static void DrawNew(ModEditor editor, Vector2 iconSize)
public static void DrawNew(MetaFileManager metaFileManager, ModEditor editor, Vector2 iconSize)
{
ImGui.TableNextColumn();
CopyToClipboardButton("Copy all current RSP manipulations to clipboard.", iconSize,
@ -731,20 +732,20 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
var canAdd = editor.MetaEditor.CanAdd(_new);
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))
editor.MetaEditor.Add(_new.Copy(defaultEntry));
// Identifier
ImGui.TableNextColumn();
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);
ImGui.TableNextColumn();
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);
@ -755,7 +756,7 @@ public partial class ModEditWindow
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);
@ -771,7 +772,7 @@ public partial class ModEditWindow
ImGui.TableNextColumn();
// Values
var def = CmpFile.GetDefault(meta.SubRace, meta.Attribute);
var def = CmpFile.GetDefault(metaFileManager, meta.SubRace, meta.Attribute);
var value = meta.Entry;
ImGui.SetNextItemWidth(FloatWidth);
using var color = ImRaii.PushColor(ImGuiCol.FrameBg,

View file

@ -13,6 +13,7 @@ using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Import.Textures;
using Penumbra.Interop.ResourceTree;
using Penumbra.Meta;
using Penumbra.Mods;
using Penumbra.Mods.Manager;
using Penumbra.String.Classes;
@ -31,6 +32,7 @@ public partial class ModEditWindow : Window, IDisposable
private readonly Configuration _config;
private readonly ItemSwapTab _itemSwapTab;
private readonly DataManager _gameData;
private readonly MetaFileManager _metaFileManager;
private Mod? _mod;
private Vector2 _iconSize = Vector2.Zero;
@ -493,16 +495,17 @@ public partial class ModEditWindow : Window, IDisposable
}
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)
{
_performance = performance;
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_modCaches = modCaches;
_gameData = gameData;
_fileDialog = fileDialog;
_performance = performance;
_itemSwapTab = itemSwapTab;
_config = config;
_editor = editor;
_modCaches = modCaches;
_metaFileManager = metaFileManager;
_gameData = gameData;
_fileDialog = fileDialog;
_materialTab = new FileEditor<MtrlTab>(this, gameData, config, _fileDialog, "Materials", ".mtrl",
() => _editor.Files.Mtrl, DrawMaterialPanel, () => _mod?.ModPath.FullName ?? string.Empty,
bytes => new MtrlTab(this, new MtrlFile(bytes)));