Add more edit options, some small fixes.

This commit is contained in:
Ottermandias 2022-05-03 18:15:35 +02:00
parent 65bbece9cf
commit 54460c39f3
13 changed files with 697 additions and 227 deletions

View file

@ -48,11 +48,6 @@ public partial class Mod
_duplicates.Clear();
}
public void Cancel()
{
DuplicatesFinished = true;
}
private void HandleDuplicate( FullPath duplicate, FullPath remaining )
{
void HandleSubMod( ISubMod subMod, int groupIdx, int optionIdx )
@ -94,8 +89,11 @@ public partial class Mod
public void StartDuplicateCheck()
{
DuplicatesFinished = false;
Task.Run( CheckDuplicates );
if( DuplicatesFinished )
{
DuplicatesFinished = false;
Task.Run( CheckDuplicates );
}
}
private void CheckDuplicates()

View file

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Linq;
using Penumbra.GameData.ByteString;
using Penumbra.Meta.Manipulations;
using Penumbra.Util;
@ -9,20 +10,23 @@ public partial class Mod
{
public partial class Editor
{
private int _groupIdx = -1;
private int _optionIdx = 0;
public int GroupIdx { get; private set; } = -1;
public int OptionIdx { get; private set; } = 0;
private IModGroup? _modGroup;
private SubMod _subMod;
public ISubMod CurrentOption
=> _subMod;
public readonly Dictionary< Utf8GamePath, FullPath > CurrentFiles = new();
public readonly Dictionary< Utf8GamePath, FullPath > CurrentSwaps = new();
public readonly HashSet< MetaManipulation > CurrentManipulations = new();
public void SetSubMod( int groupIdx, int optionIdx )
{
_groupIdx = groupIdx;
_optionIdx = optionIdx;
GroupIdx = groupIdx;
OptionIdx = optionIdx;
if( groupIdx >= 0 )
{
_modGroup = _mod.Groups[ groupIdx ];
@ -34,8 +38,38 @@ public partial class Mod
_subMod = _mod._default;
}
RevertFiles();
RevertSwaps();
RevertManipulations();
}
public void ApplyFiles()
{
Penumbra.ModManager.OptionSetFiles( _mod, GroupIdx, OptionIdx, CurrentFiles.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) );
}
public void RevertFiles()
{
CurrentFiles.SetTo( _subMod.Files );
}
public void ApplySwaps()
{
Penumbra.ModManager.OptionSetFileSwaps( _mod, GroupIdx, OptionIdx, CurrentSwaps.ToDictionary( kvp => kvp.Key, kvp => kvp.Value ) );
}
public void RevertSwaps()
{
CurrentSwaps.SetTo( _subMod.FileSwaps );
}
public void ApplyManipulations()
{
Penumbra.ModManager.OptionSetManipulations( _mod, GroupIdx, OptionIdx, CurrentManipulations.ToHashSet() );
}
public void RevertManipulations()
{
CurrentManipulations.Clear();
CurrentManipulations.UnionWith( _subMod.Manipulations );
}

View file

@ -0,0 +1,179 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Dalamud.Logging;
using Penumbra.GameData.ByteString;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.Util;
namespace Penumbra.Mods;
public partial class Mod
{
public partial class Editor
{
private static readonly Regex MaterialRegex = new(@"/mt_c(?'RaceCode'\d{4})b0001_(?'Suffix'.*?)\.mtrl", RegexOptions.Compiled);
private readonly List< MaterialInfo > _modelFiles = new();
public IReadOnlyList< MaterialInfo > ModelFiles
=> _modelFiles;
// Non-ASCII encoding can not be used.
public static bool ValidString( string to )
=> to.Length != 0
&& to.Length < 16
&& Encoding.UTF8.GetByteCount( to ) == to.Length;
public void SaveAllModels()
{
foreach( var info in _modelFiles )
{
info.Save();
}
}
public void RestoreAllModels()
{
foreach( var info in _modelFiles )
{
info.Restore();
}
}
// Go through the currently loaded files and replace all appropriate suffices.
// Does nothing if toSuffix is invalid.
// If raceCode is Unknown, apply to all raceCodes.
// If fromSuffix is empty, apply to all suffices.
public void ReplaceAllMaterials( string toSuffix, string fromSuffix = "", GenderRace raceCode = GenderRace.Unknown )
{
if( !ValidString( toSuffix ) )
return;
foreach( var info in _modelFiles )
{
for( var i = 0; i < info.Count; ++i )
{
var (_, def) = info[ i ];
var match = MaterialRegex.Match( def );
if( match.Success
&& ( raceCode == GenderRace.Unknown || raceCode.ToRaceCode() == match.Groups[ "RaceCode" ].Value )
&& ( fromSuffix.Length == 0 || fromSuffix == match.Groups[ "Suffix" ].Value ) )
{
info.SetMaterial( $"/mt_c{match.Groups["RaceCode"].Value}b0001_{toSuffix}.mtrl", i );
}
}
}
}
// Find all model files in the mod that contain skin materials.
private void ScanModels()
{
_modelFiles.Clear();
foreach( var (file, _) in AvailableFiles.Where( f => f.Item1.Extension == ".mdl" ) )
{
try
{
var bytes = File.ReadAllBytes( file.FullName );
var mdlFile = new MdlFile( bytes );
var materials = mdlFile.Materials.WithIndex().Where( p => MaterialRegex.IsMatch( p.Item1 ) )
.Select( p => p.Item2 ).ToArray();
if( materials.Length > 0 )
{
_modelFiles.Add( new MaterialInfo( file, mdlFile, materials ) );
}
}
catch( Exception e )
{
PluginLog.Error( $"Unexpected error scanning {_mod.Name}'s {file.FullName} for materials:\n{e}" );
}
}
}
// A class that collects information about skin materials in a model file and handle changes on them.
public class MaterialInfo
{
public readonly FullPath Path;
private readonly MdlFile _file;
private readonly string[] _currentMaterials;
private readonly IReadOnlyList<int> _materialIndices;
public bool Changed { get; private set; } = false;
public IReadOnlyList<string> CurrentMaterials
=> _currentMaterials;
private IEnumerable<string> DefaultMaterials
=> _materialIndices.Select( i => _file.Materials[i] );
public (string Current, string Default) this[int idx]
=> (_currentMaterials[idx], _file.Materials[_materialIndices[idx]]);
public int Count
=> _materialIndices.Count;
// Set the skin material to a new value and flag changes appropriately.
public void SetMaterial( string value, int materialIdx )
{
var mat = _file.Materials[_materialIndices[materialIdx]];
_currentMaterials[materialIdx] = value;
if( mat != value )
{
Changed = true;
}
else
{
Changed = !_currentMaterials.SequenceEqual( DefaultMaterials );
}
}
// Save a changed .mdl file.
public void Save()
{
if( !Changed )
{
return;
}
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
_file.Materials[idx] = _currentMaterials[i];
}
try
{
File.WriteAllBytes( Path.FullName, _file.Write() );
Changed = false;
}
catch( Exception e )
{
Restore();
PluginLog.Error( $"Could not write manipulated .mdl file {Path.FullName}:\n{e}" );
}
}
// Revert all current changes.
public void Restore()
{
if( !Changed )
return;
foreach( var (idx, i) in _materialIndices.WithIndex() )
{
_currentMaterials[i] = _file.Materials[idx];
}
Changed = false;
}
public MaterialInfo( FullPath path, MdlFile file, IReadOnlyList<int> indices )
{
Path = path;
_file = file;
_materialIndices = indices;
_currentMaterials = DefaultMaterials.ToArray();
}
}
}
}

View file

@ -21,13 +21,17 @@ public partial class Mod
_missingPaths = new SortedSet< FullPath >( UsedPaths.Where( f => !f.Exists ) );
_unusedFiles = new SortedSet< FullPath >( AvailableFiles.Where( p => !UsedPaths.Contains( p.Item1 ) ).Select( p => p.Item1 ) );
_subMod = _mod._default;
ScanModels();
}
public void Dispose()
public void Cancel()
{
DuplicatesFinished = true;
}
public void Dispose()
=> Cancel();
// Does not delete the base directory itself even if it is completely empty at the end.
private static void ClearEmptySubDirectories( DirectoryInfo baseDir )
{
@ -49,7 +53,7 @@ public partial class Mod
{
for( var optionIdx = 0; optionIdx < group.Count; ++optionIdx )
{
action( @group[ optionIdx ], groupIdx, optionIdx );
action( group[ optionIdx ], groupIdx, optionIdx );
}
}
}

View file

@ -18,7 +18,7 @@ public partial class Mod
// - Containing no symbols invalid for FFXIV or windows paths.
internal static DirectoryInfo CreateModFolder( DirectoryInfo outDirectory, string modListName )
{
var name = Path.GetFileNameWithoutExtension( modListName );
var name = modListName;
if( name.Length == 0 )
{
name = "_";