This commit is contained in:
Ottermandias 2023-06-16 16:13:26 +02:00
parent 7463aafa13
commit 27f151c55a
32 changed files with 1744 additions and 151 deletions

549
Glamourer/Designs/Design.cs Normal file
View file

@ -0,0 +1,549 @@
using System;
using System.IO;
using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class Design : ISavable
{
internal Design(ItemManager items)
{ }
// Metadata
public const int FileVersion = 1;
public Guid Identifier { get; internal init; }
public DateTimeOffset CreationDate { get; internal init; }
public DateTimeOffset LastEdit { get; internal set; }
public LowerString Name { get; internal set; } = LowerString.Empty;
public string Description { get; internal set; } = string.Empty;
public string[] Tags { get; internal set; } = Array.Empty<string>();
public int Index { get; internal set; }
internal DesignData DesignData;
#region Application Data
[Flags]
private enum DesignFlags : byte
{
ApplyHatVisible = 0x01,
ApplyVisorState = 0x02,
ApplyWeaponVisible = 0x04,
WriteProtected = 0x08,
}
private CustomizeFlag _applyCustomize;
private EquipFlag _applyEquip;
private DesignFlags _designFlags;
public bool DoApplyHatVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyHatVisible);
public bool DoApplyVisorToggle()
=> _designFlags.HasFlag(DesignFlags.ApplyVisorState);
public bool DoApplyWeaponVisible()
=> _designFlags.HasFlag(DesignFlags.ApplyWeaponVisible);
public bool WriteProtected()
=> _designFlags.HasFlag(DesignFlags.WriteProtected);
public bool SetApplyHatVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyHatVisible : _designFlags & ~DesignFlags.ApplyHatVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyVisorToggle(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyVisorState : _designFlags & ~DesignFlags.ApplyVisorState;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetApplyWeaponVisible(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.ApplyWeaponVisible : _designFlags & ~DesignFlags.ApplyWeaponVisible;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool SetWriteProtected(bool value)
{
var newFlag = value ? _designFlags | DesignFlags.WriteProtected : _designFlags & ~DesignFlags.WriteProtected;
if (newFlag == _designFlags)
return false;
_designFlags = newFlag;
return true;
}
public bool DoApplyEquip(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToFlag());
public bool DoApplyStain(EquipSlot slot)
=> _applyEquip.HasFlag(slot.ToStainFlag());
public bool DoApplyCustomize(CustomizeIndex idx)
=> _applyCustomize.HasFlag(idx.ToFlag());
internal bool SetApplyEquip(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToFlag() : _applyEquip & ~slot.ToFlag();
if (newValue == _applyEquip)
return false;
_applyEquip = newValue;
return true;
}
internal bool SetApplyStain(EquipSlot slot, bool value)
{
var newValue = value ? _applyEquip | slot.ToStainFlag() : _applyEquip & ~slot.ToStainFlag();
if (newValue == _applyEquip)
return false;
_applyEquip = newValue;
return true;
}
internal bool SetApplyCustomize(CustomizeIndex idx, bool value)
{
var newValue = value ? _applyCustomize | idx.ToFlag() : _applyCustomize & ~idx.ToFlag();
if (newValue == _applyCustomize)
return false;
_applyCustomize = newValue;
return true;
}
#endregion
#region ISavable
public JObject JsonSerialize()
{
var ret = new JObject
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
["CreationDate"] = CreationDate,
["LastEdit"] = LastEdit,
["Name"] = Name.Text,
["Description"] = Description,
["Tags"] = JArray.FromObject(Tags),
["WriteProtected"] = WriteProtected(),
["Equipment"] = SerializeEquipment(),
["Customize"] = SerializeCustomize(),
};
return ret;
}
public JObject SerializeEquipment()
{
static JObject Serialize(uint itemId, StainId stain, bool apply, bool applyStain)
=> new()
{
["ItemId"] = itemId,
["Stain"] = stain.Value,
["Apply"] = apply,
["ApplyStain"] = applyStain,
};
var ret = new JObject();
foreach (var slot in EquipSlotExtensions.EqdpSlots.Prepend(EquipSlot.OffHand).Prepend(EquipSlot.MainHand))
{
var item = DesignData.Item(slot);
var stain = DesignData.Stain(slot);
ret[slot.ToString()] = Serialize(item.Id, stain, DoApplyEquip(slot), DoApplyStain(slot));
}
ret["Hat"] = new QuadBool(DesignData.IsHatVisible(), DoApplyHatVisible()).ToJObject("Show", "Apply");
ret["Visor"] = new QuadBool(DesignData.IsVisorToggled(), DoApplyVisorToggle()).ToJObject("IsToggled", "Apply");
ret["Weapon"] = new QuadBool(DesignData.IsWeaponVisible(), DoApplyWeaponVisible()).ToJObject("Show", "Apply");
return ret;
}
public JObject SerializeCustomize()
{
var ret = new JObject()
{
["ModelId"] = DesignData.ModelId,
};
var customize = DesignData.Customize;
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
ret[idx.ToString()] = new JObject()
{
["Value"] = customize[idx].Value,
["Apply"] = DoApplyCustomize(idx),
};
}
ret["IsWet"] = DesignData.IsWet();
return ret;
}
public static Design LoadDesign(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
{
1 => LoadDesignV1(customizeManager, items, json, out changes),
_ => throw new Exception("The design to be loaded has no valid Version."),
};
}
private static Design LoadDesignV1(CustomizationManager customizeManager, ItemManager items, JObject json, out bool changes)
{
static string[] ParseTags(JObject json)
{
var tags = json["Tags"]?.ToObject<string[]>() ?? Array.Empty<string>();
return tags.OrderBy(t => t).Distinct().ToArray();
}
var creationDate = json["CreationDate"]?.ToObject<DateTimeOffset>() ?? throw new ArgumentNullException("CreationDate");
var design = new Design(items)
{
CreationDate = creationDate,
Identifier = json["Identifier"]?.ToObject<Guid>() ?? throw new ArgumentNullException("Identifier"),
Name = new LowerString(json["Name"]?.ToObject<string>() ?? throw new ArgumentNullException("Name")),
Description = json["Description"]?.ToObject<string>() ?? string.Empty,
Tags = ParseTags(json),
LastEdit = json["LastEdit"]?.ToObject<DateTimeOffset>() ?? creationDate,
};
changes = LoadEquip(items, json["Equipment"], design);
changes |= LoadCustomize(customizeManager, json["Customize"], design);
return design;
}
private static bool ValidateItem(ItemManager items, EquipSlot slot, uint itemId, out EquipItem item)
{
item = items.Resolve(slot, itemId);
if (item.Valid)
return true;
Glamourer.Chat.NotificationMessage($"The {slot.ToName()} item {itemId} does not exist, reset to Nothing.", "Warning",
NotificationType.Warning);
item = ItemManager.NothingItem(slot);
return false;
}
private static bool ValidateStain(ItemManager items, StainId stain, out StainId ret)
{
if (stain.Value != 0 && !items.Stains.ContainsKey(stain))
{
ret = 0;
Glamourer.Chat.NotificationMessage($"The Stain {stain} does not exist, reset to unstained.");
return false;
}
ret = stain;
return true;
}
private static bool ValidateWeapons(ItemManager items, uint mainId, uint offId, out EquipItem main, out EquipItem off)
{
var ret = true;
main = items.Resolve(EquipSlot.MainHand, mainId);
if (!main.Valid)
{
Glamourer.Chat.NotificationMessage($"The mainhand weapon {mainId} does not exist, reset to default sword.", "Warning",
NotificationType.Warning);
main = items.DefaultSword;
ret = false;
}
off = items.Resolve(main.Type.Offhand(), offId);
if (off.Valid)
return ret;
ret = false;
off = items.Resolve(main.Type.Offhand(), mainId);
if (off.Valid)
{
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to implied offhand.", "Warning",
NotificationType.Warning);
}
else
{
off = ItemManager.NothingItem(FullEquipType.Shield);
if (main.Type.Offhand() == FullEquipType.Shield)
{
Glamourer.Chat.NotificationMessage($"The offhand weapon {offId} does not exist, reset to no offhand.", "Warning",
NotificationType.Warning);
}
else
{
main = items.DefaultSword;
Glamourer.Chat.NotificationMessage(
$"The offhand weapon {offId} does not exist, but no default could be restored, reset mainhand to default sword and offhand to nothing.",
"Warning",
NotificationType.Warning);
}
}
return ret;
}
private static bool LoadEquip(ItemManager items, JToken? equip, Design design)
{
if (equip == null)
return true;
static (uint, StainId, bool, bool) ParseItem(EquipSlot slot, JToken? item)
{
var id = item?["ItemId"]?.ToObject<uint>() ?? ItemManager.NothingId(slot);
var stain = (StainId)(item?["Stain"]?.ToObject<byte>() ?? 0);
var apply = item?["Apply"]?.ToObject<bool>() ?? false;
var applyStain = item?["ApplyStain"]?.ToObject<bool>() ?? false;
return (id, stain, apply, applyStain);
}
var changes = false;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (id, stain, apply, applyStain) = ParseItem(slot, equip[slot.ToString()]);
changes |= !ValidateItem(items, slot, id, out var item);
changes |= !ValidateStain(items, stain, out stain);
design.DesignData.SetItem(item);
design.DesignData.SetStain(slot, stain);
design.SetApplyEquip(slot, apply);
design.SetApplyStain(slot, applyStain);
}
{
var (id, stain, apply, applyStain) = ParseItem(EquipSlot.MainHand, equip[EquipSlot.MainHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.MainHand))
id = items.DefaultSword.Id;
var (idOff, stainOff, applyOff, applyStainOff) = ParseItem(EquipSlot.OffHand, equip[EquipSlot.OffHand.ToString()]);
if (id == ItemManager.NothingId(EquipSlot.OffHand))
id = ItemManager.NothingId(FullEquipType.Shield);
changes |= ValidateWeapons(items, id, idOff, out var main, out var off);
changes |= ValidateStain(items, stain, out stain);
changes |= ValidateStain(items, stainOff, out stainOff);
design.DesignData.SetItem(main);
design.DesignData.SetItem(off);
design.DesignData.SetStain(EquipSlot.MainHand, stain);
design.DesignData.SetStain(EquipSlot.OffHand, stainOff);
design.SetApplyEquip(EquipSlot.MainHand, apply);
design.SetApplyEquip(EquipSlot.OffHand, applyOff);
design.SetApplyStain(EquipSlot.MainHand, applyStain);
design.SetApplyStain(EquipSlot.OffHand, applyStainOff);
}
var metaValue = QuadBool.FromJObject(equip["Hat"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyHatVisible(metaValue.Enabled);
design.DesignData.SetHatVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Weapon"], "Show", "Apply", QuadBool.NullFalse);
design.SetApplyWeaponVisible(metaValue.Enabled);
design.DesignData.SetWeaponVisible(metaValue.ForcedValue);
metaValue = QuadBool.FromJObject(equip["Visor"], "IsToggled", "Apply", QuadBool.NullFalse);
design.SetApplyVisorToggle(metaValue.Enabled);
design.DesignData.SetVisor(metaValue.ForcedValue);
return changes;
}
private static bool ValidateCustomize(CustomizationManager manager, ref Customize customize)
{
var ret = true;
if (!manager.Races.Contains(customize.Race))
{
ret = false;
if (manager.Clans.Contains(customize.Clan))
{
Glamourer.Chat.NotificationMessage(
$"The race {customize.Race.ToName()} is unknown, reset to {customize.Clan.ToRace().ToName()} from Clan.", "Warning",
NotificationType.Warning);
customize.Race = customize.Clan.ToRace();
}
else
{
Glamourer.Chat.NotificationMessage(
$"The race {customize.Race.ToName()} is unknown, reset to {Race.Hyur.ToName()} {SubRace.Midlander.ToName()}.", "Warning",
NotificationType.Warning);
customize.Race = Race.Hyur;
customize.Clan = SubRace.Midlander;
}
}
if (!manager.Clans.Contains(customize.Clan))
{
ret = false;
var oldClan = customize.Clan;
customize.Clan = (SubRace)((byte)customize.Race * 2 - 1);
if (manager.Clans.Contains(customize.Clan))
{
Glamourer.Chat.NotificationMessage($"The clan {oldClan.ToName()} is unknown, reset to {customize.Clan.ToName()} from race.",
"Warning", NotificationType.Warning);
}
else
{
customize.Race = Race.Hyur;
customize.Clan = SubRace.Midlander;
Glamourer.Chat.NotificationMessage(
$"The clan {oldClan.ToName()} is unknown, reset to {customize.Race.ToName()} {customize.Clan.ToName()}.", "Warning",
NotificationType.Warning);
}
}
if (!manager.Genders.Contains(customize.Gender))
{
ret = false;
Glamourer.Chat.NotificationMessage($"The gender {customize.Gender} is unknown, reset to {Gender.Male.ToName()}.", "Warning",
NotificationType.Warning);
customize.Gender = Gender.Male;
}
// TODO: Female Hrothgar
if (customize.Gender == Gender.Female && customize.Race == Race.Hrothgar)
{
ret = false;
Glamourer.Chat.NotificationMessage($"Hrothgar do not currently support female characters, reset to male.", "Warning",
NotificationType.Warning);
customize.Gender = Gender.Male;
}
var list = manager.GetList(customize.Clan, customize.Gender);
// Face is handled first automatically so it should not conflict with other customizations when corrupt.
foreach (var index in Enum.GetValues<CustomizeIndex>().Where(list.IsAvailable))
{
var value = customize.Get(index);
var count = list.Count(index, customize.Face);
var idx = list.DataByValue(index, value, out var data, customize.Face);
if (idx >= 0 && idx < count)
continue;
ret = false;
var name = list.Option(index);
var newValue = list.Data(index, 0, customize.Face);
Glamourer.Chat.NotificationMessage(
$"Customization {name} for {customize.Race.ToName()} {customize.Gender.ToName()}s does not support value {value.Value}, reset to {newValue.Value.Value}");
customize.Set(index, newValue.Value);
}
return ret;
}
private static bool ValidateModelId(ref uint modelId)
{
if (modelId != 0)
{
Glamourer.Chat.NotificationMessage($"Model IDs different from 0 are not currently allowed, reset {modelId} to 0.", "Warning",
NotificationType.Warning);
modelId = 0;
return false;
}
return true;
}
private static bool LoadCustomize(CustomizationManager manager, JToken? json, Design design)
{
if (json == null)
return true;
design.DesignData.ModelId = json["ModelId"]?.ToObject<uint>() ?? 0;
var ret = !ValidateModelId(ref design.DesignData.ModelId);
foreach (var idx in Enum.GetValues<CustomizeIndex>())
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design.DesignData.Customize[idx] = data;
design.SetApplyCustomize(idx, apply);
}
design.DesignData.SetIsWet(json["IsWet"]?.ToObject<bool>() ?? false);
ret |= !ValidateCustomize(manager, ref design.DesignData.Customize);
return ret;
}
//public void MigrateBase64(ItemManager items, string base64)
//{
// var data = DesignBase64Migration.MigrateBase64(items, base64, out var applyEquip, out var applyCustomize, out var writeProtected, out var wet,
// out var hat,
// out var visor, out var weapon);
// UpdateMainhand(items, data.MainHand);
// UpdateOffhand(items, data.OffHand);
// foreach (var slot in EquipSlotExtensions.EqdpSlots)
// UpdateArmor(items, slot, data.Armor(slot), true);
// ModelData.Customize = data.Customize;
// _applyEquip = applyEquip;
// _applyCustomize = applyCustomize;
// WriteProtected = writeProtected;
// Wetness = wet;
// Hat = hat;
// Visor = visor;
// Weapon = weapon;
//}
//
//public static Design CreateTemporaryFromBase64(ItemManager items, string base64, bool customize, bool equip)
//{
// var ret = new Design(items);
// ret.MigrateBase64(items, base64);
// if (!customize)
// ret._applyCustomize = 0;
// if (!equip)
// ret._applyEquip = 0;
// ret.Wetness = ret.Wetness.SetEnabled(customize);
// ret.Visor = ret.Visor.SetEnabled(equip);
// ret.Hat = ret.Hat.SetEnabled(equip);
// ret.Weapon = ret.Weapon.SetEnabled(equip);
// return ret;
//}
// Outdated.
//public string CreateOldBase64()
// => DesignBase64Migration.CreateOldBase64(in ModelData, _applyEquip, _applyCustomize, Wetness == QuadBool.True, Hat.ForcedValue,
// Hat.Enabled,
// Visor.ForcedValue, Visor.Enabled, Weapon.ForcedValue, Weapon.Enabled, WriteProtected, 1f);
public string ToFilename(FilenameService fileNames)
=> fileNames.DesignFile(this);
public void Save(StreamWriter writer)
{
using var j = new JsonTextWriter(writer)
{
Formatting = Formatting.Indented,
};
var obj = JsonSerialize();
obj.WriteTo(j);
}
public string LogName(string fileName)
=> Path.GetFileNameWithoutExtension(fileName);
#endregion
}

View file

@ -0,0 +1,150 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public static class DesignBase64Migration
{
public const int Base64Size = 91;
public static DesignData MigrateBase64(ItemManager items, string base64, out EquipFlag equipFlags, out CustomizeFlag customizeFlags,
out bool writeProtected, out bool applyHat, out bool applyVisor, out bool applyWeapon)
{
static void CheckSize(int length, int requiredLength)
{
if (length != requiredLength)
throw new Exception(
$"Can not parse Base64 string into CharacterSave:\n\tInvalid size {length} instead of {requiredLength}.");
}
byte applicationFlags;
ushort equipFlagsS;
var bytes = Convert.FromBase64String(base64);
applyHat = false;
applyVisor = false;
applyWeapon = false;
var data = new DesignData();
switch (bytes[0])
{
case 1:
{
CheckSize(bytes.Length, 86);
applicationFlags = bytes[1];
equipFlagsS = BitConverter.ToUInt16(bytes, 2);
break;
}
case 2:
{
CheckSize(bytes.Length, Base64Size);
applicationFlags = bytes[1];
equipFlagsS = BitConverter.ToUInt16(bytes, 2);
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
break;
}
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
customizeFlags = (applicationFlags & 0x01) != 0 ? CustomizeFlagExtensions.All : 0;
data.SetIsWet((applicationFlags & 0x02) != 0);
applyHat = (applicationFlags & 0x04) != 0;
applyWeapon = (applicationFlags & 0x08) != 0;
applyVisor = (applicationFlags & 0x10) != 0;
writeProtected = (applicationFlags & 0x20) != 0;
equipFlags = 0;
equipFlags |= (equipFlagsS & 0x0001) != 0 ? EquipFlag.Mainhand | EquipFlag.MainhandStain : 0;
equipFlags |= (equipFlagsS & 0x0002) != 0 ? EquipFlag.Offhand | EquipFlag.OffhandStain : 0;
var flag = 0x0002u;
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
flag <<= 1;
equipFlags |= (equipFlagsS & flag) != 0 ? slot.ToFlag() | slot.ToStainFlag() : 0;
}
unsafe
{
fixed (byte* ptr = bytes)
{
data.Customize.Load(*(Customize*)(ptr + 4));
var cur = (CharacterWeapon*)(ptr + 30);
var main = items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, (byte)cur[0].Variant);
if (!main.Valid)
throw new Exception($"Base64 string invalid, weapon could not be identified.");
data.SetItem(main);
data.SetStain(EquipSlot.MainHand, cur[0].Stain);
var off = items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, (byte)cur[1].Variant, main.Type);
if (!off.Valid)
throw new Exception($"Base64 string invalid, weapon could not be identified.");
data.SetItem(off);
data.SetStain(EquipSlot.OffHand, cur[1].Stain);
var eq = (CharacterArmor*)(ptr + 46);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
var item = items.Identify(slot, mdl.Set, mdl.Variant);
if (!item.Valid)
throw new Exception($"Base64 string invalid, item could not be identified.");
data.SetItem(item);
data.SetStain(slot, mdl.Stain);
}
}
}
return data;
}
public static unsafe string CreateOldBase64(in DesignData save, EquipFlag equipFlags, CustomizeFlag customizeFlags,
bool setHat, bool setVisor, bool setWeapon, bool writeProtected, float alpha = 1.0f)
{
var data = stackalloc byte[Base64Size];
data[0] = 2;
data[1] = (byte)((customizeFlags == CustomizeFlagExtensions.All ? 0x01 : 0)
| (save.IsWet() ? 0x02 : 0)
| (setHat ? 0x04 : 0)
| (setWeapon ? 0x08 : 0)
| (setVisor ? 0x10 : 0)
| (writeProtected ? 0x20 : 0));
data[2] = (byte)((equipFlags.HasFlag(EquipFlag.Mainhand) ? 0x01 : 0)
| (equipFlags.HasFlag(EquipFlag.Offhand) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.Head) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.Body) ? 0x08 : 0)
| (equipFlags.HasFlag(EquipFlag.Hands) ? 0x10 : 0)
| (equipFlags.HasFlag(EquipFlag.Legs) ? 0x20 : 0)
| (equipFlags.HasFlag(EquipFlag.Feet) ? 0x40 : 0)
| (equipFlags.HasFlag(EquipFlag.Ears) ? 0x80 : 0));
data[3] = (byte)((equipFlags.HasFlag(EquipFlag.Neck) ? 0x01 : 0)
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.Customize.Write((nint)data + 4);
((CharacterWeapon*)(data + 30))[0] = save.Item(EquipSlot.MainHand).Weapon(save.Stain(EquipSlot.MainHand));
((CharacterWeapon*)(data + 30))[1] = save.Item(EquipSlot.OffHand).Weapon(save.Stain(EquipSlot.OffHand));
((CharacterArmor*)(data + 46))[0] = save.Item(EquipSlot.Head).Armor(save.Stain(EquipSlot.Head));
((CharacterArmor*)(data + 46))[1] = save.Item(EquipSlot.Body).Armor(save.Stain(EquipSlot.Body));
((CharacterArmor*)(data + 46))[2] = save.Item(EquipSlot.Hands).Armor(save.Stain(EquipSlot.Hands));
((CharacterArmor*)(data + 46))[3] = save.Item(EquipSlot.Legs).Armor(save.Stain(EquipSlot.Legs));
((CharacterArmor*)(data + 46))[4] = save.Item(EquipSlot.Feet).Armor(save.Stain(EquipSlot.Feet));
((CharacterArmor*)(data + 46))[5] = save.Item(EquipSlot.Ears).Armor(save.Stain(EquipSlot.Ears));
((CharacterArmor*)(data + 46))[6] = save.Item(EquipSlot.Neck).Armor(save.Stain(EquipSlot.Neck));
((CharacterArmor*)(data + 46))[7] = save.Item(EquipSlot.Wrists).Armor(save.Stain(EquipSlot.Wrists));
((CharacterArmor*)(data + 46))[8] = save.Item(EquipSlot.RFinger).Armor(save.Stain(EquipSlot.RFinger));
((CharacterArmor*)(data + 46))[9] = save.Item(EquipSlot.LFinger).Armor(save.Stain(EquipSlot.LFinger));
*(float*)(data + 86) = 1f;
data[90] = (byte)((save.IsHatVisible() ? 0x01 : 0)
| (save.IsVisorToggled() ? 0x10 : 0)
| (save.IsWeaponVisible() ? 0x02 : 0));
return Convert.ToBase64String(new Span<byte>(data, Base64Size));
}
}

View file

@ -0,0 +1,179 @@
using System;
using System.Runtime.CompilerServices;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public unsafe struct DesignData
{
private string _nameHead = string.Empty;
private string _nameBody = string.Empty;
private string _nameHands = string.Empty;
private string _nameLegs = string.Empty;
private string _nameFeet = string.Empty;
private string _nameEars = string.Empty;
private string _nameNeck = string.Empty;
private string _nameWrists = string.Empty;
private string _nameRFinger = string.Empty;
private string _nameLFinger = string.Empty;
private string _nameMainhand = string.Empty;
private string _nameOffhand = string.Empty;
private fixed uint _itemIds[12];
private fixed ushort _iconIds[12];
private fixed byte _equipmentBytes[48];
public Customize Customize = Customize.Default;
public uint ModelId;
private WeaponType _secondaryMainhand;
private WeaponType _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public DesignData()
{}
public readonly StainId Stain(EquipSlot slot)
{
var index = slot.ToIndex();
return index > 11 ? (StainId)0 : _equipmentBytes[4 * index + 3];
}
public readonly EquipItem Item(EquipSlot slot)
=> slot.ToIndex() switch
{
// @formatter:off
0 => new EquipItem(_nameHead, _itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head ),
1 => new EquipItem(_nameBody, _itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body ),
2 => new EquipItem(_nameHands, _itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands ),
3 => new EquipItem(_nameLegs, _itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs ),
4 => new EquipItem(_nameFeet, _itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet ),
5 => new EquipItem(_nameEars, _itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears ),
6 => new EquipItem(_nameNeck, _itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck ),
7 => new EquipItem(_nameWrists, _itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists ),
8 => new EquipItem(_nameRFinger, _itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger ),
9 => new EquipItem(_nameLFinger, _itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger ),
10 => new EquipItem(_nameMainhand, _itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand ),
11 => new EquipItem(_nameOffhand, _itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand ),
_ => new EquipItem(),
// @formatter:on
};
public bool SetItem(EquipItem item)
{
var index = item.Type.ToSlot().ToIndex();
if (index > 11 || _itemIds[index] == item.Id)
return false;
_itemIds[index] = item.Id;
_iconIds[index] = item.IconId;
_equipmentBytes[4 * index + 0] = (byte)item.ModelId;
_equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Value >> 8);
_equipmentBytes[4 * index + 2] = item.Variant;
switch (index)
{
// @formatter:off
case 0: _nameHead = item.Name; return true;
case 1: _nameBody = item.Name; return true;
case 2: _nameHands = item.Name; return true;
case 3: _nameLegs = item.Name; return true;
case 4: _nameFeet = item.Name; return true;
case 5: _nameEars = item.Name; return true;
case 6: _nameNeck = item.Name; return true;
case 7: _nameWrists = item.Name; return true;
case 8: _nameRFinger = item.Name; return true;
case 9: _nameLFinger = item.Name; return true;
// @formatter:on
case 10:
_nameMainhand = item.Name;
_secondaryMainhand = item.WeaponType;
_typeMainhand = item.Type;
return true;
case 11:
_nameOffhand = item.Name;
_secondaryOffhand = item.WeaponType;
_typeOffhand = item.Type;
return true;
}
return true;
}
public bool SetStain(EquipSlot slot, StainId stain)
=> slot.ToIndex() switch
{
0 => SetIfDifferent(ref _equipmentBytes[3], stain.Value),
1 => SetIfDifferent(ref _equipmentBytes[7], stain.Value),
2 => SetIfDifferent(ref _equipmentBytes[11], stain.Value),
3 => SetIfDifferent(ref _equipmentBytes[15], stain.Value),
4 => SetIfDifferent(ref _equipmentBytes[19], stain.Value),
5 => SetIfDifferent(ref _equipmentBytes[23], stain.Value),
6 => SetIfDifferent(ref _equipmentBytes[27], stain.Value),
7 => SetIfDifferent(ref _equipmentBytes[31], stain.Value),
8 => SetIfDifferent(ref _equipmentBytes[35], stain.Value),
9 => SetIfDifferent(ref _equipmentBytes[39], stain.Value),
10 => SetIfDifferent(ref _equipmentBytes[43], stain.Value),
11 => SetIfDifferent(ref _equipmentBytes[47], stain.Value),
_ => false,
};
public readonly bool IsWet()
=> (_states & 0x01) == 0x01;
public bool SetIsWet(bool value)
{
if (value == IsWet())
return false;
_states = (byte)(value ? _states | 0x01 : _states & ~0x01);
return true;
}
public readonly bool IsVisorToggled()
=> (_states & 0x02) == 0x02;
public bool SetVisor(bool value)
{
if (value == IsVisorToggled())
return false;
_states = (byte)(value ? _states | 0x02 : _states & ~0x02);
return true;
}
public readonly bool IsHatVisible()
=> (_states & 0x04) == 0x04;
public bool SetHatVisible(bool value)
{
if (value == IsHatVisible())
return false;
_states = (byte)(value ? _states | 0x04 : _states & ~0x04);
return true;
}
public readonly bool IsWeaponVisible()
=> (_states & 0x08) == 0x09;
public bool SetWeaponVisible(bool value)
{
if (value == IsWeaponVisible())
return false;
_states = (byte)(value ? _states | 0x08 : _states & ~0x08);
return true;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
private static bool SetIfDifferent<T>(ref T old, T value) where T : IEquatable<T>
{
if (old.Equals(value))
return false;
old = value;
return true;
}
}

View file

@ -0,0 +1,46 @@
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public interface IDesign
{
public uint GetModelId();
public bool SetModelId(uint modelId);
public EquipItem GetEquipItem(EquipSlot slot);
public bool SetEquipItem(EquipItem item);
public StainId GetStain(EquipSlot slot);
public bool SetStain(EquipSlot slot, StainId stain);
public CustomizeValue GetCustomizeValue(CustomizeIndex type);
public bool SetCustomizeValue(CustomizeIndex type);
public bool DoApplyEquip(EquipSlot slot);
public bool DoApplyStain(EquipSlot slot);
public bool DoApplyCustomize(CustomizeIndex index);
public bool SetApplyEquip(EquipSlot slot, bool value);
public bool SetApplyStain(EquipSlot slot, bool value);
public bool SetApplyCustomize(CustomizeIndex slot, bool value);
public bool IsWet();
public bool SetIsWet(bool value);
public bool IsHatVisible();
public bool DoApplyHatVisible();
public bool SetHatVisible(bool value);
public bool SetApplyHatVisible(bool value);
public bool IsVisorToggled();
public bool DoApplyVisorToggle();
public bool SetVisorToggle(bool value);
public bool SetApplyVisorToggle(bool value);
public bool IsWeaponVisible();
public bool DoApplyWeaponVisible();
public bool SetWeaponVisible(bool value);
public bool SetApplyWeaponVisible(bool value);
}