mirror of
https://github.com/xivdev/Penumbra.git
synced 2025-12-12 18:27:24 +01:00
Add basic version of item swap, seemingly working for hair, tail and ears.
This commit is contained in:
parent
e534ce37d5
commit
5b3d5d1e67
22 changed files with 1730 additions and 120 deletions
|
|
@ -1,3 +1,5 @@
|
|||
using System.Text.RegularExpressions;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
|
|
@ -5,6 +7,23 @@ namespace Penumbra.GameData.Data;
|
|||
|
||||
public static partial class GamePaths
|
||||
{
|
||||
private static readonly Regex RaceCodeRegex = new(@"c(?'racecode'\d{4})", RegexOptions.Compiled);
|
||||
|
||||
//[GeneratedRegex(@"c(?'racecode'\d{4})")]
|
||||
public static partial Regex RaceCodeParser();
|
||||
|
||||
public static partial Regex RaceCodeParser()
|
||||
=> RaceCodeRegex;
|
||||
|
||||
public static GenderRace ParseRaceCode(string path)
|
||||
{
|
||||
var match = RaceCodeParser().Match(path);
|
||||
return match.Success
|
||||
? Names.GenderRaceFromCode(match.Groups["racecode"].Value)
|
||||
: GenderRace.Unknown;
|
||||
}
|
||||
|
||||
|
||||
public static partial class Monster
|
||||
{
|
||||
public static partial class Imc
|
||||
|
|
@ -139,7 +158,7 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot)
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/model/c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl";
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/model/c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}.mdl";
|
||||
}
|
||||
|
||||
public static partial class Mtrl
|
||||
|
|
@ -148,7 +167,7 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix)
|
||||
=> $"{FolderPath(equipId, variant)}/mt_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
|
||||
=> $"{FolderPath(equipId, variant)}/mt_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
|
||||
|
||||
public static string FolderPath(SetId equipId, byte variant)
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/material/v{variant:D4}";
|
||||
|
|
@ -160,7 +179,25 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId equipId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0')
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}e{equipId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
}
|
||||
|
||||
public static partial class Avfx
|
||||
{
|
||||
//[GeneratedRegex(@"chara/equipment/e(?'id'\d{4})/vfx/eff/ve(?'variant'\d{4})\.avfx")]
|
||||
//public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId equipId, byte effectId)
|
||||
=> $"chara/equipment/e{equipId.Value:D4}/vfx/eff/ve{effectId:D4}.avfx";
|
||||
}
|
||||
|
||||
public static partial class Decal
|
||||
{
|
||||
//[GeneratedRegex(@"chara/common/texture/decal_equip/-decal_(?'decalId'\d{3})\.tex")]
|
||||
//public static partial Regex Regex();
|
||||
|
||||
public static string Path(byte decalId)
|
||||
=> $"chara/common/texture/decal_equip/-decal_{decalId:D3}.tex";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -181,7 +218,7 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot)
|
||||
=> $"chara/accessory/a{accessoryId.Value:D4}/model/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl";
|
||||
=> $"chara/accessory/a{accessoryId.Value:D4}/model/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}.mdl";
|
||||
}
|
||||
|
||||
public static partial class Mtrl
|
||||
|
|
@ -190,7 +227,7 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, string suffix)
|
||||
=> $"{FolderPath(accessoryId, variant)}/c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
|
||||
=> $"{FolderPath(accessoryId, variant)}/c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}_{suffix}.mtrl";
|
||||
|
||||
public static string FolderPath(SetId accessoryId, byte variant)
|
||||
=> $"chara/accessory/a{accessoryId.Value:D4}/material/v{variant:D4}";
|
||||
|
|
@ -202,7 +239,7 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(SetId accessoryId, GenderRace raceCode, EquipSlot slot, byte variant, char suffix1, char suffix2 = '\0')
|
||||
=> $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{(ushort)raceCode:D4}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
=> $"chara/accessory/a{accessoryId.Value:D4}/texture/v{variant:D2}_c{raceCode.ToRaceCode()}a{accessoryId.Value:D4}_{slot.ToSuffix()}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -214,7 +251,19 @@ public static partial class GamePaths
|
|||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, CustomizationType type)
|
||||
=> $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl";
|
||||
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/model/c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}_{type.ToSuffix()}.mdl";
|
||||
}
|
||||
|
||||
public static partial class Phyb
|
||||
{
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId)
|
||||
=> $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/phy_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.phyb";
|
||||
}
|
||||
|
||||
public static partial class Sklb
|
||||
{
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId)
|
||||
=> $"chara/human/c{raceCode.ToRaceCode()}/skeleton/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/skl_c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}.sklb";
|
||||
}
|
||||
|
||||
public static partial class Mtrl
|
||||
|
|
@ -222,11 +271,52 @@ public static partial class GamePaths
|
|||
// [GeneratedRegex(@"chara/human/c(?'race'\d{4})/obj/(?'type'[a-z]+)/(?'typeabr'[a-z])(?'id'\d{4})/material(/v(?'variant'\d{4}))?/mt_c\k'race'\k'typeabr'\k'id'(_(?'slot'[a-z]{3}))?_[a-z]+\.mtrl")]
|
||||
// public static partial Regex Regex();
|
||||
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string suffix,
|
||||
CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue)
|
||||
=> $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material/"
|
||||
+ (variant != byte.MaxValue ? $"v{variant:D4}/" : string.Empty)
|
||||
+ $"mt_c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}_{suffix}.mtrl";
|
||||
public static string FolderPath(GenderRace raceCode, BodySlot slot, SetId slotId, byte variant = byte.MaxValue)
|
||||
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/material{(variant != byte.MaxValue ? $"/v{variant:D4}" : string.Empty)}";
|
||||
|
||||
public static string HairPath(GenderRace raceCode, SetId slotId, string fileName, out GenderRace actualGr)
|
||||
{
|
||||
actualGr = MaterialHandling.GetGameGenderRace(raceCode, slotId);
|
||||
var folder = FolderPath(actualGr, BodySlot.Hair, slotId, 1);
|
||||
return actualGr == raceCode
|
||||
? $"{folder}{fileName}"
|
||||
: $"{folder}/mt_c{actualGr.ToRaceCode()}{fileName[9..]}";
|
||||
}
|
||||
|
||||
public static string TailPath(GenderRace raceCode, SetId slotId, string fileName, byte variant, out SetId actualSlotId)
|
||||
{
|
||||
switch (raceCode)
|
||||
{
|
||||
case GenderRace.HrothgarMale:
|
||||
case GenderRace.HrothgarFemale:
|
||||
case GenderRace.HrothgarMaleNpc:
|
||||
case GenderRace.HrothgarFemaleNpc:
|
||||
var folder = FolderPath(raceCode, BodySlot.Tail, 1, variant == byte.MaxValue ? (byte)1 : variant);
|
||||
actualSlotId = 1;
|
||||
return $"{folder}{fileName}";
|
||||
default:
|
||||
actualSlotId = slotId;
|
||||
return $"{FolderPath(raceCode, BodySlot.Tail, slotId, variant)}{fileName}";
|
||||
}
|
||||
}
|
||||
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, string fileName,
|
||||
out GenderRace actualGr, out SetId actualSlotId, byte variant = byte.MaxValue)
|
||||
{
|
||||
switch (slot)
|
||||
{
|
||||
case BodySlot.Hair:
|
||||
actualSlotId = slotId;
|
||||
return HairPath(raceCode, slotId, fileName, out actualGr);
|
||||
case BodySlot.Tail:
|
||||
actualGr = raceCode;
|
||||
return TailPath(raceCode, slotId, fileName, variant, out actualSlotId);
|
||||
default:
|
||||
actualSlotId = slotId;
|
||||
actualGr = raceCode;
|
||||
return $"{FolderPath(raceCode, slot, slotId, variant)}{fileName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static partial class Tex
|
||||
|
|
@ -236,10 +326,10 @@ public static partial class GamePaths
|
|||
|
||||
public static string Path(GenderRace raceCode, BodySlot slot, SetId slotId, char suffix1, bool minus = false,
|
||||
CustomizationType type = CustomizationType.Unknown, byte variant = byte.MaxValue, char suffix2 = '\0')
|
||||
=> $"chara/human/c{(ushort)raceCode:D4}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/"
|
||||
=> $"chara/human/c{raceCode.ToRaceCode()}/obj/{slot.ToSuffix()}/{slot.ToAbbreviation()}{slotId.Value:D4}/texture/"
|
||||
+ (minus ? "--" : string.Empty)
|
||||
+ (variant != byte.MaxValue ? $"v{variant:D2}_" : string.Empty)
|
||||
+ $"c{(ushort)raceCode:D4}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
+ $"c{raceCode.ToRaceCode()}{slot.ToAbbreviation()}{slotId.Value:D4}{(type != CustomizationType.Unknown ? $"_{type.ToSuffix()}" : string.Empty)}{(suffix2 != '\0' ? $"_{suffix2}" : string.Empty)}_{suffix1}.tex";
|
||||
|
||||
|
||||
// [GeneratedRegex(@"chara/common/texture/(?'catchlight'catchlight)(.*)\.tex")]
|
||||
|
|
|
|||
31
Penumbra.GameData/Data/MaterialHandling.cs
Normal file
31
Penumbra.GameData/Data/MaterialHandling.cs
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
|
||||
namespace Penumbra.GameData.Data;
|
||||
|
||||
public static class MaterialHandling
|
||||
{
|
||||
public static GenderRace GetGameGenderRace(GenderRace actualGr, SetId hairId)
|
||||
{
|
||||
// Hrothgar do not share hairstyles.
|
||||
if (actualGr is GenderRace.HrothgarFemale or GenderRace.HrothgarMale)
|
||||
return actualGr;
|
||||
|
||||
// Some hairstyles are miqo'te specific but otherwise shared.
|
||||
if (hairId.Value is >= 101 and <= 115)
|
||||
{
|
||||
if (actualGr is GenderRace.MiqoteFemale or GenderRace.MiqoteMale)
|
||||
return actualGr;
|
||||
|
||||
return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale;
|
||||
}
|
||||
|
||||
// All hairstyles above 116 are shared except for Hrothgar
|
||||
if (hairId.Value is >= 116 and <= 200)
|
||||
{
|
||||
return actualGr.Split().Item1 == Gender.Female ? GenderRace.MidlanderFemale : GenderRace.MidlanderMale;
|
||||
}
|
||||
|
||||
return actualGr;
|
||||
}
|
||||
}
|
||||
|
|
@ -37,6 +37,17 @@ public static class BodySlotEnumExtension
|
|||
BodySlot.Zear => 'z',
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
|
||||
public static CustomizationType ToCustomizationType(this BodySlot value)
|
||||
=> value switch
|
||||
{
|
||||
BodySlot.Hair => CustomizationType.Hair,
|
||||
BodySlot.Face => CustomizationType.Face,
|
||||
BodySlot.Tail => CustomizationType.Tail,
|
||||
BodySlot.Body => CustomizationType.Body,
|
||||
BodySlot.Zear => CustomizationType.Zear,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(value), value, null)
|
||||
};
|
||||
}
|
||||
|
||||
public static partial class Names
|
||||
|
|
|
|||
|
|
@ -328,7 +328,7 @@ public static class RaceEnumExtensions
|
|||
VieraFemaleNpc => "1804",
|
||||
UnknownMaleNpc => "9104",
|
||||
UnknownFemaleNpc => "9204",
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -427,7 +427,7 @@ public static partial class Names
|
|||
"1804" => VieraFemaleNpc,
|
||||
"9104" => UnknownMaleNpc,
|
||||
"9204" => UnknownFemaleNpc,
|
||||
_ => throw new KeyNotFoundException(),
|
||||
_ => Unknown,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,5 +2,6 @@ namespace Penumbra.GameData.Files;
|
|||
|
||||
public interface IWritable
|
||||
{
|
||||
public bool Valid { get; }
|
||||
public byte[] Write();
|
||||
}
|
||||
|
|
@ -85,6 +85,8 @@ public partial class MdlFile : IWritable
|
|||
// Raw, unparsed data.
|
||||
public byte[] RemainingData;
|
||||
|
||||
public bool Valid { get; }
|
||||
|
||||
public MdlFile(byte[] data)
|
||||
{
|
||||
using var stream = new MemoryStream(data);
|
||||
|
|
@ -180,6 +182,7 @@ public partial class MdlFile : IWritable
|
|||
BoneBoundingBoxes[i] = MdlStructs.BoundingBoxStruct.Read(r);
|
||||
|
||||
RemainingData = r.ReadBytes((int)(r.BaseStream.Length - r.BaseStream.Position));
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
private MdlStructs.ModelFileHeader LoadModelFileHeader(LuminaBinaryReader r)
|
||||
|
|
|
|||
|
|
@ -231,6 +231,9 @@ public partial class MtrlFile : IWritable
|
|||
{
|
||||
public string Path;
|
||||
public ushort Flags;
|
||||
|
||||
public bool DX11
|
||||
=> (Flags & 0x8000) != 0;
|
||||
}
|
||||
|
||||
public struct Constant
|
||||
|
|
@ -251,6 +254,7 @@ public partial class MtrlFile : IWritable
|
|||
|
||||
|
||||
public readonly uint Version;
|
||||
public bool Valid { get; }
|
||||
|
||||
public Texture[] Textures;
|
||||
public UvSet[] UvSets;
|
||||
|
|
@ -368,6 +372,7 @@ public partial class MtrlFile : IWritable
|
|||
ShaderPackage.Constants = r.ReadStructuresAsArray<Constant>(constantCount);
|
||||
ShaderPackage.Samplers = r.ReadStructuresAsArray<Sampler>(samplerCount);
|
||||
ShaderPackage.ShaderValues = r.ReadStructuresAsArray<float>(shaderValueListSize / 4);
|
||||
Valid = true;
|
||||
}
|
||||
|
||||
private static Texture[] ReadTextureOffsets(BinaryReader r, int count, out ushort[] offsets)
|
||||
|
|
|
|||
|
|
@ -87,39 +87,42 @@ public enum EqpEntry : ulong
|
|||
public static class Eqp
|
||||
{
|
||||
// cf. Client::Graphics::Scene::CharacterUtility.GetSlotEqpFlags
|
||||
public const EqpEntry DefaultEntry = ( EqpEntry )0x3fe00070603f00;
|
||||
public const EqpEntry DefaultEntry = (EqpEntry)0x3fe00070603f00;
|
||||
|
||||
public static (int, int) BytesAndOffset( EquipSlot slot )
|
||||
public static (int, int) BytesAndOffset(EquipSlot slot)
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
EquipSlot.Body => ( 2, 0 ),
|
||||
EquipSlot.Legs => ( 1, 2 ),
|
||||
EquipSlot.Hands => ( 1, 3 ),
|
||||
EquipSlot.Feet => ( 1, 4 ),
|
||||
EquipSlot.Head => ( 3, 5 ),
|
||||
EquipSlot.Body => (2, 0),
|
||||
EquipSlot.Legs => (1, 2),
|
||||
EquipSlot.Hands => (1, 3),
|
||||
EquipSlot.Feet => (1, 4),
|
||||
EquipSlot.Head => (3, 5),
|
||||
_ => throw new InvalidEnumArgumentException(),
|
||||
};
|
||||
}
|
||||
|
||||
public static EqpEntry FromSlotAndBytes( EquipSlot slot, byte[] value )
|
||||
public static EqpEntry ShiftAndMask(this EqpEntry entry, EquipSlot slot)
|
||||
{
|
||||
var (_, offset) = BytesAndOffset(slot);
|
||||
var mask = Mask(slot);
|
||||
return (EqpEntry)((ulong)(entry & mask) >> (offset * 8));
|
||||
}
|
||||
|
||||
public static EqpEntry FromSlotAndBytes(EquipSlot slot, byte[] value)
|
||||
{
|
||||
EqpEntry ret = 0;
|
||||
var (bytes, offset) = BytesAndOffset( slot );
|
||||
if( bytes != value.Length )
|
||||
{
|
||||
var (bytes, offset) = BytesAndOffset(slot);
|
||||
if (bytes != value.Length)
|
||||
throw new ArgumentException();
|
||||
}
|
||||
|
||||
for( var i = 0; i < bytes; ++i )
|
||||
{
|
||||
ret |= ( EqpEntry )( ( ulong )value[ i ] << ( ( offset + i ) * 8 ) );
|
||||
}
|
||||
for (var i = 0; i < bytes; ++i)
|
||||
ret |= (EqpEntry)((ulong)value[i] << ((offset + i) * 8));
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
public static EqpEntry Mask( EquipSlot slot )
|
||||
public static EqpEntry Mask(EquipSlot slot)
|
||||
{
|
||||
return slot switch
|
||||
{
|
||||
|
|
@ -132,7 +135,7 @@ public static class Eqp
|
|||
};
|
||||
}
|
||||
|
||||
public static EquipSlot ToEquipSlot( this EqpEntry entry )
|
||||
public static EquipSlot ToEquipSlot(this EqpEntry entry)
|
||||
{
|
||||
return entry switch
|
||||
{
|
||||
|
|
@ -211,7 +214,7 @@ public static class Eqp
|
|||
};
|
||||
}
|
||||
|
||||
public static string ToLocalName( this EqpEntry entry )
|
||||
public static string ToLocalName(this EqpEntry entry)
|
||||
{
|
||||
return entry switch
|
||||
{
|
||||
|
|
@ -289,25 +292,25 @@ public static class Eqp
|
|||
};
|
||||
}
|
||||
|
||||
private static EqpEntry[] GetEntriesForSlot( EquipSlot slot )
|
||||
private static EqpEntry[] GetEntriesForSlot(EquipSlot slot)
|
||||
{
|
||||
return ( ( EqpEntry[] )Enum.GetValues( typeof( EqpEntry ) ) )
|
||||
.Where( e => e.ToEquipSlot() == slot )
|
||||
.ToArray();
|
||||
return ((EqpEntry[])Enum.GetValues(typeof(EqpEntry)))
|
||||
.Where(e => e.ToEquipSlot() == slot)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot( EquipSlot.Body );
|
||||
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot( EquipSlot.Legs );
|
||||
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot( EquipSlot.Hands );
|
||||
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot( EquipSlot.Feet );
|
||||
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot( EquipSlot.Head );
|
||||
public static readonly EqpEntry[] EqpAttributesBody = GetEntriesForSlot(EquipSlot.Body);
|
||||
public static readonly EqpEntry[] EqpAttributesLegs = GetEntriesForSlot(EquipSlot.Legs);
|
||||
public static readonly EqpEntry[] EqpAttributesHands = GetEntriesForSlot(EquipSlot.Hands);
|
||||
public static readonly EqpEntry[] EqpAttributesFeet = GetEntriesForSlot(EquipSlot.Feet);
|
||||
public static readonly EqpEntry[] EqpAttributesHead = GetEntriesForSlot(EquipSlot.Head);
|
||||
|
||||
public static readonly IReadOnlyDictionary< EquipSlot, EqpEntry[] > EqpAttributes = new Dictionary< EquipSlot, EqpEntry[] >()
|
||||
public static readonly IReadOnlyDictionary<EquipSlot, EqpEntry[]> EqpAttributes = new Dictionary<EquipSlot, EqpEntry[]>()
|
||||
{
|
||||
[ EquipSlot.Body ] = EqpAttributesBody,
|
||||
[ EquipSlot.Legs ] = EqpAttributesLegs,
|
||||
[ EquipSlot.Hands ] = EqpAttributesHands,
|
||||
[ EquipSlot.Feet ] = EqpAttributesFeet,
|
||||
[ EquipSlot.Head ] = EqpAttributesHead,
|
||||
[EquipSlot.Body] = EqpAttributesBody,
|
||||
[EquipSlot.Legs] = EqpAttributesLegs,
|
||||
[EquipSlot.Hands] = EqpAttributesHands,
|
||||
[EquipSlot.Feet] = EqpAttributesFeet,
|
||||
[EquipSlot.Head] = EqpAttributesHead,
|
||||
};
|
||||
}
|
||||
|
|
@ -25,4 +25,7 @@ public unsafe struct MtrlResource
|
|||
|
||||
public byte* TexString( int idx )
|
||||
=> StringList + *( TexSpace + 4 + idx * 8 );
|
||||
|
||||
public bool TexIsDX11( int idx )
|
||||
=> *(TexSpace + 5 + idx * 8) >= 0x8000;
|
||||
}
|
||||
|
|
@ -198,6 +198,25 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa
|
|||
};
|
||||
}
|
||||
|
||||
public MetaManipulation WithEntryOf( MetaManipulation other )
|
||||
{
|
||||
if( ManipulationType != other.ManipulationType )
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
return ManipulationType switch
|
||||
{
|
||||
Type.Eqp => Eqp.Copy( other.Eqp.Entry ),
|
||||
Type.Gmp => Gmp.Copy( other.Gmp.Entry ),
|
||||
Type.Eqdp => Eqdp.Copy( other.Eqdp.Entry ),
|
||||
Type.Est => Est.Copy( other.Est.Entry ),
|
||||
Type.Rsp => Rsp.Copy( other.Rsp.Entry ),
|
||||
Type.Imc => Imc.Copy( other.Imc.Entry ),
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
}
|
||||
|
||||
public override bool Equals( object? obj )
|
||||
=> obj is MetaManipulation other && Equals( other );
|
||||
|
||||
|
|
@ -237,8 +256,8 @@ public readonly struct MetaManipulation : IEquatable< MetaManipulation >, ICompa
|
|||
=> ManipulationType switch
|
||||
{
|
||||
Type.Imc => $"{Imc.Entry.DecalId}-{Imc.Entry.MaterialId}-{Imc.Entry.VfxId}-{Imc.Entry.SoundId}-{Imc.Entry.MaterialAnimationId}-{Imc.Entry.AttributeMask}",
|
||||
Type.Eqdp => $"{(ushort) Eqdp.Entry:X}",
|
||||
Type.Eqp => $"{(ulong)Eqp.Entry:X}",
|
||||
Type.Eqdp => $"{( ushort )Eqdp.Entry:X}",
|
||||
Type.Eqp => $"{( ulong )Eqp.Entry:X}",
|
||||
Type.Est => $"{Est.Entry}",
|
||||
Type.Gmp => $"{Gmp.Entry.Value}",
|
||||
Type.Rsp => $"{Rsp.Entry}",
|
||||
|
|
|
|||
214
Penumbra/Mods/ItemSwap/CustomizationSwap.cs
Normal file
214
Penumbra/Mods/ItemSwap/CustomizationSwap.cs
Normal file
|
|
@ -0,0 +1,214 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
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 bool CreateMdl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, out FileSwap mdl )
|
||||
{
|
||||
if( idFrom.Value > byte.MaxValue )
|
||||
{
|
||||
mdl = new FileSwap();
|
||||
return false;
|
||||
}
|
||||
|
||||
var mdlPathFrom = GamePaths.Character.Mdl.Path( race, slot, idFrom, slot.ToCustomizationType() );
|
||||
var mdlPathTo = GamePaths.Character.Mdl.Path( race, slot, idTo, slot.ToCustomizationType() );
|
||||
|
||||
if( !FileSwap.CreateSwap( ResourceType.Mdl, redirections, mdlPathFrom, mdlPathTo, out mdl ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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() )
|
||||
{
|
||||
var name = materialFileName;
|
||||
foreach( var variant in Enumerable.Range( 1, range ) )
|
||||
{
|
||||
name = materialFileName;
|
||||
if( !CreateMtrl( redirections, slot, race, idFrom, idTo, ( byte )variant, ref name, ref mdl.DataWasChanged, out var mtrl ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mdl.ChildSwaps.Add( mtrl );
|
||||
}
|
||||
|
||||
materialFileName = name;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string ReplaceAnyId( string path, char idType, SetId id, bool condition = true )
|
||||
=> condition
|
||||
? Regex.Replace( path, $"{idType}\\d{{4}}", $"{idType}{id.Value:D4}" )
|
||||
: path;
|
||||
|
||||
public static string ReplaceAnyRace( string path, GenderRace to, bool condition = true )
|
||||
=> ReplaceAnyId( path, 'c', ( ushort )to, condition );
|
||||
|
||||
public static string ReplaceAnyBody( string path, BodySlot slot, SetId to, bool condition = true )
|
||||
=> ReplaceAnyId( path, slot.ToAbbreviation(), to, condition );
|
||||
|
||||
public static string ReplaceId( string path, char type, SetId idFrom, SetId idTo, bool condition = true )
|
||||
=> condition
|
||||
? path.Replace( $"{type}{idFrom.Value:D4}", $"{type}{idTo.Value:D4}" )
|
||||
: path;
|
||||
|
||||
public static string ReplaceRace( string path, GenderRace from, GenderRace to, bool condition = true )
|
||||
=> ReplaceId( path, 'c', ( ushort )from, ( ushort )to, condition );
|
||||
|
||||
public static string ReplaceBody( string path, BodySlot slot, SetId idFrom, SetId idTo, bool condition = true )
|
||||
=> ReplaceId( path, slot.ToAbbreviation(), idFrom, idTo, condition );
|
||||
|
||||
public static string AddSuffix( string path, string ext, string suffix, bool condition = true )
|
||||
=> condition
|
||||
? path.Replace( ext, suffix + ext )
|
||||
: path;
|
||||
|
||||
public static bool CreateMtrl( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, SetId idTo, byte variant,
|
||||
ref string fileName, ref bool dataWasChanged, out FileSwap mtrl )
|
||||
{
|
||||
variant = slot is BodySlot.Face or BodySlot.Zear ? byte.MaxValue : variant;
|
||||
var mtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, fileName, out var gameRaceFrom, out var gameSetIdFrom, variant );
|
||||
var mtrlToPath = GamePaths.Character.Mtrl.Path( race, slot, idTo, fileName, out var gameRaceTo, out var gameSetIdTo, variant );
|
||||
|
||||
var newFileName = fileName;
|
||||
newFileName = ReplaceRace( newFileName, gameRaceTo, race, gameRaceTo != race );
|
||||
newFileName = ReplaceBody( newFileName, slot, idTo, idFrom, idFrom.Value != idTo.Value );
|
||||
newFileName = AddSuffix( newFileName, ".mtrl", $"_c{race.ToRaceCode()}", gameRaceFrom != race );
|
||||
newFileName = AddSuffix( newFileName, ".mtrl", $"_{slot.ToAbbreviation()}{idFrom.Value:D4}", gameSetIdFrom.Value != idFrom.Value );
|
||||
|
||||
var actualMtrlFromPath = mtrlFromPath;
|
||||
if( newFileName != fileName )
|
||||
{
|
||||
actualMtrlFromPath = GamePaths.Character.Mtrl.Path( race, slot, idFrom, newFileName, out _, out _, variant );
|
||||
fileName = newFileName;
|
||||
dataWasChanged = true;
|
||||
}
|
||||
|
||||
if( !FileSwap.CreateSwap( ResourceType.Mtrl, redirections, actualMtrlFromPath, mtrlToPath, out mtrl, actualMtrlFromPath ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !CreateShader( redirections, ref mtrl.AsMtrl()!.ShaderPackage.Name, ref mtrl.DataWasChanged, out var shpk ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mtrl.ChildSwaps.Add( shpk );
|
||||
|
||||
foreach( ref var texture in mtrl.AsMtrl()!.Textures.AsSpan() )
|
||||
{
|
||||
if( !CreateTex( redirections, slot, race, idFrom, ref texture, ref mtrl.DataWasChanged, out var tex ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
mtrl.ChildSwaps.Add( tex );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CreateTex( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, SetId idFrom, ref MtrlFile.Texture texture,
|
||||
ref bool dataWasChanged, out FileSwap tex )
|
||||
{
|
||||
var path = texture.Path;
|
||||
var addedDashes = false;
|
||||
if( texture.DX11 )
|
||||
{
|
||||
var fileName = Path.GetFileName( path );
|
||||
if( !fileName.StartsWith( "--" ) )
|
||||
{
|
||||
path = path.Replace( fileName, $"--{fileName}" );
|
||||
addedDashes = true;
|
||||
}
|
||||
}
|
||||
|
||||
var newPath = ReplaceAnyRace( path, race );
|
||||
newPath = ReplaceAnyBody( newPath, slot, idFrom );
|
||||
if( newPath != path )
|
||||
{
|
||||
texture.Path = addedDashes ? newPath.Replace( "--", string.Empty ) : newPath;
|
||||
dataWasChanged = true;
|
||||
}
|
||||
|
||||
return FileSwap.CreateSwap( ResourceType.Tex, redirections, newPath, path, out tex, path );
|
||||
}
|
||||
|
||||
|
||||
public static bool CreateShader( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, ref string shaderName, ref bool dataWasChanged, out FileSwap shpk )
|
||||
{
|
||||
var path = $"shader/sm5/shpk/{shaderName}";
|
||||
return FileSwap.CreateSwap( ResourceType.Shpk, redirections, path, path, out shpk );
|
||||
}
|
||||
|
||||
/// <remarks> metaChanges is not manipulated, but IReadOnlySet does not support TryGetValue. </remarks>
|
||||
public static bool CreateEst( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, HashSet< MetaManipulation > metaChanges, BodySlot slot, GenderRace gr, SetId idFrom,
|
||||
SetId idTo, out MetaSwap? est )
|
||||
{
|
||||
var (gender, race) = gr.Split();
|
||||
var estSlot = slot switch
|
||||
{
|
||||
BodySlot.Hair => EstManipulation.EstType.Hair,
|
||||
BodySlot.Body => EstManipulation.EstType.Body,
|
||||
_ => ( EstManipulation.EstType )0,
|
||||
};
|
||||
if( estSlot == 0 )
|
||||
{
|
||||
est = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
var fromDefault = new EstManipulation( gender, race, estSlot, idFrom.Value, EstFile.GetDefault( estSlot, gr, idFrom.Value ) );
|
||||
var toDefault = new EstManipulation( gender, race, estSlot, idTo.Value, EstFile.GetDefault( estSlot, gr, idTo.Value ) );
|
||||
est = new MetaSwap( metaChanges, fromDefault, toDefault );
|
||||
|
||||
if( est.SwapApplied.Est.Entry >= 2 )
|
||||
{
|
||||
if( !CreatePhyb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var phyb ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !CreateSklb( redirections, slot, gr, est.SwapApplied.Est.Entry, out var sklb ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
est.ChildSwaps.Add( phyb );
|
||||
est.ChildSwaps.Add( sklb );
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool CreatePhyb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap phyb )
|
||||
{
|
||||
var phybPath = GamePaths.Character.Phyb.Path( race, slot, estEntry );
|
||||
return FileSwap.CreateSwap( ResourceType.Phyb, redirections, phybPath, phybPath, out phyb );
|
||||
}
|
||||
|
||||
public static bool CreateSklb( IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, BodySlot slot, GenderRace race, ushort estEntry, out FileSwap sklb )
|
||||
{
|
||||
var sklbPath = GamePaths.Character.Sklb.Path( race, slot, estEntry );
|
||||
return FileSwap.CreateSwap( ResourceType.Sklb, redirections, sklbPath, sklbPath, out sklb );
|
||||
}
|
||||
}
|
||||
230
Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs
Normal file
230
Penumbra/Mods/ItemSwap/EquipmentDataContainer.cs
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Lumina.Data.Parsing;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using Penumbra.GameData.Data;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Interop.Structs;
|
||||
using Penumbra.Meta.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.Mods.ItemSwap;
|
||||
|
||||
public class EquipmentDataContainer
|
||||
{
|
||||
public Item Item;
|
||||
public EquipSlot Slot;
|
||||
public SetId ModelId;
|
||||
public byte Variant;
|
||||
|
||||
public ImcManipulation ImcData;
|
||||
|
||||
public EqpManipulation EqpData;
|
||||
public GmpManipulation GmpData;
|
||||
|
||||
// Example: Abyssos Helm / Body
|
||||
public string AvfxPath = string.Empty;
|
||||
|
||||
// Example: Dodore Doublet, but unknown what it does?
|
||||
public string SoundPath = string.Empty;
|
||||
|
||||
// Example: Crimson Standard Bracelet
|
||||
public string DecalPath = string.Empty;
|
||||
|
||||
// Example: The Howling Spirit and The Wailing Spirit, but unknown what it does.
|
||||
public string AnimationPath = string.Empty;
|
||||
|
||||
public Dictionary< GenderRace, GenderRaceContainer > Files = new();
|
||||
|
||||
public struct GenderRaceContainer
|
||||
{
|
||||
public EqdpManipulation Eqdp;
|
||||
public GenderRace ModelRace;
|
||||
public GenderRace MaterialRace;
|
||||
public EstManipulation Est;
|
||||
public string MdlPath;
|
||||
public MtrlContainer[] MtrlPaths;
|
||||
}
|
||||
|
||||
public struct MtrlContainer
|
||||
{
|
||||
public string MtrlPath;
|
||||
public string[] Textures;
|
||||
public string Shader;
|
||||
|
||||
public MtrlContainer( string mtrlPath )
|
||||
{
|
||||
MtrlPath = mtrlPath;
|
||||
var file = Dalamud.GameData.GetFile( mtrlPath );
|
||||
if( file != null )
|
||||
{
|
||||
var mtrl = new MtrlFile( file.Data );
|
||||
Textures = mtrl.Textures.Select( t => t.Path ).ToArray();
|
||||
Shader = $"shader/sm5/shpk/{mtrl.ShaderPackage.Name}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Textures = Array.Empty< string >();
|
||||
Shader = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static EstManipulation GetEstEntry( GenderRace genderRace, SetId setId, EquipSlot slot )
|
||||
{
|
||||
if( slot == EquipSlot.Head )
|
||||
{
|
||||
var entry = EstFile.GetDefault( EstManipulation.EstType.Head, genderRace, setId.Value );
|
||||
return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Head, setId.Value, entry );
|
||||
}
|
||||
|
||||
if( slot == EquipSlot.Body )
|
||||
{
|
||||
var entry = EstFile.GetDefault( EstManipulation.EstType.Body, genderRace, setId.Value );
|
||||
return new EstManipulation( genderRace.Split().Item1, genderRace.Split().Item2, EstManipulation.EstType.Body, setId.Value, entry );
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
private static GenderRaceContainer GetGenderRace( GenderRace genderRace, SetId modelId, EquipSlot slot, ushort materialId )
|
||||
{
|
||||
var ret = new GenderRaceContainer()
|
||||
{
|
||||
Eqdp = GetEqdpEntry( genderRace, modelId, slot ),
|
||||
Est = GetEstEntry( genderRace, modelId, slot ),
|
||||
};
|
||||
( ret.ModelRace, ret.MaterialRace ) = TraverseEqdpTree( genderRace, modelId, slot );
|
||||
ret.MdlPath = GamePaths.Equipment.Mdl.Path( modelId, ret.ModelRace, slot );
|
||||
ret.MtrlPaths = MtrlPaths( ret.MdlPath, ret.MaterialRace, modelId, materialId );
|
||||
return ret;
|
||||
}
|
||||
|
||||
private static EqdpManipulation GetEqdpEntry( GenderRace genderRace, SetId modelId, EquipSlot slot )
|
||||
{
|
||||
var entry = ExpandedEqdpFile.GetDefault( genderRace, slot.IsAccessory(), modelId.Value );
|
||||
return new EqdpManipulation( entry, slot, genderRace.Split().Item1, genderRace.Split().Item2, modelId.Value );
|
||||
}
|
||||
|
||||
private static MtrlContainer[] MtrlPaths( string mdlPath, GenderRace mtrlRace, SetId modelId, ushort materialId )
|
||||
{
|
||||
var file = Dalamud.GameData.GetFile( mdlPath );
|
||||
if( file == null )
|
||||
{
|
||||
return Array.Empty< MtrlContainer >();
|
||||
}
|
||||
|
||||
var mdl = new MdlFile( Dalamud.GameData.GetFile( mdlPath )!.Data );
|
||||
var basePath = GamePaths.Equipment.Mtrl.FolderPath( modelId, ( byte )materialId );
|
||||
var equipPart = $"e{modelId.Value:D4}";
|
||||
var racePart = $"c{mtrlRace.ToRaceCode()}";
|
||||
|
||||
return mdl.Materials
|
||||
.Where( m => m.Contains( equipPart ) )
|
||||
.Select( m => new MtrlContainer( $"{basePath}{m.Replace( "c0101", racePart )}" ) )
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static (GenderRace, GenderRace) TraverseEqdpTree( GenderRace genderRace, SetId modelId, EquipSlot slot )
|
||||
{
|
||||
var model = GenderRace.Unknown;
|
||||
var material = GenderRace.Unknown;
|
||||
var accessory = slot.IsAccessory();
|
||||
foreach( var gr in genderRace.Dependencies() )
|
||||
{
|
||||
var entry = ExpandedEqdpFile.GetDefault( gr, accessory, modelId.Value );
|
||||
var (b1, b2) = entry.ToBits( slot );
|
||||
if( b1 && material == GenderRace.Unknown )
|
||||
{
|
||||
material = gr;
|
||||
if( model != GenderRace.Unknown )
|
||||
{
|
||||
return ( model, material );
|
||||
}
|
||||
}
|
||||
|
||||
if( b2 && model == GenderRace.Unknown )
|
||||
{
|
||||
model = gr;
|
||||
if( material != GenderRace.Unknown )
|
||||
{
|
||||
return ( model, material );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ( GenderRace.MidlanderMale, GenderRace.MidlanderMale );
|
||||
}
|
||||
|
||||
|
||||
public EquipmentDataContainer( Item i )
|
||||
{
|
||||
Item = i;
|
||||
LookupItem( i, out Slot, out ModelId, out Variant );
|
||||
LookupImc( ModelId, Variant, Slot );
|
||||
EqpData = new EqpManipulation( ExpandedEqpFile.GetDefault( ModelId.Value ), Slot, ModelId.Value );
|
||||
GmpData = Slot == EquipSlot.Head ? new GmpManipulation( ExpandedGmpFile.GetDefault( ModelId.Value ), ModelId.Value ) : default;
|
||||
|
||||
|
||||
foreach( var genderRace in Enum.GetValues< GenderRace >() )
|
||||
{
|
||||
if( CharacterUtility.EqdpIdx( genderRace, Slot.IsAccessory() ) < 0 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
Files[ genderRace ] = GetGenderRace( genderRace, ModelId, Slot, ImcData.Entry.MaterialId );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static void LookupItem( Item i, out EquipSlot slot, out SetId modelId, out byte variant )
|
||||
{
|
||||
slot = ( ( EquipSlot )i.EquipSlotCategory.Row ).ToSlot();
|
||||
if( !slot.IsEquipment() )
|
||||
{
|
||||
throw new ItemSwap.InvalidItemTypeException();
|
||||
}
|
||||
|
||||
modelId = ( ( Quad )i.ModelMain ).A;
|
||||
variant = ( byte )( ( Quad )i.ModelMain ).B;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private void LookupImc( SetId modelId, byte variant, EquipSlot slot )
|
||||
{
|
||||
var imc = ImcFile.GetDefault( GamePaths.Equipment.Imc.Path( modelId ), slot, variant, out var exists );
|
||||
if( !exists )
|
||||
{
|
||||
throw new ItemSwap.InvalidImcException();
|
||||
}
|
||||
|
||||
ImcData = new ImcManipulation( slot, variant, modelId.Value, imc );
|
||||
if( imc.DecalId != 0 )
|
||||
{
|
||||
DecalPath = GamePaths.Equipment.Decal.Path( imc.DecalId );
|
||||
}
|
||||
|
||||
// TODO: Figure out how this works.
|
||||
if( imc.SoundId != 0 )
|
||||
{
|
||||
SoundPath = string.Empty;
|
||||
}
|
||||
|
||||
if( imc.VfxId != 0 )
|
||||
{
|
||||
AvfxPath = GamePaths.Equipment.Avfx.Path( modelId, imc.VfxId );
|
||||
}
|
||||
|
||||
// TODO: Figure out how this works.
|
||||
if( imc.MaterialAnimationId != 0 )
|
||||
{
|
||||
AnimationPath = string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
112
Penumbra/Mods/ItemSwap/ItemSwap.cs
Normal file
112
Penumbra/Mods/ItemSwap/ItemSwap.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Mods.ItemSwap;
|
||||
|
||||
public static class ItemSwap
|
||||
{
|
||||
public class InvalidItemTypeException : Exception
|
||||
{ }
|
||||
|
||||
public class InvalidImcException : Exception
|
||||
{ }
|
||||
|
||||
public class IdUnavailableException : Exception
|
||||
{ }
|
||||
|
||||
private static bool LoadFile( FullPath path, out byte[] data )
|
||||
{
|
||||
if( path.FullName.Length > 0 )
|
||||
{
|
||||
try
|
||||
{
|
||||
if( path.IsRooted )
|
||||
{
|
||||
data = File.ReadAllBytes( path.FullName );
|
||||
return true;
|
||||
}
|
||||
|
||||
var file = Dalamud.GameData.GetFile( path.InternalName.ToString() );
|
||||
if( file != null )
|
||||
{
|
||||
data = file.Data;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Debug( $"Could not load file {path}:\n{e}" );
|
||||
}
|
||||
}
|
||||
|
||||
data = Array.Empty< byte >();
|
||||
return false;
|
||||
}
|
||||
|
||||
public class GenericFile : IWritable
|
||||
{
|
||||
public readonly byte[] Data;
|
||||
public bool Valid { get; }
|
||||
|
||||
public GenericFile( FullPath path )
|
||||
=> Valid = LoadFile( path, out Data );
|
||||
|
||||
public byte[] Write()
|
||||
=> Data;
|
||||
|
||||
public static readonly GenericFile Invalid = new(FullPath.Empty);
|
||||
}
|
||||
|
||||
public static bool LoadFile( FullPath path, [NotNullWhen( true )] out GenericFile? file )
|
||||
{
|
||||
file = new GenericFile( path );
|
||||
if( file.Valid )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool LoadMdl( FullPath path, [NotNullWhen( true )] out MdlFile? file )
|
||||
{
|
||||
try
|
||||
{
|
||||
if( LoadFile( path, out byte[] data ) )
|
||||
{
|
||||
file = new MdlFile( data );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Debug( $"Could not parse file {path} to Mdl:\n{e}" );
|
||||
}
|
||||
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool LoadMtrl( FullPath path, [NotNullWhen( true )] out MtrlFile? file )
|
||||
{
|
||||
try
|
||||
{
|
||||
if( LoadFile( path, out byte[] data ) )
|
||||
{
|
||||
file = new MtrlFile( data );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Debug( $"Could not parse file {path} to Mtrl:\n{e}" );
|
||||
}
|
||||
|
||||
file = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
134
Penumbra/Mods/ItemSwap/ItemSwapContainer.cs
Normal file
134
Penumbra/Mods/ItemSwap/ItemSwapContainer.cs
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Mods.ItemSwap;
|
||||
|
||||
public class ItemSwapContainer
|
||||
{
|
||||
private Dictionary< Utf8GamePath, FullPath > _modRedirections = new();
|
||||
private HashSet< MetaManipulation > _modManipulations = new();
|
||||
|
||||
public IReadOnlyDictionary< Utf8GamePath, FullPath > ModRedirections
|
||||
=> _modRedirections;
|
||||
|
||||
public IReadOnlySet< MetaManipulation > ModManipulations
|
||||
=> _modManipulations;
|
||||
|
||||
public readonly List< Swap > Swaps = new();
|
||||
public bool Loaded { get; private set; }
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Swaps.Clear();
|
||||
Loaded = false;
|
||||
}
|
||||
|
||||
public enum WriteType
|
||||
{
|
||||
UseSwaps,
|
||||
NoSwaps,
|
||||
}
|
||||
|
||||
public bool WriteMod( Mod mod, WriteType writeType = WriteType.NoSwaps )
|
||||
{
|
||||
var convertedManips = new HashSet< MetaManipulation >( Swaps.Count );
|
||||
var convertedFiles = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
|
||||
var convertedSwaps = new Dictionary< Utf8GamePath, FullPath >( Swaps.Count );
|
||||
try
|
||||
{
|
||||
foreach( var swap in Swaps.SelectMany( s => s.WithChildren() ) )
|
||||
{
|
||||
switch( swap )
|
||||
{
|
||||
case FileSwap file:
|
||||
// Skip, nothing to do
|
||||
if( file.SwapToModdedEqualsOriginal )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
if( writeType == WriteType.UseSwaps && file.SwapToModdedExistsInGame && !file.DataWasChanged )
|
||||
{
|
||||
convertedSwaps.TryAdd( file.SwapFromRequestPath, file.SwapToModded );
|
||||
}
|
||||
else
|
||||
{
|
||||
var path = file.GetNewPath( mod.ModPath.FullName );
|
||||
var bytes = file.FileData.Write();
|
||||
Directory.CreateDirectory( Path.GetDirectoryName( path )! );
|
||||
File.WriteAllBytes( path, bytes );
|
||||
convertedFiles.TryAdd( file.SwapFromRequestPath, new FullPath( path ) );
|
||||
}
|
||||
|
||||
break;
|
||||
case MetaSwap meta:
|
||||
if( !meta.SwapAppliedIsDefault )
|
||||
{
|
||||
convertedManips.Add( meta.SwapApplied );
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Penumbra.ModManager.OptionSetFiles( mod, -1, 0, convertedFiles );
|
||||
Penumbra.ModManager.OptionSetFileSwaps( mod, -1, 0, convertedSwaps );
|
||||
Penumbra.ModManager.OptionSetManipulations( mod, -1, 0, convertedManips );
|
||||
return true;
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not write FileSwapContainer to {mod.ModPath}:\n{e}" );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadMod( Mod? mod, ModSettings? settings )
|
||||
{
|
||||
Clear();
|
||||
if( mod == null )
|
||||
{
|
||||
_modRedirections = new Dictionary< Utf8GamePath, FullPath >();
|
||||
_modManipulations = new HashSet< MetaManipulation >();
|
||||
}
|
||||
else
|
||||
{
|
||||
( _modRedirections, _modManipulations ) = ModSettings.GetResolveData( mod, settings );
|
||||
}
|
||||
}
|
||||
|
||||
public ItemSwapContainer()
|
||||
{
|
||||
LoadMod( null, null );
|
||||
}
|
||||
|
||||
|
||||
public bool LoadCustomization( BodySlot slot, GenderRace race, SetId from, SetId to )
|
||||
{
|
||||
if( !CustomizationSwap.CreateMdl( ModRedirections, slot, race, from, to, out var mdl ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if( !CustomizationSwap.CreateEst( ModRedirections, _modManipulations, slot, race, from, to, out var est ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Swaps.Add( mdl );
|
||||
if( est != null )
|
||||
{
|
||||
Swaps.Add( est );
|
||||
}
|
||||
|
||||
Loaded = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
185
Penumbra/Mods/ItemSwap/Swaps.cs
Normal file
185
Penumbra/Mods/ItemSwap/Swaps.cs
Normal file
|
|
@ -0,0 +1,185 @@
|
|||
using System;
|
||||
using Penumbra.GameData.Files;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using Penumbra.GameData.Enums;
|
||||
|
||||
namespace Penumbra.Mods.ItemSwap;
|
||||
|
||||
public class Swap
|
||||
{
|
||||
/// <summary> Any further swaps belonging specifically to this tree of changes. </summary>
|
||||
public List< Swap > ChildSwaps = new();
|
||||
|
||||
public IEnumerable< Swap > WithChildren()
|
||||
=> ChildSwaps.SelectMany( c => c.WithChildren() ).Prepend( this );
|
||||
}
|
||||
|
||||
public sealed class MetaSwap : Swap
|
||||
{
|
||||
/// <summary> The default value of a specific meta manipulation that needs to be redirected. </summary>
|
||||
public MetaManipulation SwapFrom;
|
||||
|
||||
/// <summary> The default value of the same Meta entry of the redirected item. </summary>
|
||||
public MetaManipulation SwapToDefault;
|
||||
|
||||
/// <summary> The modded value of the same Meta entry of the redirected item, or the same as SwapToDefault if unmodded. </summary>
|
||||
public MetaManipulation SwapToModded;
|
||||
|
||||
/// <summary> The modded value applied to the specific meta manipulation target before redirection. </summary>
|
||||
public MetaManipulation SwapApplied;
|
||||
|
||||
/// <summary> Whether SwapToModded equals SwapToDefault. </summary>
|
||||
public bool SwapToIsDefault;
|
||||
|
||||
/// <summary> Whether the applied meta manipulation does not change anything against the default. </summary>
|
||||
public bool SwapAppliedIsDefault;
|
||||
|
||||
/// <summary>
|
||||
/// Create a new MetaSwap from the original meta identifier and the target meta identifier.
|
||||
/// </summary>
|
||||
/// <param name="manipulations">A set of modded meta manipulations to consider. This is not manipulated, but can not be IReadOnly because TryGetValue is not available for that.</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( HashSet< MetaManipulation > manipulations, MetaManipulation manipFrom, MetaManipulation manipTo )
|
||||
{
|
||||
SwapFrom = manipFrom;
|
||||
SwapToDefault = manipTo;
|
||||
|
||||
if( manipulations.TryGetValue( manipTo, out var actual ) )
|
||||
{
|
||||
SwapToModded = actual;
|
||||
SwapToIsDefault = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
SwapToModded = manipTo;
|
||||
SwapToIsDefault = true;
|
||||
}
|
||||
|
||||
SwapApplied = SwapFrom.WithEntryOf( SwapToModded );
|
||||
SwapAppliedIsDefault = SwapApplied.EntryEquals( SwapFrom );
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class FileSwap : Swap
|
||||
{
|
||||
/// <summary> The file type, used for bookkeeping. </summary>
|
||||
public ResourceType Type;
|
||||
|
||||
/// <summary> The binary or parsed data of the file at SwapToModded. </summary>
|
||||
public IWritable FileData = ItemSwap.GenericFile.Invalid;
|
||||
|
||||
/// <summary> The path that would be requested without manipulated parent files. </summary>
|
||||
public string SwapFromPreChangePath = string.Empty;
|
||||
|
||||
/// <summary> The Path that needs to be redirected. </summary>
|
||||
public Utf8GamePath SwapFromRequestPath;
|
||||
|
||||
/// <summary> The path that the game should request instead, if no mods are involved. </summary>
|
||||
public Utf8GamePath SwapToRequestPath;
|
||||
|
||||
/// <summary> The path to the actual file that should be loaded. This can be the same as SwapToRequestPath or a file on the drive. </summary>
|
||||
public FullPath SwapToModded;
|
||||
|
||||
/// <summary> Whether the target file is an actual game file. </summary>
|
||||
public bool SwapToModdedExistsInGame;
|
||||
|
||||
/// <summary> Whether the target file could be read either from the game or the drive. </summary>
|
||||
public bool SwapToModdedExists
|
||||
=> FileData.Valid;
|
||||
|
||||
/// <summary> Whether SwapToModded is a path to a game file that equals SwapFromGamePath. </summary>
|
||||
public bool SwapToModdedEqualsOriginal;
|
||||
|
||||
/// <summary> Whether the data in FileData was manipulated from the original file. </summary>
|
||||
public bool DataWasChanged;
|
||||
|
||||
/// <summary> Whether SwapFromPreChangePath equals SwapFromRequest. </summary>
|
||||
public bool SwapFromChanged;
|
||||
|
||||
public string GetNewPath( string newMod )
|
||||
=> Path.Combine( newMod, new Utf8RelPath( SwapFromRequestPath ).ToString() );
|
||||
|
||||
public MdlFile? AsMdl()
|
||||
=> FileData as MdlFile;
|
||||
|
||||
public MtrlFile? AsMtrl()
|
||||
=> FileData as MtrlFile;
|
||||
|
||||
/// <summary>
|
||||
/// Create a full swap container for a specific file type using a modded redirection set, the actually requested path and the game file it should load instead after the swap.
|
||||
/// </summary>
|
||||
/// <param name="type">The file type. Mdl and Mtrl have special file loading treatment.</param>
|
||||
/// <param name="redirections">The set of redirections that need to be considered.</param>
|
||||
/// <param name="swapFromRequest">The path the game is going to request when loading the file.</param>
|
||||
/// <param name="swapToRequest">The unmodded path to the file the game is supposed to load instead.</param>
|
||||
/// <param name="swap">A full swap container with the actual file in memory.</param>
|
||||
/// <returns>True if everything could be read correctly, false otherwise.</returns>
|
||||
public static bool CreateSwap( ResourceType type, IReadOnlyDictionary< Utf8GamePath, FullPath > redirections, string swapFromRequest, string swapToRequest, out FileSwap swap,
|
||||
string? swapFromPreChange = null )
|
||||
{
|
||||
swap = new FileSwap
|
||||
{
|
||||
Type = type,
|
||||
FileData = ItemSwap.GenericFile.Invalid,
|
||||
DataWasChanged = false,
|
||||
SwapFromPreChangePath = swapFromPreChange ?? swapFromRequest,
|
||||
SwapFromChanged = swapFromPreChange != swapFromRequest,
|
||||
SwapFromRequestPath = Utf8GamePath.Empty,
|
||||
SwapToRequestPath = Utf8GamePath.Empty,
|
||||
SwapToModded = FullPath.Empty,
|
||||
};
|
||||
|
||||
if( swapFromRequest.Length == 0
|
||||
|| swapToRequest.Length == 0
|
||||
|| !Utf8GamePath.FromString( swapToRequest, out swap.SwapToRequestPath )
|
||||
|| !Utf8GamePath.FromString( swapFromRequest, out swap.SwapFromRequestPath ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
swap.SwapToModded = redirections.TryGetValue( swap.SwapToRequestPath, out var p ) ? p : new FullPath( swap.SwapToRequestPath );
|
||||
swap.SwapToModdedExistsInGame = !swap.SwapToModded.IsRooted && Dalamud.GameData.FileExists( swap.SwapToModded.InternalName.ToString() );
|
||||
swap.SwapToModdedEqualsOriginal = !swap.SwapToModded.IsRooted && swap.SwapToModded.InternalName.Equals( swap.SwapFromRequestPath.Path );
|
||||
|
||||
swap.FileData = type switch
|
||||
{
|
||||
ResourceType.Mdl => ItemSwap.LoadMdl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
|
||||
ResourceType.Mtrl => ItemSwap.LoadMtrl( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
|
||||
_ => ItemSwap.LoadFile( swap.SwapToModded, out var f ) ? f : ItemSwap.GenericFile.Invalid,
|
||||
};
|
||||
|
||||
return swap.SwapToModdedExists;
|
||||
}
|
||||
|
||||
|
||||
/// <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( IReadOnlyDictionary< 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 );
|
||||
if( !CreateSwap( swap.Type, redirections, newPath, swap.SwapToRequestPath.ToString(), out var newSwap ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
path = newPath;
|
||||
dataWasChanged = true;
|
||||
swap = newSwap;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,8 @@ using System.Numerics;
|
|||
using OtterGui;
|
||||
using OtterGui.Filesystem;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
using Penumbra.String.Classes;
|
||||
|
||||
namespace Penumbra.Mods;
|
||||
|
||||
|
|
@ -34,6 +36,56 @@ public class ModSettings
|
|||
Settings = mod.Groups.Select( g => g.DefaultSettings ).ToList(),
|
||||
};
|
||||
|
||||
// Return everything required to resolve things for a single mod with given settings (which can be null, in which case the default is used.
|
||||
public static (Dictionary< Utf8GamePath, FullPath >, HashSet< MetaManipulation >) GetResolveData( Mod mod, ModSettings? settings )
|
||||
{
|
||||
if( settings == null )
|
||||
{
|
||||
settings = DefaultSettings( mod );
|
||||
}
|
||||
else
|
||||
{
|
||||
settings.AddMissingSettings( mod );
|
||||
}
|
||||
|
||||
var dict = new Dictionary< Utf8GamePath, FullPath >();
|
||||
var set = new HashSet< MetaManipulation >();
|
||||
|
||||
void AddOption( ISubMod option )
|
||||
{
|
||||
foreach( var (path, file) in option.Files.Concat( option.FileSwaps ) )
|
||||
{
|
||||
dict.TryAdd( path, file );
|
||||
}
|
||||
|
||||
foreach( var manip in option.Manipulations )
|
||||
{
|
||||
set.Add( manip );
|
||||
}
|
||||
}
|
||||
|
||||
foreach( var (group, index) in mod.Groups.WithIndex().OrderByDescending( g => g.Value.Priority ) )
|
||||
{
|
||||
if( group.Type is GroupType.Single )
|
||||
{
|
||||
AddOption( group[ ( int )settings.Settings[ index ] ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( var (option, optionIdx) in group.WithIndex().OrderByDescending( o => group.OptionPriority( o.Index ) ) )
|
||||
{
|
||||
if( ( ( settings.Settings[ index ] >> optionIdx ) & 1 ) == 1 )
|
||||
{
|
||||
AddOption( option );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AddOption( mod.Default );
|
||||
return ( dict, set );
|
||||
}
|
||||
|
||||
// Automatically react to changes in a mods available options.
|
||||
public bool HandleChanges( ModOptionChangeType type, Mod mod, int groupIdx, int optionIdx, int movedToIdx )
|
||||
{
|
||||
|
|
@ -42,7 +94,7 @@ public class ModSettings
|
|||
case ModOptionChangeType.GroupRenamed: return true;
|
||||
case ModOptionChangeType.GroupAdded:
|
||||
// Add new empty setting for new mod.
|
||||
Settings.Insert( groupIdx, mod.Groups[groupIdx].DefaultSettings );
|
||||
Settings.Insert( groupIdx, mod.Groups[ groupIdx ].DefaultSettings );
|
||||
return true;
|
||||
case ModOptionChangeType.GroupDeleted:
|
||||
// Remove setting for deleted mod.
|
||||
|
|
@ -59,7 +111,7 @@ public class ModSettings
|
|||
{
|
||||
GroupType.Single => ( uint )Math.Max( Math.Min( group.Count - 1, BitOperations.TrailingZeroCount( config ) ), 0 ),
|
||||
GroupType.Multi => 1u << ( int )config,
|
||||
_ => config,
|
||||
_ => config,
|
||||
};
|
||||
return config != Settings[ groupIdx ];
|
||||
}
|
||||
|
|
@ -73,7 +125,7 @@ public class ModSettings
|
|||
{
|
||||
GroupType.Single => config >= optionIdx ? config > 1 ? config - 1 : 0 : config,
|
||||
GroupType.Multi => Functions.RemoveBit( config, optionIdx ),
|
||||
_ => config,
|
||||
_ => config,
|
||||
};
|
||||
return config != Settings[ groupIdx ];
|
||||
}
|
||||
|
|
@ -90,7 +142,7 @@ public class ModSettings
|
|||
{
|
||||
GroupType.Single => config == optionIdx ? ( uint )movedToIdx : config,
|
||||
GroupType.Multi => Functions.MoveBit( config, optionIdx, movedToIdx ),
|
||||
_ => config,
|
||||
_ => config,
|
||||
};
|
||||
return config != Settings[ groupIdx ];
|
||||
}
|
||||
|
|
@ -104,27 +156,28 @@ public class ModSettings
|
|||
{
|
||||
GroupType.Single => ( uint )Math.Min( value, group.Count - 1 ),
|
||||
GroupType.Multi => ( uint )( value & ( ( 1ul << group.Count ) - 1 ) ),
|
||||
_ => value,
|
||||
_ => value,
|
||||
};
|
||||
|
||||
// Set a setting. Ensures that there are enough settings and fixes the setting beforehand.
|
||||
public void SetValue( Mod mod, int groupIdx, uint newValue )
|
||||
{
|
||||
AddMissingSettings( groupIdx + 1 );
|
||||
AddMissingSettings( mod );
|
||||
var group = mod.Groups[ groupIdx ];
|
||||
Settings[ groupIdx ] = FixSetting( group, newValue );
|
||||
}
|
||||
|
||||
// Add defaulted settings up to the required count.
|
||||
private bool AddMissingSettings( int totalCount )
|
||||
private bool AddMissingSettings( Mod mod )
|
||||
{
|
||||
if( totalCount <= Settings.Count )
|
||||
var changes = false;
|
||||
for( var i = Settings.Count; i < mod.Groups.Count; ++i )
|
||||
{
|
||||
return false;
|
||||
Settings.Add( mod.Groups[ i ].DefaultSettings );
|
||||
changes = true;
|
||||
}
|
||||
|
||||
Settings.AddRange( Enumerable.Repeat( 0u, totalCount - Settings.Count ) );
|
||||
return true;
|
||||
return changes;
|
||||
}
|
||||
|
||||
// A simple struct conversion to easily save settings by name instead of value.
|
||||
|
|
@ -147,7 +200,7 @@ public class ModSettings
|
|||
Priority = settings.Priority;
|
||||
Enabled = settings.Enabled;
|
||||
Settings = new Dictionary< string, long >( mod.Groups.Count );
|
||||
settings.AddMissingSettings( mod.Groups.Count );
|
||||
settings.AddMissingSettings( mod );
|
||||
|
||||
foreach( var (group, setting) in mod.Groups.Zip( settings.Settings ) )
|
||||
{
|
||||
|
|
|
|||
45
Penumbra/UI/Classes/Combos.cs
Normal file
45
Penumbra/UI/Classes/Combos.cs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
using Dalamud.Interface;
|
||||
using OtterGui;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.Meta.Manipulations;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public static class Combos
|
||||
{
|
||||
// Different combos to use with enums.
|
||||
public static bool Race( string label, ModelRace current, out ModelRace race )
|
||||
=> Race( label, 100, current, out race );
|
||||
|
||||
public static bool Race( string label, float unscaledWidth, ModelRace current, out ModelRace race )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
public static bool Gender( string label, Gender current, out Gender gender )
|
||||
=> Gender( label, 120, current, out gender );
|
||||
|
||||
public static bool Gender( string label, float unscaledWidth, Gender current, out Gender gender )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, unscaledWidth * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
public static bool EqdpEquipSlot( string label, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName );
|
||||
|
||||
public static bool EqpEquipSlot( string label, float width, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName );
|
||||
|
||||
public static bool AccessorySlot( string label, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName );
|
||||
|
||||
public static bool SubRace( string label, SubRace current, out SubRace subRace )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
public static bool RspAttribute( string label, RspAttribute current, out RspAttribute attribute )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute,
|
||||
RspAttributeExtensions.ToFullString, 0, 1 );
|
||||
|
||||
public static bool EstSlot( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute );
|
||||
|
||||
public static bool ImcType( string label, ObjectType current, out ObjectType type )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes,
|
||||
ObjectTypeExtensions.ToName );
|
||||
}
|
||||
490
Penumbra/UI/Classes/ItemSwapWindow.cs
Normal file
490
Penumbra/UI/Classes/ItemSwapWindow.cs
Normal file
|
|
@ -0,0 +1,490 @@
|
|||
using System;
|
||||
using System.Linq;
|
||||
using System.Numerics;
|
||||
using Dalamud.Interface;
|
||||
using ImGuiNET;
|
||||
using Lumina.Excel.GeneratedSheets;
|
||||
using OtterGui;
|
||||
using OtterGui.Raii;
|
||||
using OtterGui.Widgets;
|
||||
using Penumbra.Api.Enums;
|
||||
using Penumbra.Collections;
|
||||
using Penumbra.GameData.Enums;
|
||||
using Penumbra.GameData.Structs;
|
||||
using Penumbra.Mods;
|
||||
using Penumbra.Mods.ItemSwap;
|
||||
|
||||
namespace Penumbra.UI.Classes;
|
||||
|
||||
public class ItemSwapWindow : IDisposable
|
||||
{
|
||||
private class ItemSelector : FilterComboCache< Item >
|
||||
{
|
||||
public ItemSelector()
|
||||
: base( Dalamud.GameData.GetExcelSheet< Item >()!.Where( i
|
||||
=> ( ( EquipSlot )i.EquipSlotCategory.Row ).IsEquipmentPiece() && i.ModelMain != 0 && i.Name.RawData.Length > 0 ) )
|
||||
{ }
|
||||
|
||||
protected override string ToString( Item obj )
|
||||
=> obj.Name.ToString();
|
||||
}
|
||||
|
||||
public ItemSwapWindow()
|
||||
{
|
||||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged += OnSettingChange;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Penumbra.CollectionManager.CollectionChanged += OnCollectionChange;
|
||||
Penumbra.CollectionManager.Current.ModSettingChanged -= OnSettingChange;
|
||||
}
|
||||
|
||||
private readonly ItemSelector _itemSelector = new();
|
||||
private readonly ItemSwapContainer _swapData = new();
|
||||
|
||||
private Mod? _mod;
|
||||
private ModSettings? _modSettings;
|
||||
private bool _dirty;
|
||||
|
||||
private SwapType _lastTab = SwapType.Equipment;
|
||||
private Gender _currentGender = Gender.Male;
|
||||
private ModelRace _currentRace = ModelRace.Midlander;
|
||||
private int _targetId = 0;
|
||||
private int _sourceId = 0;
|
||||
private int _currentVariant = 1;
|
||||
private Exception? _loadException = null;
|
||||
|
||||
private string _newModName = string.Empty;
|
||||
private string _newGroupName = "Swaps";
|
||||
private string _newOptionName = string.Empty;
|
||||
private bool _useFileSwaps = false;
|
||||
|
||||
|
||||
public void UpdateMod( Mod mod, ModSettings? settings )
|
||||
{
|
||||
if( mod == _mod && settings == _modSettings )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldDefaultName = $"{_mod?.Name.Text ?? "Unknown"} (Swapped)";
|
||||
if( _newModName.Length == 0 || oldDefaultName == _newModName )
|
||||
{
|
||||
_newModName = $"{mod.Name.Text} (Swapped)";
|
||||
}
|
||||
|
||||
_mod = mod;
|
||||
_modSettings = settings;
|
||||
_swapData.LoadMod( _mod, _modSettings );
|
||||
_dirty = true;
|
||||
}
|
||||
|
||||
private void UpdateState()
|
||||
{
|
||||
if( !_dirty )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_swapData.Clear();
|
||||
_loadException = null;
|
||||
if( _targetId > 0 && _sourceId > 0 )
|
||||
{
|
||||
try
|
||||
{
|
||||
switch( _lastTab )
|
||||
{
|
||||
case SwapType.Equipment: break;
|
||||
case SwapType.Accessory: break;
|
||||
case SwapType.Hair:
|
||||
|
||||
_swapData.LoadCustomization( BodySlot.Hair, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
|
||||
break;
|
||||
case SwapType.Face:
|
||||
_swapData.LoadCustomization( BodySlot.Face, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
|
||||
break;
|
||||
case SwapType.Ears:
|
||||
_swapData.LoadCustomization( BodySlot.Zear, Names.CombinedRace( _currentGender, ModelRace.Viera ), ( SetId )_sourceId, ( SetId )_targetId );
|
||||
break;
|
||||
case SwapType.Tail:
|
||||
_swapData.LoadCustomization( BodySlot.Tail, Names.CombinedRace( _currentGender, _currentRace ), ( SetId )_sourceId, ( SetId )_targetId );
|
||||
break;
|
||||
case SwapType.Weapon: break;
|
||||
case SwapType.Minion: break;
|
||||
case SwapType.Mount: break;
|
||||
}
|
||||
}
|
||||
catch( Exception e )
|
||||
{
|
||||
Penumbra.Log.Error( $"Could not get Customization Data container for {_lastTab}:\n{e}" );
|
||||
_loadException = e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string SwapToString( Swap swap )
|
||||
{
|
||||
return swap switch
|
||||
{
|
||||
MetaSwap meta => $"{meta.SwapFrom}: {meta.SwapFrom.EntryToString()} -> {meta.SwapApplied.EntryToString()}",
|
||||
FileSwap file => $"{file.Type}: {file.SwapFromRequestPath} -> {file.SwapToModded.FullName}{( file.DataWasChanged ? " (EDITED)" : string.Empty )}",
|
||||
_ => string.Empty,
|
||||
};
|
||||
}
|
||||
|
||||
private string CreateDescription()
|
||||
=> $"Created by swapping {_lastTab} {_sourceId} onto {_lastTab} {_targetId} for {_currentRace.ToName()} {_currentGender.ToName()}s in {_mod!.Name}.";
|
||||
|
||||
private void DrawHeaderLine( float width )
|
||||
{
|
||||
var newModAvailable = _loadException == null && _swapData.Loaded;
|
||||
|
||||
ImGui.SetNextItemWidth( width );
|
||||
if( ImGui.InputTextWithHint( "##newModName", "New Mod Name...", ref _newModName, 64 ) )
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
var tt = "Create a new mod of the given name containing only the swap.";
|
||||
if( ImGuiUtil.DrawDisabledButton( "Create New Mod", new Vector2( width / 2, 0 ), tt, !newModAvailable || _newModName.Length == 0 ) )
|
||||
{
|
||||
var newDir = Mod.CreateModFolder( Penumbra.ModManager.BasePath, _newModName );
|
||||
Mod.CreateMeta( newDir, _newModName, Penumbra.Config.DefaultModAuthor, CreateDescription(), "1.0", string.Empty );
|
||||
Mod.CreateDefaultFiles( newDir );
|
||||
Penumbra.ModManager.AddMod( newDir );
|
||||
if( !_swapData.WriteMod( Penumbra.ModManager.Last(), _useFileSwaps ? ItemSwapContainer.WriteType.UseSwaps : ItemSwapContainer.WriteType.NoSwaps ) )
|
||||
{
|
||||
Penumbra.ModManager.DeleteMod( Penumbra.ModManager.Count - 1 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 );
|
||||
if( ImGui.InputTextWithHint( "##groupName", "Group Name...", ref _newGroupName, 32 ) )
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
ImGui.SetNextItemWidth( ( width - ImGui.GetStyle().ItemSpacing.X ) / 2 );
|
||||
if( ImGui.InputTextWithHint( "##optionName", "New Option Name...", ref _newOptionName, 32 ) )
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
tt = "Create a new option inside this mod containing only the swap.";
|
||||
if( ImGuiUtil.DrawDisabledButton( "Create New Option (WIP)", new Vector2( width / 2, 0 ), tt,
|
||||
true || (!newModAvailable || _newGroupName.Length == 0 || _newOptionName.Length == 0 || _mod == null || _mod.AllSubMods.Any( m => m.Name == _newOptionName ) )) )
|
||||
{ }
|
||||
|
||||
ImGui.SameLine();
|
||||
var newPos = new Vector2( ImGui.GetCursorPosX() + 10 * ImGuiHelpers.GlobalScale, ImGui.GetCursorPosY() - ( ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y ) / 2 );
|
||||
ImGui.SetCursorPos( newPos );
|
||||
ImGui.Checkbox( "Use File Swaps", ref _useFileSwaps );
|
||||
ImGuiUtil.HoverTooltip( "Use File Swaps." );
|
||||
}
|
||||
|
||||
private enum SwapType
|
||||
{
|
||||
Equipment,
|
||||
Accessory,
|
||||
Hair,
|
||||
Face,
|
||||
Ears,
|
||||
Tail,
|
||||
Weapon,
|
||||
Minion,
|
||||
Mount,
|
||||
}
|
||||
|
||||
private void DrawSwapBar()
|
||||
{
|
||||
using var bar = ImRaii.TabBar( "##swapBar", ImGuiTabBarFlags.None );
|
||||
|
||||
DrawHairSwap();
|
||||
DrawFaceSwap();
|
||||
DrawEarSwap();
|
||||
DrawTailSwap();
|
||||
DrawArmorSwap();
|
||||
DrawAccessorySwap();
|
||||
DrawWeaponSwap();
|
||||
DrawMinionSwap();
|
||||
DrawMountSwap();
|
||||
}
|
||||
|
||||
private ImRaii.IEndObject DrawTab( SwapType newTab )
|
||||
{
|
||||
using var tab = ImRaii.TabItem( newTab.ToString() );
|
||||
if( tab )
|
||||
{
|
||||
_dirty = _lastTab != newTab;
|
||||
_lastTab = newTab;
|
||||
}
|
||||
|
||||
UpdateState();
|
||||
|
||||
return tab;
|
||||
}
|
||||
|
||||
private void DrawArmorSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Equipment );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawAccessorySwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Accessory );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawHairSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Hair );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Hairstyle" );
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
||||
private void DrawFaceSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Face );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Face Type" );
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput();
|
||||
}
|
||||
|
||||
private void DrawTailSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Tail );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Tail Type" );
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput("for all", 2);
|
||||
}
|
||||
|
||||
|
||||
private void DrawEarSwap()
|
||||
{
|
||||
using var tab = DrawTab( SwapType.Ears );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var table = ImRaii.Table( "##settings", 2, ImGuiTableFlags.SizingFixedFit );
|
||||
DrawTargetIdInput( "Take this Ear Type" );
|
||||
DrawSourceIdInput();
|
||||
DrawGenderInput( "for all Viera", 0 );
|
||||
}
|
||||
|
||||
|
||||
private void DrawWeaponSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Weapon );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMinionSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Minion );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void DrawMountSwap()
|
||||
{
|
||||
using var disabled = ImRaii.Disabled();
|
||||
using var tab = DrawTab( SwapType.Mount );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private const float InputWidth = 120;
|
||||
|
||||
private void DrawTargetIdInput( string text = "Take this ID" )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputInt( "##targetId", ref _targetId, 0, 0 ) )
|
||||
_targetId = Math.Clamp( _targetId, 0, byte.MaxValue );
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawSourceIdInput( string text = "and put it on this one" )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
|
||||
if (ImGui.InputInt( "##sourceId", ref _sourceId, 0, 0 ))
|
||||
_sourceId = Math.Clamp( _sourceId, 0, byte.MaxValue );
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawVariantInput( string text )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.SetNextItemWidth( InputWidth * ImGuiHelpers.GlobalScale );
|
||||
if( ImGui.InputInt( "##variantId", ref _currentVariant, 0, 0 ) )
|
||||
_currentVariant = Math.Clamp( _currentVariant, 0, byte.MaxValue );
|
||||
_dirty |= ImGui.IsItemDeactivatedAfterEdit();
|
||||
}
|
||||
|
||||
private void DrawGenderInput( string text = "for all", int drawRace = 1 )
|
||||
{
|
||||
ImGui.TableNextColumn();
|
||||
ImGui.AlignTextToFramePadding();
|
||||
ImGui.TextUnformatted( text );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
_dirty |= Combos.Gender( "##Gender", InputWidth, _currentGender, out _currentGender );
|
||||
if( drawRace == 1 )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
_dirty |= Combos.Race( "##Race", InputWidth, _currentRace, out _currentRace );
|
||||
}
|
||||
else if( drawRace == 2 )
|
||||
{
|
||||
ImGui.SameLine();
|
||||
if( _currentRace is not ModelRace.Miqote and not ModelRace.AuRa and not ModelRace.Hrothgar )
|
||||
{
|
||||
_currentRace = ModelRace.Miqote;
|
||||
}
|
||||
|
||||
_dirty |= ImGuiUtil.GenericEnumCombo( "##Race", InputWidth, _currentRace, out _currentRace, new[] { ModelRace.Miqote, ModelRace.AuRa, ModelRace.Hrothgar },
|
||||
RaceEnumExtensions.ToName );
|
||||
}
|
||||
}
|
||||
|
||||
private string NonExistentText()
|
||||
=> _lastTab switch
|
||||
{
|
||||
SwapType.Equipment => "One of the selected pieces of equipment does not seem to exist.",
|
||||
SwapType.Accessory => "One of the selected accessories does not seem to exist.",
|
||||
SwapType.Hair => "One of the selected hairstyles does not seem to exist for this gender and race combo.",
|
||||
SwapType.Face => "One of the selected faces does not seem to exist for this gender and race combo.",
|
||||
SwapType.Ears => "One of the selected ear types does not seem to exist for this gender and race combo.",
|
||||
SwapType.Tail => "One of the selected tails does not seem to exist for this gender and race combo.",
|
||||
SwapType.Weapon => "One of the selected weapons does not seem to exist.",
|
||||
SwapType.Minion => "One of the selected minions does not seem to exist.",
|
||||
SwapType.Mount => "One of the selected mounts does not seem to exist.",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
|
||||
public void DrawItemSwapPanel()
|
||||
{
|
||||
using var tab = ImRaii.TabItem( "Item Swap (WIP)" );
|
||||
if( !tab )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui.NewLine();
|
||||
DrawHeaderLine( 300 * ImGuiHelpers.GlobalScale );
|
||||
ImGui.NewLine();
|
||||
|
||||
DrawSwapBar();
|
||||
|
||||
using var table = ImRaii.ListBox( "##swaps", -Vector2.One );
|
||||
if( _loadException != null )
|
||||
{
|
||||
ImGuiUtil.TextWrapped( $"Could not load Customization Swap:\n{_loadException}" );
|
||||
}
|
||||
else if( _swapData.Loaded )
|
||||
{
|
||||
foreach( var swap in _swapData.Swaps )
|
||||
{
|
||||
DrawSwap( swap );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ImGui.TextUnformatted( NonExistentText() );
|
||||
}
|
||||
}
|
||||
|
||||
private static void DrawSwap( Swap swap )
|
||||
{
|
||||
var flags = swap.ChildSwaps.Count == 0 ? ImGuiTreeNodeFlags.Bullet | ImGuiTreeNodeFlags.Leaf : ImGuiTreeNodeFlags.DefaultOpen;
|
||||
using var tree = ImRaii.TreeNode( SwapToString( swap ), flags );
|
||||
if( !tree )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach( var child in swap.ChildSwaps )
|
||||
{
|
||||
DrawSwap( child );
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCollectionChange( CollectionType collectionType, ModCollection? oldCollection,
|
||||
ModCollection? newCollection, string _ )
|
||||
{
|
||||
if( collectionType != CollectionType.Current || _mod == null || newCollection == null )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateMod( _mod, newCollection.Settings[ _mod.Index ] );
|
||||
newCollection.ModSettingChanged += OnSettingChange;
|
||||
if( oldCollection != null )
|
||||
{
|
||||
oldCollection.ModSettingChanged -= OnSettingChange;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingChange( ModSettingChange type, int modIdx, int oldValue, int groupIdx, bool inherited )
|
||||
{
|
||||
if( modIdx == _mod?.Index )
|
||||
{
|
||||
_swapData.LoadMod( _mod, _modSettings );
|
||||
_dirty = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,11 +51,6 @@ public partial class ModEditWindow
|
|||
public void Draw()
|
||||
{
|
||||
_list = _getFiles();
|
||||
if( _list.Count == 0 )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using var tab = ImRaii.TabItem( _tabName );
|
||||
if( !tab )
|
||||
{
|
||||
|
|
|
|||
|
|
@ -147,6 +147,17 @@ public partial class ModEditWindow
|
|||
return false;
|
||||
}
|
||||
|
||||
using( var textures = ImRaii.TreeNode( "Textures", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( textures )
|
||||
{
|
||||
foreach( var tex in file.Textures )
|
||||
{
|
||||
ImRaii.TreeNode( $"{tex.Path} - {tex.Flags:X4}", ImGuiTreeNodeFlags.Leaf ).Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
using( var sets = ImRaii.TreeNode( "UV Sets", ImGuiTreeNodeFlags.DefaultOpen ) )
|
||||
{
|
||||
if( sets )
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( EqpEquipSlotCombo( "##eqpSlot", 100, _new.Slot, out var slot ) )
|
||||
if( Combos.EqpEquipSlot( "##eqpSlot", 100, _new.Slot, out var slot ) )
|
||||
{
|
||||
_new = new EqpManipulation( ExpandedEqpFile.GetDefault( setId ), slot, _new.SetId );
|
||||
}
|
||||
|
|
@ -241,7 +241,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( RaceCombo( "##eqdpRace", _new.Race, out var race ) )
|
||||
if( Combos.Race( "##eqdpRace", _new.Race, out var race ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, race ), _new.Slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, _new.Gender, race, _new.SetId );
|
||||
|
|
@ -250,7 +250,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( GenderCombo( "##eqdpGender", _new.Gender, out var gender ) )
|
||||
if( Combos.Gender( "##eqdpGender", _new.Gender, out var gender ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( gender, _new.Race ), _new.Slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, _new.Slot, gender, _new.Race, _new.SetId );
|
||||
|
|
@ -259,7 +259,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( EqdpEquipSlotCombo( "##eqdpSlot", _new.Slot, out var slot ) )
|
||||
if( Combos.EqdpEquipSlot( "##eqdpSlot", _new.Slot, out var slot ) )
|
||||
{
|
||||
var newDefaultEntry = ExpandedEqdpFile.GetDefault( Names.CombinedRace( _new.Gender, _new.Race ), slot.IsAccessory(), _new.SetId );
|
||||
_new = new EqdpManipulation( newDefaultEntry, slot, _new.Gender, _new.Race, _new.SetId );
|
||||
|
|
@ -356,7 +356,7 @@ public partial class ModEditWindow
|
|||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( ImcTypeCombo( "##imcType", _new.ObjectType, out var type ) )
|
||||
if( Combos.ImcType( "##imcType", _new.ObjectType, out var type ) )
|
||||
{
|
||||
var equipSlot = type switch
|
||||
{
|
||||
|
|
@ -386,7 +386,7 @@ public partial class ModEditWindow
|
|||
// Equipment and accessories are slightly different imcs than other types.
|
||||
if( _new.ObjectType is ObjectType.Equipment )
|
||||
{
|
||||
if( EqpEquipSlotCombo( "##imcSlot", 100, _new.EquipSlot, out var slot ) )
|
||||
if( Combos.EqpEquipSlot( "##imcSlot", 100, _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
|
|
@ -396,7 +396,7 @@ public partial class ModEditWindow
|
|||
}
|
||||
else if( _new.ObjectType is ObjectType.Accessory )
|
||||
{
|
||||
if( AccessorySlotCombo( "##imcSlot", _new.EquipSlot, out var slot ) )
|
||||
if( Combos.AccessorySlot( "##imcSlot", _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
|
|
@ -425,7 +425,7 @@ public partial class ModEditWindow
|
|||
ImGui.TableNextColumn();
|
||||
if( _new.ObjectType is ObjectType.DemiHuman )
|
||||
{
|
||||
if( EqpEquipSlotCombo( "##imcSlot", 70, _new.EquipSlot, out var slot ) )
|
||||
if( Combos.EqpEquipSlot( "##imcSlot", 70, _new.EquipSlot, out var slot ) )
|
||||
{
|
||||
_new = new ImcManipulation( _new.ObjectType, _new.BodySlot, _new.PrimaryId, _new.SecondaryId, _new.Variant, slot, _new.Entry ).Copy( GetDefault( _new )
|
||||
?? new ImcEntry() );
|
||||
|
|
@ -599,7 +599,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( ModelSetIdTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( RaceCombo( "##estRace", _new.Race, out var race ) )
|
||||
if( Combos.Race( "##estRace", _new.Race, out var race ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( _new.Gender, race ), _new.SetId );
|
||||
_new = new EstManipulation( _new.Gender, race, _new.Slot, _new.SetId, newDefaultEntry );
|
||||
|
|
@ -608,7 +608,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( ModelRaceTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( GenderCombo( "##estGender", _new.Gender, out var gender ) )
|
||||
if( Combos.Gender( "##estGender", _new.Gender, out var gender ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( _new.Slot, Names.CombinedRace( gender, _new.Race ), _new.SetId );
|
||||
_new = new EstManipulation( gender, _new.Race, _new.Slot, _new.SetId, newDefaultEntry );
|
||||
|
|
@ -617,7 +617,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( GenderTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( EstSlotCombo( "##estSlot", _new.Slot, out var slot ) )
|
||||
if( Combos.EstSlot( "##estSlot", _new.Slot, out var slot ) )
|
||||
{
|
||||
var newDefaultEntry = EstFile.GetDefault( slot, Names.CombinedRace( _new.Gender, _new.Race ), _new.SetId );
|
||||
_new = new EstManipulation( _new.Gender, _new.Race, slot, _new.SetId, newDefaultEntry );
|
||||
|
|
@ -805,7 +805,7 @@ public partial class ModEditWindow
|
|||
|
||||
// Identifier
|
||||
ImGui.TableNextColumn();
|
||||
if( SubRaceCombo( "##rspSubRace", _new.SubRace, out var subRace ) )
|
||||
if( Combos.SubRace( "##rspSubRace", _new.SubRace, out var subRace ) )
|
||||
{
|
||||
_new = new RspManipulation( subRace, _new.Attribute, CmpFile.GetDefault( subRace, _new.Attribute ) );
|
||||
}
|
||||
|
|
@ -813,7 +813,7 @@ public partial class ModEditWindow
|
|||
ImGuiUtil.HoverTooltip( RacialTribeTooltip );
|
||||
|
||||
ImGui.TableNextColumn();
|
||||
if( RspAttributeCombo( "##rspAttribute", _new.Attribute, out var attribute ) )
|
||||
if( Combos.RspAttribute( "##rspAttribute", _new.Attribute, out var attribute ) )
|
||||
{
|
||||
_new = new RspManipulation( _new.SubRace, attribute, CmpFile.GetDefault( subRace, attribute ) );
|
||||
}
|
||||
|
|
@ -858,36 +858,6 @@ public partial class ModEditWindow
|
|||
}
|
||||
}
|
||||
|
||||
// Different combos to use with enums.
|
||||
private static bool RaceCombo( string label, ModelRace current, out ModelRace race )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out race, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
private static bool GenderCombo( string label, Gender current, out Gender gender )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 120 * ImGuiHelpers.GlobalScale, current, out gender, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
private static bool EqdpEquipSlotCombo( string label, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EqdpSlots, EquipSlotExtensions.ToName );
|
||||
|
||||
private static bool EqpEquipSlotCombo( string label, float width, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, width * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.EquipmentSlots, EquipSlotExtensions.ToName );
|
||||
|
||||
private static bool AccessorySlotCombo( string label, EquipSlot current, out EquipSlot slot )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 100 * ImGuiHelpers.GlobalScale, current, out slot, EquipSlotExtensions.AccessorySlots, EquipSlotExtensions.ToName );
|
||||
|
||||
private static bool SubRaceCombo( string label, SubRace current, out SubRace subRace )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 150 * ImGuiHelpers.GlobalScale, current, out subRace, RaceEnumExtensions.ToName, 1 );
|
||||
|
||||
private static bool RspAttributeCombo( string label, RspAttribute current, out RspAttribute attribute )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute,
|
||||
RspAttributeExtensions.ToFullString, 0, 1 );
|
||||
|
||||
private static bool EstSlotCombo( string label, EstManipulation.EstType current, out EstManipulation.EstType attribute )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 200 * ImGuiHelpers.GlobalScale, current, out attribute );
|
||||
|
||||
private static bool ImcTypeCombo( string label, ObjectType current, out ObjectType type )
|
||||
=> ImGuiUtil.GenericEnumCombo( label, 110 * ImGuiHelpers.GlobalScale, current, out type, ObjectTypeExtensions.ValidImcTypes,
|
||||
ObjectTypeExtensions.ToName );
|
||||
|
||||
// A number input for ids with a optional max id of given width.
|
||||
// Returns true if newId changed against currentId.
|
||||
private static bool IdInput( string label, float width, ushort currentId, out ushort newId, int minId, int maxId, bool border )
|
||||
|
|
|
|||
|
|
@ -20,11 +20,13 @@ namespace Penumbra.UI.Classes;
|
|||
|
||||
public partial class ModEditWindow : Window, IDisposable
|
||||
{
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
private Editor? _editor;
|
||||
private Mod? _mod;
|
||||
private Vector2 _iconSize = Vector2.Zero;
|
||||
private bool _allowReduplicate = false;
|
||||
private const string WindowBaseLabel = "###SubModEdit";
|
||||
internal readonly ItemSwapWindow _swapWindow = new();
|
||||
|
||||
private Editor? _editor;
|
||||
private Mod? _mod;
|
||||
private Vector2 _iconSize = Vector2.Zero;
|
||||
private bool _allowReduplicate = false;
|
||||
|
||||
public void ChangeMod( Mod mod )
|
||||
{
|
||||
|
|
@ -45,6 +47,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_selectedFiles.Clear();
|
||||
_modelTab.Reset();
|
||||
_materialTab.Reset();
|
||||
_swapWindow.UpdateMod( mod, Penumbra.CollectionManager.Current[ mod.Index ].Settings );
|
||||
}
|
||||
|
||||
public void ChangeOption( ISubMod? subMod )
|
||||
|
|
@ -148,6 +151,7 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_modelTab.Draw();
|
||||
_materialTab.Draw();
|
||||
DrawTextureTab();
|
||||
_swapWindow.DrawItemSwapPanel();
|
||||
}
|
||||
|
||||
// A row of three buttonSizes and a help marker that can be used for material suffix changing.
|
||||
|
|
@ -544,5 +548,6 @@ public partial class ModEditWindow : Window, IDisposable
|
|||
_left.Dispose();
|
||||
_right.Dispose();
|
||||
_center.Dispose();
|
||||
_swapWindow.Dispose();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue