Merge branch 'main' into Limiana/main

This commit is contained in:
Ottermandias 2023-12-31 13:33:24 +01:00
commit 5f28644b56
138 changed files with 3314 additions and 4006 deletions

View file

@ -1,11 +1,7 @@
using System.Buffers.Text;
using System.Collections.Generic;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;

View file

@ -0,0 +1,92 @@
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Events;
using Glamourer.Services;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Api;
public partial class GlamourerIpc
{
public enum GlamourerErrorCode
{
Success,
ActorNotFound,
ActorNotHuman,
ItemInvalid,
}
public const string LabelSetItem = "Glamourer.SetItem";
public const string LabelSetItemByActorName = "Glamourer.SetItemByActorName";
private readonly FuncProvider<Character?, byte, ulong, byte, uint, int> _setItemProvider;
private readonly FuncProvider<string, byte, ulong, byte, uint, int> _setItemByActorNameProvider;
public static FuncSubscriber<Character?, byte, ulong, byte, uint, int> SetItemSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItem);
public static FuncSubscriber<string, byte, ulong, byte, uint, int> SetItemByActorNameSubscriber(DalamudPluginInterface pi)
=> new(pi, LabelSetItemByActorName);
private GlamourerErrorCode SetItem(Character? character, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
var item = _items.Resolve(slot, itemId);
if (!item.Valid)
return GlamourerErrorCode.ItemInvalid;
var identifier = _actors.FromObject(character, false, false, false);
if (!identifier.IsValid)
return GlamourerErrorCode.ActorNotFound;
if (!_stateManager.TryGetValue(identifier, out var state))
{
_objects.Update();
var data = _objects[identifier];
if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state))
return GlamourerErrorCode.ActorNotFound;
}
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key);
return GlamourerErrorCode.Success;
}
private GlamourerErrorCode SetItemByActorName(string name, EquipSlot slot, CustomItemId itemId, StainId stainId, uint key)
{
if (itemId.Id == 0)
itemId = ItemManager.NothingId(slot);
var item = _items.Resolve(slot, itemId);
if (!item.Valid)
return GlamourerErrorCode.ItemInvalid;
var found = false;
_objects.Update();
foreach (var identifier in FindActorsRevert(name).Distinct())
{
if (!_stateManager.TryGetValue(identifier, out var state))
{
var data = _objects[identifier];
if (!data.Valid || !_stateManager.GetOrCreate(identifier, data.Objects[0], out state))
continue;
}
if (!state.ModelData.IsHuman)
return GlamourerErrorCode.ActorNotHuman;
_stateManager.ChangeEquip(state, slot, item, stainId, StateChanged.Source.Ipc, key);
found = true;
}
return found ? GlamourerErrorCode.Success : GlamourerErrorCode.ActorNotFound;
}
}

View file

@ -11,31 +11,34 @@ using Glamourer.Services;
using Glamourer.State;
using Penumbra.Api.Helpers;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.String;
namespace Glamourer.Api;
public partial class GlamourerIpc : IDisposable
public sealed partial class GlamourerIpc : IDisposable
{
public const int CurrentApiVersionMajor = 0;
public const int CurrentApiVersionMinor = 4;
private readonly StateManager _stateManager;
private readonly ObjectManager _objects;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private readonly DesignConverter _designConverter;
private readonly AutoDesignApplier _autoDesignApplier;
private readonly DesignManager _designManager;
private readonly ItemManager _items;
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorService actors,
public GlamourerIpc(DalamudPluginInterface pi, StateManager stateManager, ObjectManager objects, ActorManager actors,
DesignConverter designConverter, StateChanged stateChangedEvent, GPoseService gPose, AutoDesignApplier autoDesignApplier,
DesignManager designManager)
DesignManager designManager, ItemManager items)
{
_stateManager = stateManager;
_objects = objects;
_actors = actors;
_designConverter = designConverter;
_autoDesignApplier = autoDesignApplier;
_items = items;
_gPose = gPose;
_stateChangedEvent = stateChangedEvent;
_designManager = designManager;
@ -82,6 +85,11 @@ public partial class GlamourerIpc : IDisposable
_stateChangedProvider = new EventProvider<StateChanged.Type, nint, Lazy<string>>(pi, LabelStateChanged);
_gPoseChangedProvider = new EventProvider<bool>(pi, LabelGPoseChanged);
_setItemProvider = new FuncProvider<Character?, byte, ulong, byte, uint, int>(pi, LabelSetItem,
(idx, slot, item, stain, key) => (int)SetItem(idx, (EquipSlot)slot, item, stain, key));
_setItemByActorNameProvider = new FuncProvider<string, byte, ulong, byte, uint, int>(pi, LabelSetItemByActorName,
(name, slot, item, stain, key) => (int)SetItemByActorName(name, (EquipSlot)slot, item, stain, key));
_stateChangedEvent.Subscribe(OnStateChanged, StateChanged.Priority.GlamourerIpc);
_gPose.Subscribe(OnGPoseChanged, GPoseService.Priority.GlamourerIpc);
@ -126,6 +134,9 @@ public partial class GlamourerIpc : IDisposable
_gPoseChangedProvider.Dispose();
_getDesignListProvider.Dispose();
_setItemProvider.Dispose();
_setItemByActorNameProvider.Dispose();
}
private IEnumerable<ActorIdentifier> FindActors(string actorName)
@ -154,7 +165,7 @@ public partial class GlamourerIpc : IDisposable
private IEnumerable<ActorIdentifier> FindActors(Character? character)
{
var id = _actors.AwaitedService.FromObject(character, true, true, false);
var id = _actors.FromObject(character, true, true, false);
if (!id.IsValid)
yield break;

View file

@ -1,10 +1,10 @@
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.Structs;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
@ -74,7 +74,7 @@ public class AutoDesign
var ret = new JObject
{
["Gearset"] = GearsetIndex,
["JobGroup"] = Jobs.Id,
["JobGroup"] = Jobs.Id.Id,
};
return ret;

View file

@ -4,18 +4,16 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Unlocks;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -28,8 +26,8 @@ public class AutoDesignApplier : IDisposable
private readonly StateManager _state;
private readonly JobService _jobs;
private readonly EquippedGearset _equippedGearset;
private readonly ActorService _actors;
private readonly CustomizationService _customizations;
private readonly ActorManager _actors;
private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly ItemUnlockManager _itemUnlocks;
private readonly AutomationChanged _event;
@ -50,7 +48,7 @@ public class AutoDesignApplier : IDisposable
}
public AutoDesignApplier(Configuration config, AutoDesignManager manager, StateManager state, JobService jobs,
CustomizationService customizations, ActorService actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
CustomizeService customizations, ActorManager actors, ItemUnlockManager itemUnlocks, CustomizeUnlockManager customizeUnlocks,
AutomationChanged @event, ObjectManager objects, WeaponLoading weapons, HumanModelList humans, IClientState clientState,
EquippedGearset equippedGearset)
{
@ -87,7 +85,7 @@ public class AutoDesignApplier : IDisposable
if (_jobChangeState == null || !_config.EnableAutoDesigns)
return;
var id = actor.GetIdentifier(_actors.AwaitedService);
var id = actor.GetIdentifier(_actors);
if (id == _jobChangeState.Identifier)
{
var current = _jobChangeState.BaseData.Item(slot);
@ -161,7 +159,7 @@ public class AutoDesignApplier : IDisposable
{
foreach (var actor in data.Objects)
{
var specificId = actor.GetIdentifier(_actors.AwaitedService);
var specificId = actor.GetIdentifier(_actors);
if (_state.GetOrCreate(specificId, actor, out var state))
{
Reduce(actor, state, newSet, false, false);
@ -203,13 +201,13 @@ public class AutoDesignApplier : IDisposable
private void OnJobChange(Actor actor, Job oldJob, Job newJob)
{
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors.AwaitedService, out var id))
if (!_config.EnableAutoDesigns || !actor.Identifier(_actors, out var id))
return;
if (!GetPlayerSet(id, out var set))
{
if (_state.TryGetValue(id, out var s))
s.LastJob = (byte)newJob.Id;
s.LastJob = newJob.Id;
return;
}
@ -312,13 +310,13 @@ public class AutoDesignApplier : IDisposable
if (_manager.EnabledSets.TryGetValue(identifier, out set))
return true;
identifier = _actors.AwaitedService.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
identifier = _actors.CreatePlayer(identifier.PlayerName, ushort.MaxValue);
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Retainer:
case IdentifierType.Npc:
return _manager.EnabledSets.TryGetValue(identifier, out set);
case IdentifierType.Owned:
identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId);
return _manager.EnabledSets.TryGetValue(identifier, out set);
default:
set = null;
@ -470,7 +468,7 @@ public class AutoDesignApplier : IDisposable
totalCustomizeFlags |= CustomizeFlag.Face;
}
var set = _customizations.AwaitedService.GetList(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
var set = _customizations.Manager.GetSet(state.ModelData.Customize.Clan, state.ModelData.Customize.Gender);
var face = state.ModelData.Customize.Face;
foreach (var index in Enum.GetValues<CustomizeIndex>())
{
@ -479,7 +477,7 @@ public class AutoDesignApplier : IDisposable
continue;
var value = design.Customize[index];
if (CustomizationService.IsCustomizationValid(set, face, index, value, out var data))
if (CustomizeService.IsCustomizationValid(set, face, index, value, out var data))
{
if (data.HasValue && _config.UnlockedItemMode && !_customizeUnlocks.IsUnlocked(data.Value, out _))
continue;

View file

@ -10,13 +10,14 @@ using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Filesystem;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Automation;
@ -28,17 +29,17 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
private readonly JobService _jobs;
private readonly DesignManager _designs;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private readonly AutomationChanged _event;
private readonly DesignChanged _designEvent;
private readonly List<AutoDesignSet> _data = new();
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = new();
private readonly List<AutoDesignSet> _data = [];
private readonly Dictionary<ActorIdentifier, AutoDesignSet> _enabled = [];
public IReadOnlyDictionary<ActorIdentifier, AutoDesignSet> EnabledSets
=> _enabled;
public AutoDesignManager(JobService jobs, ActorService actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
public AutoDesignManager(JobService jobs, ActorManager actors, SaveService saveService, DesignManager designs, AutomationChanged @event,
FixedDesignMigrator migrator, DesignFileSystem fileSystem, DesignChanged designEvent)
{
_jobs = jobs;
@ -419,7 +420,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
continue;
}
var id = _actors.AwaitedService.FromJson(obj["Identifier"] as JObject);
var id = _actors.FromJson(obj["Identifier"] as JObject);
if (!IdentifierValid(id, out var group))
{
Glamourer.Messager.NotificationMessage("Skipped loading Automation Set: Invalid Identifier.", NotificationType.Warning);
@ -514,7 +515,7 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var jobs = conditions["JobGroup"]?.ToObject<int>() ?? -1;
if (jobs >= 0)
{
if (!_jobs.JobGroups.TryGetValue((ushort)jobs, out var jobGroup))
if (!_jobs.JobGroups.TryGetValue((JobGroupId)jobs, out var jobGroup))
{
Glamourer.Messager.NotificationMessage(
$"Error parsing automatically applied design for set {setName}: The job condition {jobs} does not exist.",
@ -562,9 +563,9 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
var name = manager.Data.ToName(identifier.Kind, identifier.DataId);
var table = identifier.Kind switch
{
ObjectKind.BattleNpc => manager.Data.BNpcs,
ObjectKind.BattleNpc => (IReadOnlyDictionary<NpcId, string>)manager.Data.BNpcs,
ObjectKind.EventNpc => manager.Data.ENpcs,
_ => new Dictionary<uint, string>(),
_ => new Dictionary<NpcId, string>(),
};
return table.Where(kvp => kvp.Value == name)
.Select(kvp => manager.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, identifier.HomeWorld.Id,
@ -580,12 +581,12 @@ public class AutoDesignManager : ISavable, IReadOnlyList<AutoDesignSet>, IDispos
},
IdentifierType.Retainer => new[]
{
_actors.AwaitedService.CreateRetainer(identifier.PlayerName,
_actors.CreateRetainer(identifier.PlayerName,
identifier.Retainer == ActorIdentifier.RetainerType.Mannequin
? ActorIdentifier.RetainerType.Mannequin
: ActorIdentifier.RetainerType.Bell).CreatePermanent(),
},
IdentifierType.Npc => CreateNpcs(_actors.AwaitedService, identifier),
IdentifierType.Npc => CreateNpcs(_actors, identifier),
_ => Array.Empty<ActorIdentifier>(),
};
}

View file

@ -3,59 +3,51 @@ using System.Linq;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Designs;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Automation;
public class FixedDesignMigrator
public class FixedDesignMigrator(JobService jobs)
{
private readonly JobService _jobs;
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
private List<(string Name, List<(string, JobGroup, bool)> Data)>? _migratedData;
public FixedDesignMigrator(JobService jobs)
=> _jobs = jobs;
public void ConsumeMigratedData(ActorService actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
public void ConsumeMigratedData(ActorManager actors, DesignFileSystem designFileSystem, AutoDesignManager autoManager)
{
if (_migratedData == null)
return;
foreach (var data in _migratedData)
foreach (var (name, data) in _migratedData)
{
var allEnabled = true;
var name = data.Name;
if (autoManager.Any(d => name == d.Name))
continue;
var id = ActorIdentifier.Invalid;
if (ByteString.FromString(data.Name, out var byteString, false))
if (ByteString.FromString(name, out var byteString))
{
id = actors.AwaitedService.CreatePlayer(byteString, ushort.MaxValue);
id = actors.CreatePlayer(byteString, ushort.MaxValue);
if (!id.IsValid)
id = actors.AwaitedService.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
id = actors.CreateRetainer(byteString, ActorIdentifier.RetainerType.Both);
}
if (!id.IsValid)
{
byteString = ByteString.FromSpanUnsafe("Mig Ration"u8, true, false, true);
id = actors.AwaitedService.CreatePlayer(byteString, actors.AwaitedService.Data.Worlds.First().Key);
id = actors.CreatePlayer(byteString, actors.Data.Worlds.First().Key);
if (!id.IsValid)
{
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {data.Name}.", NotificationType.Error);
allEnabled = false;
Glamourer.Messager.NotificationMessage($"Could not migrate fixed design {name}.", NotificationType.Error);
continue;
}
}
autoManager.AddDesignSet(name, id);
autoManager.SetState(autoManager.Count - 1, allEnabled);
autoManager.SetState(autoManager.Count - 1, true);
var set = autoManager[^1];
foreach (var design in data.Data.AsEnumerable().Reverse())
foreach (var design in data.AsEnumerable().Reverse())
{
if (!designFileSystem.Find(design.Item1, out var child) || child is not DesignFileSystem.Leaf leaf)
{
@ -96,7 +88,7 @@ public class FixedDesignMigrator
}
var job = obj["JobGroups"]?.ToObject<int>() ?? -1;
if (job < 0 || !_jobs.JobGroups.TryGetValue((ushort)job, out var group))
if (job < 0 || !jobs.JobGroups.TryGetValue((JobGroupId)job, out var group))
{
Glamourer.Messager.NotificationMessage("Could not semi-migrate fixed design: Invalid job group specified.",
NotificationType.Warning);

View file

@ -15,7 +15,7 @@ public sealed class Design : DesignBase, ISavable
{
#region Data
internal Design(CustomizationService customize, ItemManager items)
internal Design(CustomizeService customize, ItemManager items)
: base(customize, items)
{ }
@ -98,7 +98,7 @@ public sealed class Design : DesignBase, ISavable
#region Deserialization
public static Design LoadDesign(CustomizationService customizations, ItemManager items, JObject json)
public static Design LoadDesign(CustomizeService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
@ -108,7 +108,7 @@ public sealed class Design : DesignBase, ISavable
};
}
private static Design LoadDesignV1(CustomizationService customizations, ItemManager items, JObject json)
private static Design LoadDesignV1(CustomizeService customizations, ItemManager items, JObject json)
{
static string[] ParseTags(JObject json)
{

View file

@ -1,14 +1,13 @@
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.GameData.Data;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using System;
using System.Linq;
using Penumbra.GameData.DataContainers;
namespace Glamourer.Designs;
@ -26,35 +25,35 @@ public class DesignBase
public ref DesignData GetDesignDataRef()
=> ref _designData;
internal DesignBase(CustomizationService customize, ItemManager items)
internal DesignBase(CustomizeService customize, ItemManager items)
{
_designData.SetDefaultEquipment(items);
CustomizationSet = SetCustomizationSet(customize);
CustomizeSet = SetCustomizationSet(customize);
}
internal DesignBase(CustomizationService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags)
internal DesignBase(CustomizeService customize, in DesignData designData, EquipFlag equipFlags, CustomizeFlag customizeFlags)
{
_designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
ApplyEquip = equipFlags & EquipFlagExtensions.All;
_designFlags = 0;
CustomizationSet = SetCustomizationSet(customize);
_designData = designData;
ApplyCustomize = customizeFlags & CustomizeFlagExtensions.AllRelevant;
ApplyEquip = equipFlags & EquipFlagExtensions.All;
_designFlags = 0;
CustomizeSet = SetCustomizationSet(customize);
}
internal DesignBase(DesignBase clone)
{
_designData = clone._designData;
CustomizationSet = clone.CustomizationSet;
ApplyCustomize = clone.ApplyCustomizeRaw;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
_designFlags = clone._designFlags & (DesignFlags)0x0F;
_designData = clone._designData;
CustomizeSet = clone.CustomizeSet;
ApplyCustomize = clone.ApplyCustomizeRaw;
ApplyEquip = clone.ApplyEquip & EquipFlagExtensions.All;
_designFlags = clone._designFlags & (DesignFlags)0x0F;
}
/// <summary> Ensure that the customization set is updated when the design data changes. </summary>
internal void SetDesignData(CustomizationService customize, in DesignData other)
internal void SetDesignData(CustomizeService customize, in DesignData other)
{
_designData = other;
CustomizationSet = SetCustomizationSet(customize);
_designData = other;
CustomizeSet = SetCustomizationSet(customize);
}
#region Application Data
@ -69,15 +68,18 @@ public class DesignBase
WriteProtected = 0x10,
}
private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant;
public CustomizationSet CustomizationSet { get; private set; }
private CustomizeFlag _applyCustomize = CustomizeFlagExtensions.AllRelevant;
public CustomizeSet CustomizeSet { get; private set; }
internal CustomizeFlag ApplyCustomize
{
get => _applyCustomize.FixApplication(CustomizationSet);
set => _applyCustomize = value & CustomizeFlagExtensions.AllRelevant;
get => _applyCustomize.FixApplication(CustomizeSet);
set => _applyCustomize = (value & CustomizeFlagExtensions.AllRelevant) | CustomizeFlag.BodyType;
}
internal CustomizeFlag ApplyCustomizeExcludingBodyType
=> _applyCustomize.FixApplication(CustomizeSet) & ~CustomizeFlag.BodyType;
internal CustomizeFlag ApplyCustomizeRaw
=> _applyCustomize;
@ -85,13 +87,13 @@ public class DesignBase
internal CrestFlag ApplyCrest = CrestExtensions.AllRelevant;
private DesignFlags _designFlags = DesignFlags.ApplyHatVisible | DesignFlags.ApplyVisorState | DesignFlags.ApplyWeaponVisible;
public bool SetCustomize(CustomizationService customizationService, Customize customize)
public bool SetCustomize(CustomizeService customizeService, CustomizeArray customize)
{
if (customize.Equals(_designData.Customize))
return false;
_designData.Customize.Load(customize);
CustomizationSet = customizationService.AwaitedService.GetList(customize.Clan, customize.Gender);
_designData.Customize = customize;
CustomizeSet = customizeService.Manager.GetSet(customize.Clan, customize.Gender);
return true;
}
@ -241,10 +243,10 @@ public class DesignBase
}
}
private CustomizationSet SetCustomizationSet(CustomizationService customize)
private CustomizeSet SetCustomizationSet(CustomizeService customize)
=> !_designData.IsHuman
? customize.AwaitedService.GetList(SubRace.Midlander, Gender.Male)
: customize.AwaitedService.GetList(_designData.Customize.Clan, _designData.Customize.Gender);
? customize.Manager.GetSet(SubRace.Midlander, Gender.Male)
: customize.Manager.GetSet(_designData.Customize.Clan, _designData.Customize.Gender);
#endregion
@ -331,7 +333,7 @@ public class DesignBase
#region Deserialization
public static DesignBase LoadDesignBase(CustomizationService customizations, ItemManager items, JObject json)
public static DesignBase LoadDesignBase(CustomizeService customizations, ItemManager items, JObject json)
{
var version = json["FileVersion"]?.ToObject<int>() ?? 0;
return version switch
@ -341,7 +343,7 @@ public class DesignBase
};
}
private static DesignBase LoadDesignV1Base(CustomizationService customizations, ItemManager items, JObject json)
private static DesignBase LoadDesignV1Base(CustomizeService customizations, ItemManager items, JObject json)
{
var ret = new DesignBase(customizations, items);
LoadCustomize(customizations, json["Customize"], ret, "Temporary Design", false, true);
@ -436,14 +438,14 @@ public class DesignBase
design._designData.SetVisor(metaValue.ForcedValue);
}
protected static void LoadCustomize(CustomizationService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
protected static void LoadCustomize(CustomizeService customizations, JToken? json, DesignBase design, string name, bool forbidNonHuman,
bool allowUnknown)
{
if (json == null)
{
design._designData.ModelId = 0;
design._designData.IsHuman = true;
design.SetCustomize(customizations, Customize.Default);
design.SetCustomize(customizations, CustomizeArray.Default);
Glamourer.Messager.NotificationMessage("The loaded design does not contain any customization data, reset to default.",
NotificationType.Warning);
return;
@ -474,7 +476,7 @@ public class DesignBase
{
var arrayText = json["Array"]?.ToObject<string>() ?? string.Empty;
design._designData.Customize.LoadBase64(arrayText);
design.CustomizationSet = design.SetCustomizationSet(customizations);
design.CustomizeSet = design.SetCustomizationSet(customizations);
return;
}
@ -486,18 +488,18 @@ public class DesignBase
design._designData.Customize.Race = race;
design._designData.Customize.Clan = clan;
design._designData.Customize.Gender = gender;
design.CustomizationSet = design.SetCustomizationSet(customizations);
design.CustomizeSet = design.SetCustomizationSet(customizations);
design.SetApplyCustomize(CustomizeIndex.Race, json[CustomizeIndex.Race.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
design.SetApplyCustomize(CustomizeIndex.Clan, json[CustomizeIndex.Clan.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
design.SetApplyCustomize(CustomizeIndex.Gender, json[CustomizeIndex.Gender.ToString()]?["Apply"]?.ToObject<bool>() ?? false);
var set = design.CustomizationSet;
var set = design.CustomizeSet;
foreach (var idx in CustomizationExtensions.AllBasic)
{
var tok = json[idx.ToString()];
var data = (CustomizeValue)(tok?["Value"]?.ToObject<byte>() ?? 0);
if (set.IsAvailable(idx))
PrintWarning(CustomizationService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data,
if (set.IsAvailable(idx) && design._designData.Customize.BodyType == 1)
PrintWarning(CustomizeService.ValidateCustomizeValue(set, design._designData.Customize.Face, idx, data, out data,
allowUnknown));
var apply = tok?["Apply"]?.ToObject<bool>() ?? false;
design._designData.Customize[idx] = data;
@ -505,7 +507,7 @@ public class DesignBase
}
}
public void MigrateBase64(CustomizationService customize, ItemManager items, HumanModelList humans, string base64)
public void MigrateBase64(CustomizeService customize, ItemManager items, HumanModelList humans, string base64)
{
try
{
@ -519,7 +521,7 @@ public class DesignBase
SetApplyVisorToggle(applyVisor);
SetApplyWeaponVisible(applyWeapon);
SetApplyWetness(true);
CustomizationSet = SetCustomizationSet(customize);
CustomizeSet = SetCustomizationSet(customize);
}
catch (Exception ex)
{

View file

@ -1,9 +1,7 @@
using System;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -62,7 +60,7 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
}
case 5:
@ -73,7 +71,7 @@ public class DesignBase64Migration
data.SetHatVisible((bytes[90] & 0x01) == 0);
data.SetVisor((bytes[90] & 0x10) != 0);
data.SetWeaponVisible((bytes[90] & 0x02) == 0);
data.ModelId = (uint)bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
data.ModelId = bytes[91] | ((uint)bytes[92] << 8) | ((uint)bytes[93] << 16) | ((uint)bytes[94] << 24);
break;
default: throw new Exception($"Can not parse Base64 string into design for migration:\n\tInvalid Version {bytes[0]}.");
}
@ -102,11 +100,11 @@ public class DesignBase64Migration
if (!humans.IsHuman(data.ModelId))
{
data.LoadNonHuman(data.ModelId, *(Customize*)(ptr + 4), (nint)eq);
data.LoadNonHuman(data.ModelId, *(CustomizeArray*)(ptr + 4), (nint)eq);
return data;
}
data.Customize.Load(*(Customize*)(ptr + 4));
data.Customize = *(CustomizeArray*)(ptr + 4);
foreach (var (slot, idx) in EquipSlotExtensions.EqdpSlots.WithIndex())
{
var mdl = eq[idx];
@ -121,9 +119,9 @@ public class DesignBase64Migration
data.SetStain(slot, mdl.Stain);
}
var main = cur[0].Set.Id == 0
var main = cur[0].Skeleton.Id == 0
? items.DefaultSword
: items.Identify(EquipSlot.MainHand, cur[0].Set, cur[0].Type, cur[0].Variant);
: items.Identify(EquipSlot.MainHand, cur[0].Skeleton, cur[0].Weapon, cur[0].Variant);
if (!main.Valid)
{
Glamourer.Log.Warning("Base64 string invalid, weapon could not be identified.");
@ -135,10 +133,10 @@ public class DesignBase64Migration
EquipItem off;
// Fist weapon hack
if (main.ModelId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
if (main.PrimaryId.Id is > 1600 and < 1651 && cur[1].Variant == 0)
{
off = items.Identify(EquipSlot.OffHand, (SetId)(main.ModelId.Id + 50), main.WeaponType, main.Variant, main.Type);
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Set, (Variant)cur[1].Type.Id);
off = items.Identify(EquipSlot.OffHand, (PrimaryId)(main.PrimaryId.Id + 50), main.SecondaryId, main.Variant, main.Type);
var gauntlet = items.Identify(EquipSlot.Hands, cur[1].Skeleton, (Variant)cur[1].Weapon.Id);
if (gauntlet.Valid)
{
data.SetItem(EquipSlot.Hands, gauntlet);
@ -147,9 +145,9 @@ public class DesignBase64Migration
}
else
{
off = cur[0].Set.Id == 0
off = cur[0].Skeleton.Id == 0
? ItemManager.NothingItem(FullEquipType.Shield)
: items.Identify(EquipSlot.OffHand, cur[1].Set, cur[1].Type, cur[1].Variant, main.Type);
: items.Identify(EquipSlot.OffHand, cur[1].Skeleton, cur[1].Weapon, cur[1].Variant, main.Type);
}
if (main.Type.ValidOffhand() != FullEquipType.Unknown && !off.Valid)
@ -187,7 +185,7 @@ public class DesignBase64Migration
| (equipFlags.HasFlag(EquipFlag.Wrist) ? 0x02 : 0)
| (equipFlags.HasFlag(EquipFlag.RFinger) ? 0x04 : 0)
| (equipFlags.HasFlag(EquipFlag.LFinger) ? 0x08 : 0));
save.Customize.Write((nint)data + 4);
save.Customize.Write(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));
foreach (var slot in EquipSlotExtensions.EqdpSlots)

View file

@ -26,9 +26,9 @@ public class DesignColorUi
public DesignColorUi(DesignColors colors, DesignManager designs, Configuration config)
{
_colors = colors;
_designs = designs;
_config = config;
_colors = colors;
_designs = designs;
_config = config;
}
public void Draw()
@ -78,7 +78,7 @@ public class DesignColorUi
{
using var id = ImRaii.PushId(idx);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Trash.ToIconString(), buttonSize, tt, disabled, true))
{
changeString = name;
@ -119,7 +119,7 @@ public class DesignColorUi
changeString = _newName;
changeValue = 0xFFFFFFFF;
}
if (changeString.Length > 0)
{
@ -139,6 +139,7 @@ public class DesignColorUi
newColor = color;
return false;
}
ImGuiUtil.HoverTooltip(tooltip);
newColor = ImGui.ColorConvertFloat4ToU32(vec);
@ -148,12 +149,12 @@ public class DesignColorUi
public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
{
public const string AutomaticName = "Automatic";
public const string AutomaticName = "Automatic";
public const string MissingColorName = "Missing Color";
public const uint MissingColorDefault = 0xFF0000D0;
private readonly SaveService _saveService;
private readonly Dictionary<string, uint> _colors = new();
private readonly Dictionary<string, uint> _colors = [];
public uint MissingColor { get; private set; } = MissingColorDefault;
public event Action? ColorChanged;
@ -281,7 +282,7 @@ public class DesignColors : ISavable, IReadOnlyDictionary<string, uint>
public static uint AutoColor(DesignBase design)
{
var customize = design.ApplyCustomize == 0;
var customize = design.ApplyCustomizeExcludingBodyType == 0;
var equip = design.ApplyEquip == 0;
return (customize, equip) switch
{

View file

@ -2,20 +2,18 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Glamourer.Utility;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Designs;
public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizationService _customize, HumanModelList _humans)
public class DesignConverter(ItemManager _items, DesignManager _designs, CustomizeService _customize, HumanModelList _humans)
{
public const byte Version = 6;
@ -41,12 +39,18 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
=> ShareBase64(state, EquipFlagExtensions.All, CustomizeFlagExtensions.All, CrestExtensions.All);
public string ShareBase64(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
=> ShareBase64(state.ModelData, equipFlags, customizeFlags, crestFlags);
public string ShareBase64(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = Convert(state, equipFlags, customizeFlags, crestFlags);
var design = Convert(data, equipFlags, customizeFlags, crestFlags);
return ShareBase64(ShareJObject(design));
}
public DesignBase Convert(ActorState state, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
=> Convert(state.ModelData, equipFlags, customizeFlags, crestFlags);
public DesignBase Convert(in DesignData data, EquipFlag equipFlags, CustomizeFlag customizeFlags, CrestFlag crestFlags)
{
var design = _designs.CreateTemporary();
design.ApplyEquip = equipFlags & EquipFlagExtensions.All;
@ -56,7 +60,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
design.SetApplyVisorToggle(design.DoApplyEquip(EquipSlot.Head));
design.SetApplyWeaponVisible(design.DoApplyEquip(EquipSlot.MainHand) || design.DoApplyEquip(EquipSlot.OffHand));
design.SetApplyWetness(true);
design.SetDesignData(_customize, state.ModelData);
design.SetDesignData(_customize, data);
return design;
}
@ -146,7 +150,7 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
}
public IEnumerable<(EquipSlot Slot, EquipItem Item, StainId Stain)> FromDrawData(IReadOnlyList<CharacterArmor> armors,
CharacterWeapon mainhand, CharacterWeapon offhand)
CharacterWeapon mainhand, CharacterWeapon offhand, bool skipWarnings)
{
if (armors.Count != 10)
throw new ArgumentException("Invalid length of armor array.");
@ -158,15 +162,16 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
var item = _items.Identify(slot, armor.Set, armor.Variant);
if (!item.Valid)
{
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
if (!skipWarnings)
Glamourer.Log.Warning($"Appearance data {armor} for slot {slot} invalid, item could not be identified.");
item = ItemManager.NothingItem(slot);
}
yield return (slot, item, armor.Stain);
}
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Set, mainhand.Type, mainhand.Variant, FullEquipType.Unknown);
if (!mh.Valid)
var mh = _items.Identify(EquipSlot.MainHand, mainhand.Skeleton, mainhand.Weapon, mainhand.Variant);
if (!skipWarnings && !mh.Valid)
{
Glamourer.Log.Warning($"Appearance data {mainhand} for mainhand weapon invalid, item could not be identified.");
mh = _items.DefaultSword;
@ -174,8 +179,8 @@ public class DesignConverter(ItemManager _items, DesignManager _designs, Customi
yield return (EquipSlot.MainHand, mh, mainhand.Stain);
var oh = _items.Identify(EquipSlot.OffHand, offhand.Set, offhand.Type, offhand.Variant, mh.Type);
if (!oh.Valid)
var oh = _items.Identify(EquipSlot.OffHand, offhand.Skeleton, offhand.Weapon, offhand.Variant, mh.Type);
if (!skipWarnings && !oh.Valid)
{
Glamourer.Log.Warning($"Appearance data {offhand} for offhand weapon invalid, item could not be identified.");
oh = _items.GetDefaultOffhand(mh);

View file

@ -1,42 +1,39 @@
using System;
using System.Runtime.CompilerServices;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String.Functions;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
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;
public CrestFlag CrestVisibility;
private WeaponType _secondaryMainhand;
private WeaponType _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
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 CustomizeArray Customize = CustomizeArray.Default;
public uint ModelId;
public CrestFlag CrestVisibility;
private SecondaryId _secondaryMainhand;
private SecondaryId _secondaryOffhand;
private FullEquipType _typeMainhand;
private FullEquipType _typeOffhand;
private byte _states;
public bool IsHuman = true;
public DesignData()
{ }
@ -75,18 +72,18 @@ public unsafe struct DesignData
=> slot.ToIndex() switch
{
// @formatter:off
0 => EquipItem.FromIds(_itemIds[ 0], _iconIds[ 0], (SetId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (WeaponType)0, _equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ),
1 => EquipItem.FromIds(_itemIds[ 1], _iconIds[ 1], (SetId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (WeaponType)0, _equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ),
2 => EquipItem.FromIds(_itemIds[ 2], _iconIds[ 2], (SetId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (WeaponType)0, _equipmentBytes[10], FullEquipType.Hands, name: _nameHands ),
3 => EquipItem.FromIds(_itemIds[ 3], _iconIds[ 3], (SetId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (WeaponType)0, _equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ),
4 => EquipItem.FromIds(_itemIds[ 4], _iconIds[ 4], (SetId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (WeaponType)0, _equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ),
5 => EquipItem.FromIds(_itemIds[ 5], _iconIds[ 5], (SetId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (WeaponType)0, _equipmentBytes[22], FullEquipType.Ears, name: _nameEars ),
6 => EquipItem.FromIds(_itemIds[ 6], _iconIds[ 6], (SetId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (WeaponType)0, _equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ),
7 => EquipItem.FromIds(_itemIds[ 7], _iconIds[ 7], (SetId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (WeaponType)0, _equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ),
8 => EquipItem.FromIds(_itemIds[ 8], _iconIds[ 8], (SetId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (WeaponType)0, _equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ),
9 => EquipItem.FromIds(_itemIds[ 9], _iconIds[ 9], (SetId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (WeaponType)0, _equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ),
10 => EquipItem.FromIds(_itemIds[10], _iconIds[10], (SetId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, _equipmentBytes[42], _typeMainhand, name: _nameMainhand),
11 => EquipItem.FromIds(_itemIds[11], _iconIds[11], (SetId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, _equipmentBytes[46], _typeOffhand, name: _nameOffhand ),
0 => EquipItem.FromIds((ItemId)_itemIds[ 0], (IconId)_iconIds[ 0], (PrimaryId)(_equipmentBytes[ 0] | (_equipmentBytes[ 1] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 2], FullEquipType.Head, name: _nameHead ),
1 => EquipItem.FromIds((ItemId)_itemIds[ 1], (IconId)_iconIds[ 1], (PrimaryId)(_equipmentBytes[ 4] | (_equipmentBytes[ 5] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[ 6], FullEquipType.Body, name: _nameBody ),
2 => EquipItem.FromIds((ItemId)_itemIds[ 2], (IconId)_iconIds[ 2], (PrimaryId)(_equipmentBytes[ 8] | (_equipmentBytes[ 9] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[10], FullEquipType.Hands, name: _nameHands ),
3 => EquipItem.FromIds((ItemId)_itemIds[ 3], (IconId)_iconIds[ 3], (PrimaryId)(_equipmentBytes[12] | (_equipmentBytes[13] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[14], FullEquipType.Legs, name: _nameLegs ),
4 => EquipItem.FromIds((ItemId)_itemIds[ 4], (IconId)_iconIds[ 4], (PrimaryId)(_equipmentBytes[16] | (_equipmentBytes[17] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[18], FullEquipType.Feet, name: _nameFeet ),
5 => EquipItem.FromIds((ItemId)_itemIds[ 5], (IconId)_iconIds[ 5], (PrimaryId)(_equipmentBytes[20] | (_equipmentBytes[21] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[22], FullEquipType.Ears, name: _nameEars ),
6 => EquipItem.FromIds((ItemId)_itemIds[ 6], (IconId)_iconIds[ 6], (PrimaryId)(_equipmentBytes[24] | (_equipmentBytes[25] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[26], FullEquipType.Neck, name: _nameNeck ),
7 => EquipItem.FromIds((ItemId)_itemIds[ 7], (IconId)_iconIds[ 7], (PrimaryId)(_equipmentBytes[28] | (_equipmentBytes[29] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[30], FullEquipType.Wrists, name: _nameWrists ),
8 => EquipItem.FromIds((ItemId)_itemIds[ 8], (IconId)_iconIds[ 8], (PrimaryId)(_equipmentBytes[32] | (_equipmentBytes[33] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[34], FullEquipType.Finger, name: _nameRFinger ),
9 => EquipItem.FromIds((ItemId)_itemIds[ 9], (IconId)_iconIds[ 9], (PrimaryId)(_equipmentBytes[36] | (_equipmentBytes[37] << 8)), (SecondaryId)0, (Variant)_equipmentBytes[38], FullEquipType.Finger, name: _nameLFinger ),
10 => EquipItem.FromIds((ItemId)_itemIds[10], (IconId)_iconIds[10], (PrimaryId)(_equipmentBytes[40] | (_equipmentBytes[41] << 8)), _secondaryMainhand, (Variant)_equipmentBytes[42], _typeMainhand, name: _nameMainhand),
11 => EquipItem.FromIds((ItemId)_itemIds[11], (IconId)_iconIds[11], (PrimaryId)(_equipmentBytes[44] | (_equipmentBytes[45] << 8)), _secondaryOffhand, (Variant)_equipmentBytes[46], _typeOffhand, name: _nameOffhand ),
_ => new EquipItem(),
// @formatter:on
};
@ -129,8 +126,8 @@ public unsafe struct DesignData
_itemIds[index] = item.ItemId.Id;
_iconIds[index] = item.IconId.Id;
_equipmentBytes[4 * index + 0] = (byte)item.ModelId.Id;
_equipmentBytes[4 * index + 1] = (byte)(item.ModelId.Id >> 8);
_equipmentBytes[4 * index + 0] = (byte)item.PrimaryId.Id;
_equipmentBytes[4 * index + 1] = (byte)(item.PrimaryId.Id >> 8);
_equipmentBytes[4 * index + 2] = item.Variant.Id;
switch (index)
{
@ -148,12 +145,12 @@ public unsafe struct DesignData
// @formatter:on
case 10:
_nameMainhand = item.Name;
_secondaryMainhand = item.WeaponType;
_secondaryMainhand = item.SecondaryId;
_typeMainhand = item.Type;
return true;
case 11:
_nameOffhand = item.Name;
_secondaryOffhand = item.WeaponType;
_secondaryOffhand = item.SecondaryId;
_typeOffhand = item.Type;
return true;
}
@ -256,11 +253,11 @@ public unsafe struct DesignData
}
public bool LoadNonHuman(uint modelId, Customize customize, nint equipData)
public bool LoadNonHuman(uint modelId, CustomizeArray customize, nint equipData)
{
ModelId = modelId;
IsHuman = false;
Customize.Load(customize);
Customize.Read(customize.Data);
fixed (byte* ptr = _equipmentBytes)
{
MemoryUtility.MemCpyUnchecked(ptr, (byte*)equipData, 40);
@ -294,8 +291,8 @@ public unsafe struct DesignData
public readonly byte[] GetCustomizeBytes()
{
var ret = new byte[CustomizeData.Size];
fixed (byte* retPtr = ret, inPtr = Customize.Data.Data)
var ret = new byte[CustomizeArray.Size];
fixed (byte* retPtr = ret, inPtr = Customize.Data)
{
MemoryUtility.MemCpyUnchecked(retPtr, inPtr, ret.Length);
}

View file

@ -1,18 +1,20 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Utility;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -20,18 +22,18 @@ namespace Glamourer.Designs;
public class DesignManager
{
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly ItemManager _items;
private readonly HumanModelList _humans;
private readonly SaveService _saveService;
private readonly DesignChanged _event;
private readonly List<Design> _designs = new();
private readonly Dictionary<Guid, DesignData> _undoStore = new();
private readonly List<Design> _designs = [];
private readonly Dictionary<Guid, DesignData> _undoStore = [];
public IReadOnlyList<Design> Designs
=> _designs;
public DesignManager(SaveService saveService, ItemManager items, CustomizationService customizations,
public DesignManager(SaveService saveService, ItemManager items, CustomizeService customizations,
DesignChanged @event, HumanModelList humans)
{
_saveService = saveService;
@ -50,29 +52,44 @@ public class DesignManager
/// </summary>
public void LoadDesigns()
{
_humans.Awaiter.Wait();
_customizations.Awaiter.Wait();
_items.ItemData.Awaiter.Wait();
var stopwatch = Stopwatch.StartNew();
_designs.Clear();
List<(Design, string)> invalidNames = new();
var skipped = 0;
foreach (var file in _saveService.FileNames.Designs())
var skipped = 0;
ThreadLocal<List<(Design, string)>> designs = new(() => [], true);
Parallel.ForEach(_saveService.FileNames.Designs(), (f, _) =>
{
try
{
var text = File.ReadAllText(file.FullName);
var text = File.ReadAllText(f.FullName);
var data = JObject.Parse(text);
var design = Design.LoadDesign(_customizations, _items, data);
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(file.Name))
invalidNames.Add((design, file.FullName));
if (_designs.Any(f => f.Identifier == design.Identifier))
throw new Exception($"Identifier {design.Identifier} was not unique.");
design.Index = _designs.Count;
_designs.Add(design);
designs.Value!.Add((design, f.FullName));
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not load design, skipped:\n{ex}");
++skipped;
Interlocked.Increment(ref skipped);
}
});
List<(Design, string)> invalidNames = [];
foreach (var (design, path) in designs.Values.SelectMany(v => v))
{
if (design.Identifier.ToString() != Path.GetFileNameWithoutExtension(path))
invalidNames.Add((design, path));
if (_designs.Any(d => d.Identifier == design.Identifier))
{
Glamourer.Log.Error($"Could not load design, skipped: Identifier {design.Identifier} was not unique.");
++skipped;
continue;
}
design.Index = _designs.Count;
_designs.Add(design);
}
var failed = MoveInvalidNames(invalidNames);
@ -81,7 +98,7 @@ public class DesignManager
$"Moved {invalidNames.Count - failed} designs to correct names.{(failed > 0 ? $" Failed to move {failed} designs to correct names." : string.Empty)}");
Glamourer.Log.Information(
$"Loaded {_designs.Count} designs.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
$"Loaded {_designs.Count} designs in {stopwatch.ElapsedMilliseconds} ms.{(skipped > 0 ? $" Skipped loading {skipped} designs due to errors." : string.Empty)}");
_event.Invoke(DesignChanged.Type.ReloadedAll, null!);
}
@ -193,10 +210,10 @@ public class DesignManager
public void ChangeColor(Design design, string newColor)
{
var oldColor = design.Color;
if (oldColor == newColor)
if (oldColor == newColor)
return;
design.Color = newColor;
design.Color = newColor;
design.LastEdit = DateTimeOffset.UtcNow;
_saveService.QueueSave(design);
Glamourer.Log.Debug($"Changed color of design {design.Identifier}.");
@ -298,7 +315,7 @@ public class DesignManager
return;
case CustomizeIndex.Clan:
{
var customize = new Customize(design.DesignData.Customize.Data.Clone());
var customize = design.DesignData.Customize;
if (_customizations.ChangeClan(ref customize, (SubRace)value.Value) == 0)
return;
if (!design.SetCustomize(_customizations, customize))
@ -308,7 +325,7 @@ public class DesignManager
}
case CustomizeIndex.Gender:
{
var customize = new Customize(design.DesignData.Customize.Data.Clone());
var customize = design.DesignData.Customize;
if (_customizations.ChangeGender(ref customize, (Gender)(value.Value + 1)) == 0)
return;
if (!design.SetCustomize(_customizations, customize))

View file

@ -0,0 +1,126 @@
using Lumina.Data;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace Glamourer.GameData;
/// <summary> A custom version of CharaMakeParams that is easier to parse. </summary>
[Sheet("CharaMakeParams")]
public class CharaMakeParams : ExcelRow
{
public const int NumMenus = 28;
public const int NumVoices = 12;
public const int NumGraphics = 10;
public const int MaxNumValues = 100;
public const int NumFaces = 8;
public const int NumFeatures = 7;
public const int NumEquip = 3;
public enum MenuType
{
ListSelector = 0,
IconSelector = 1,
ColorPicker = 2,
DoubleColorPicker = 3,
IconCheckmark = 4,
Percentage = 5,
Checkmark = 6, // custom
Nothing = 7, // custom
List1Selector = 8, // custom, 1-indexed lists
}
public struct Menu
{
public uint Id;
public byte InitVal;
public MenuType Type;
public byte Size;
public byte LookAt;
public uint Mask;
public uint Customize;
public uint[] Values;
public byte[] Graphic;
}
public struct FacialFeatures
{
public uint[] Icons;
}
public LazyRow<Race> Race { get; set; } = null!;
public LazyRow<Tribe> Tribe { get; set; } = null!;
public sbyte Gender { get; set; }
public Menu[] Menus { get; set; } = new Menu[NumMenus];
public byte[] Voices { get; set; } = new byte[NumVoices];
public FacialFeatures[] FacialFeatureByFace { get; set; } = new FacialFeatures[NumFaces];
public CharaMakeType.CharaMakeTypeUnkData3347Obj[] Equip { get; set; } = new CharaMakeType.CharaMakeTypeUnkData3347Obj[NumEquip];
public override void PopulateData(RowParser parser, Lumina.GameData gameData, Language language)
{
RowId = parser.RowId;
SubRowId = parser.SubRowId;
Race = new LazyRow<Race>(gameData, parser.ReadColumn<uint>(0), language);
Tribe = new LazyRow<Tribe>(gameData, parser.ReadColumn<uint>(1), language);
Gender = parser.ReadColumn<sbyte>(2);
int currentOffset;
for (var i = 0; i < NumMenus; ++i)
{
currentOffset = 3 + i;
Menus[i].Id = parser.ReadColumn<uint>(0 * NumMenus + currentOffset);
Menus[i].InitVal = parser.ReadColumn<byte>(1 * NumMenus + currentOffset);
Menus[i].Type = (MenuType)parser.ReadColumn<byte>(2 * NumMenus + currentOffset);
Menus[i].Size = parser.ReadColumn<byte>(3 * NumMenus + currentOffset);
Menus[i].LookAt = parser.ReadColumn<byte>(4 * NumMenus + currentOffset);
Menus[i].Mask = parser.ReadColumn<uint>(5 * NumMenus + currentOffset);
Menus[i].Customize = parser.ReadColumn<uint>(6 * NumMenus + currentOffset);
Menus[i].Values = new uint[Menus[i].Size];
switch (Menus[i].Type)
{
case MenuType.ColorPicker:
case MenuType.DoubleColorPicker:
case MenuType.Percentage:
break;
default:
currentOffset += 7 * NumMenus;
for (var j = 0; j < Menus[i].Size; ++j)
Menus[i].Values[j] = parser.ReadColumn<uint>(j * NumMenus + currentOffset);
break;
}
Menus[i].Graphic = new byte[NumGraphics];
currentOffset = 3 + (MaxNumValues + 7) * NumMenus + i;
for (var j = 0; j < NumGraphics; ++j)
Menus[i].Graphic[j] = parser.ReadColumn<byte>(j * NumMenus + currentOffset);
}
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus;
for (var i = 0; i < NumVoices; ++i)
Voices[i] = parser.ReadColumn<byte>(currentOffset++);
for (var i = 0; i < NumFaces; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + i;
FacialFeatureByFace[i].Icons = new uint[NumFeatures];
for (var j = 0; j < NumFeatures; ++j)
FacialFeatureByFace[i].Icons[j] = (uint)parser.ReadColumn<int>(j * NumFaces + currentOffset);
}
for (var i = 0; i < NumEquip; ++i)
{
currentOffset = 3 + (MaxNumValues + 7 + NumGraphics) * NumMenus + NumVoices + NumFaces * NumFeatures + i * 7;
Equip[i] = new CharaMakeType.CharaMakeTypeUnkData3347Obj()
{
Helmet = parser.ReadColumn<ulong>(currentOffset + 0),
Top = parser.ReadColumn<ulong>(currentOffset + 1),
Gloves = parser.ReadColumn<ulong>(currentOffset + 2),
Legs = parser.ReadColumn<ulong>(currentOffset + 3),
Shoes = parser.ReadColumn<ulong>(currentOffset + 4),
Weapon = parser.ReadColumn<ulong>(currentOffset + 5),
SubWeapon = parser.ReadColumn<ulong>(currentOffset + 6),
};
}
}
}

View file

@ -0,0 +1,57 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Dalamud.Plugin.Services;
using Penumbra.String.Functions;
namespace Glamourer.GameData;
/// <summary> Parse the Human.cmp file as a list of 4-byte integer values to obtain colors. </summary>
public class ColorParameters : IReadOnlyList<uint>
{
private readonly uint[] _rgbaColors;
/// <summary> Get a slice of the colors starting at <paramref name="offset"/> and containing <paramref name="count"/> colors. </summary>
public ReadOnlySpan<uint> GetSlice(int offset, int count)
=> _rgbaColors.AsSpan(offset, count);
public unsafe ColorParameters(IDataManager gameData, IPluginLog log)
{
try
{
var file = gameData.GetFile("chara/xls/charamake/human.cmp")!;
// Just copy all the data into an uint array.
_rgbaColors = new uint[file.Data.Length >> 2];
fixed (byte* ptr1 = file.Data)
{
fixed (uint* ptr2 = _rgbaColors)
{
MemoryUtility.MemCpyUnchecked(ptr2, ptr1, file.Data.Length);
}
}
}
catch (Exception e)
{
log.Error("READ THIS\n======== Could not obtain the human.cmp file which is necessary for color sets.\n"
+ "======== This usually indicates an error with your index files caused by TexTools modifications.\n"
+ "======== If you have used TexTools before, you will probably need to start over in it to use Glamourer.", e);
_rgbaColors = [];
}
}
/// <inheritdoc/>
public IEnumerator<uint> GetEnumerator()
=> (IEnumerator<uint>)_rgbaColors.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> _rgbaColors.Length;
/// <inheritdoc/>
public uint this[int index]
=> _rgbaColors[index];
}

View file

@ -0,0 +1,64 @@
using System;
using System.Runtime.InteropServices;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
/// <summary>
/// Any customization value can be represented in 8 bytes by its ID,
/// a byte value, an optional value-id and an optional icon or color.
/// </summary>
[StructLayout(LayoutKind.Explicit)]
public readonly struct CustomizeData : IEquatable<CustomizeData>
{
/// <summary> The index of the option this value is for. </summary>
[FieldOffset(0)]
public readonly CustomizeIndex Index;
/// <summary> The value for the option. </summary>
[FieldOffset(1)]
public readonly CustomizeValue Value;
/// <summary> The internal ID for sheets. </summary>
[FieldOffset(2)]
public readonly ushort CustomizeId;
/// <summary> An ID for an associated icon. </summary>
[FieldOffset(4)]
public readonly uint IconId;
/// <summary> An ID for an associated color. </summary>
[FieldOffset(4)]
public readonly uint Color;
/// <summary> Construct a CustomizeData from single data values. </summary>
public CustomizeData(CustomizeIndex index, CustomizeValue value, uint data = 0, ushort customizeId = 0)
{
Index = index;
Value = value;
IconId = data;
Color = data;
CustomizeId = customizeId;
}
/// <inheritdoc/>
public bool Equals(CustomizeData other)
=> Index == other.Index
&& Value.Value == other.Value.Value
&& CustomizeId == other.CustomizeId;
/// <inheritdoc/>
public override bool Equals(object? obj)
=> obj is CustomizeData other && Equals(other);
/// <inheritdoc/>
public override int GetHashCode()
=> HashCode.Combine((int)Index, Value.Value, CustomizeId);
public static bool operator ==(CustomizeData left, CustomizeData right)
=> left.Equals(right);
public static bool operator !=(CustomizeData left, CustomizeData right)
=> !(left == right);
}

View file

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
using OtterGui.Classes;
using OtterGui.Services;
using Penumbra.GameData.Enums;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData;
/// <summary> Generate everything about customization per tribe and gender. </summary>
public class CustomizeManager : IAsyncDataContainer
{
/// <summary> All races except for Unknown </summary>
public static readonly IReadOnlyList<Race> Races = ((Race[])Enum.GetValues(typeof(Race))).Skip(1).ToArray();
/// <summary> All tribes except for Unknown </summary>
public static readonly IReadOnlyList<SubRace> Clans = ((SubRace[])Enum.GetValues(typeof(SubRace))).Skip(1).ToArray();
/// <summary> Two genders. </summary>
public static readonly IReadOnlyList<Gender> Genders =
[
Gender.Male,
Gender.Female,
];
/// <summary> Every tribe and gender has a separate set of available customizations. </summary>
public CustomizeSet GetSet(SubRace race, Gender gender)
{
if (!Finished)
Awaiter.Wait();
return _customizationSets[ToIndex(race, gender)];
}
/// <summary> Get specific icons. </summary>
public IDalamudTextureWrap GetIcon(uint id)
=> _icons.LoadIcon(id)!;
/// <summary> Iterate over all supported genders and clans. </summary>
public static IEnumerable<(SubRace Clan, Gender Gender)> AllSets()
{
foreach (var clan in Clans)
{
yield return (clan, Gender.Male);
yield return (clan, Gender.Female);
}
}
public CustomizeManager(ITextureProvider textures, IDataManager gameData, IPluginLog log, NpcCustomizeSet npcCustomizeSet)
{
_icons = new IconStorage(textures, gameData);
var stopwatch = new Stopwatch();
var tmpTask = Task.Run(() =>
{
stopwatch.Start();
return new CustomizeSetFactory(gameData, log, _icons, npcCustomizeSet);
});
var setTasks = AllSets().Select(p
=> tmpTask.ContinueWith(t => _customizationSets[ToIndex(p.Clan, p.Gender)] = t.Result.CreateSet(p.Clan, p.Gender)));
Awaiter = Task.WhenAll(setTasks).ContinueWith(_ =>
{
// This is far too hard to estimate sensibly.
TotalCount = 0;
Memory = 0;
Time = stopwatch.ElapsedMilliseconds;
});
}
/// <inheritdoc/>
public Task Awaiter { get; }
/// <inheritdoc/>
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
private readonly IconStorage _icons;
private static readonly int ListSize = Clans.Count * Genders.Count;
private readonly CustomizeSet[] _customizationSets = new CustomizeSet[ListSize];
/// <summary> Get the index for the given pair of tribe and gender. </summary>
private static int ToIndex(SubRace race, Gender gender)
{
var idx = ((int)race - 1) * Genders.Count + (gender == Gender.Female ? 1 : 0);
if (idx < 0 || idx >= ListSize)
throw new Exception($"Invalid customization requested for {race} {gender}.");
return idx;
}
public long Time { get; private set; }
public long Memory { get; private set; }
public string Name
=> nameof(CustomizeManager);
public int TotalCount { get; private set; }
}

View file

@ -0,0 +1,301 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
/// <summary>
/// Each SubRace and Gender combo has a customization set.
/// This describes the available customizations, their types and their names.
/// </summary>
public class CustomizeSet
{
internal CustomizeSet(SubRace clan, Gender gender)
{
Gender = gender;
Clan = clan;
Race = clan.ToRace();
SettingAvailable = 0;
}
public Gender Gender { get; }
public SubRace Clan { get; }
public Race Race { get; }
public string Name { get; internal init; } = string.Empty;
public CustomizeFlag SettingAvailable { get; internal set; }
internal void SetAvailable(CustomizeIndex index)
=> SettingAvailable |= index.ToFlag();
public bool IsAvailable(CustomizeIndex index)
=> SettingAvailable.HasFlag(index.ToFlag());
// Meta
public IReadOnlyList<string> OptionName { get; internal init; } = null!;
public string Option(CustomizeIndex index)
=> OptionName[(int)index];
public IReadOnlyList<byte> Voices { get; internal init; } = null!;
public IReadOnlyList<CharaMakeParams.MenuType> Types { get; internal set; } = null!;
public IReadOnlyDictionary<CharaMakeParams.MenuType, CustomizeIndex[]> Order { get; internal set; } = null!;
// Always list selector.
public int NumEyebrows { get; internal init; }
public int NumEyeShapes { get; internal init; }
public int NumNoseShapes { get; internal init; }
public int NumJawShapes { get; internal init; }
public int NumMouthShapes { get; internal init; }
// Always Icon Selector
public IReadOnlyList<CustomizeData> Faces { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HairStyles { get; internal init; } = null!;
public IReadOnlyList<IReadOnlyList<CustomizeData>> HairByFace { get; internal set; } = null!;
public IReadOnlyList<CustomizeData> TailEarShapes { get; internal init; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature1 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature2 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature3 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature4 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature5 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature6 { get; internal set; } = null!;
public IReadOnlyList<(CustomizeData, CustomizeData)> FacialFeature7 { get; internal set; } = null!;
public (CustomizeData, CustomizeData) LegacyTattoo { get; internal set; }
public IReadOnlyList<CustomizeData> FacePaints { get; internal init; } = null!;
public IReadOnlyList<(CustomizeIndex Type, CustomizeValue Value)> NpcOptions { get; internal set; } =
Array.Empty<(CustomizeIndex Type, CustomizeValue Value)>();
// Always Color Selector
public IReadOnlyList<CustomizeData> SkinColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HairColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> HighlightColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> EyeColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> TattooColors { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> FacePaintColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> FacePaintColorsDark { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> LipColorsLight { get; internal init; } = null!;
public IReadOnlyList<CustomizeData> LipColorsDark { get; internal init; } = null!;
public bool Validate(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{
if (IsAvailable(index))
return DataByValue(index, value, out custom, face) >= 0
|| NpcOptions.Any(t => t.Type == index && t.Value == value);
custom = null;
return value == CustomizeValue.Zero;
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int DataByValue(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom, CustomizeValue face)
{
var type = Types[(int)index];
return type switch
{
CharaMakeParams.MenuType.ListSelector => GetInteger0(out custom),
CharaMakeParams.MenuType.List1Selector => GetInteger1(out custom),
CharaMakeParams.MenuType.IconSelector => index switch
{
CustomizeIndex.Face => Get(Faces, HrothgarFaceHack(value), out custom),
CustomizeIndex.Hairstyle => Get((face = HrothgarFaceHack(face)).Value < HairByFace.Count ? HairByFace[face.Value] : HairStyles,
value, out custom),
CustomizeIndex.TailShape => Get(TailEarShapes, value, out custom),
CustomizeIndex.FacePaint => Get(FacePaints, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark, value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.ColorPicker => index switch
{
CustomizeIndex.SkinColor => Get(SkinColors, value, out custom),
CustomizeIndex.EyeColorLeft => Get(EyeColors, value, out custom),
CustomizeIndex.EyeColorRight => Get(EyeColors, value, out custom),
CustomizeIndex.HairColor => Get(HairColors, value, out custom),
CustomizeIndex.HighlightsColor => Get(HighlightColors, value, out custom),
CustomizeIndex.TattooColor => Get(TattooColors, value, out custom),
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.DoubleColorPicker => index switch
{
CustomizeIndex.LipColor => Get(LipColorsDark.Concat(LipColorsLight), value, out custom),
CustomizeIndex.FacePaintColor => Get(FacePaintColorsDark.Concat(FacePaintColorsLight), value, out custom),
_ => Invalid(out custom),
},
CharaMakeParams.MenuType.IconCheckmark => GetBool(index, value, out custom),
CharaMakeParams.MenuType.Percentage => GetInteger0(out custom),
CharaMakeParams.MenuType.Checkmark => GetBool(index, value, out custom),
_ => Invalid(out custom),
};
int Get(IEnumerable<CustomizeData> list, CustomizeValue v, out CustomizeData? output)
{
var (val, idx) = list.Cast<CustomizeData?>().WithIndex().FirstOrDefault(p => p.Value!.Value.Value == v);
if (val == null)
{
output = null;
return -1;
}
output = val;
return idx;
}
static int Invalid(out CustomizeData? custom)
{
custom = null;
return -1;
}
static int GetBool(CustomizeIndex index, CustomizeValue value, out CustomizeData? custom)
{
if (value == CustomizeValue.Zero)
{
custom = new CustomizeData(index, CustomizeValue.Zero);
return 0;
}
var (_, mask) = index.ToByteAndMask();
if (value.Value == mask)
{
custom = new CustomizeData(index, new CustomizeValue(mask), 0, 1);
return 1;
}
custom = null;
return -1;
}
int GetInteger1(out CustomizeData? custom)
{
if (value > 0 && value < Count(index) + 1)
{
custom = new CustomizeData(index, value, 0, (ushort)(value.Value - 1));
return value.Value;
}
custom = null;
return -1;
}
int GetInteger0(out CustomizeData? custom)
{
if (value < Count(index))
{
custom = new CustomizeData(index, value, 0, value.Value);
return value.Value;
}
custom = null;
return -1;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CustomizeData Data(CustomizeIndex index, int idx)
=> Data(index, idx, CustomizeValue.Zero);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CustomizeData Data(CustomizeIndex index, int idx, CustomizeValue face)
{
if (idx >= Count(index, face = HrothgarFaceHack(face)))
throw new IndexOutOfRangeException();
switch (Types[(int)index])
{
case CharaMakeParams.MenuType.Percentage: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.ListSelector: return new CustomizeData(index, (CustomizeValue)idx, 0, (ushort)idx);
case CharaMakeParams.MenuType.List1Selector: return new CustomizeData(index, (CustomizeValue)(idx + 1), 0, (ushort)idx);
case CharaMakeParams.MenuType.Checkmark: return new CustomizeData(index, CustomizeValue.Bool(idx != 0), 0, (ushort)idx);
}
return index switch
{
CustomizeIndex.Face => Faces[idx],
CustomizeIndex.Hairstyle => face < HairByFace.Count ? HairByFace[face.Value][idx] : HairStyles[idx],
CustomizeIndex.TailShape => TailEarShapes[idx],
CustomizeIndex.FacePaint => FacePaints[idx],
CustomizeIndex.FacialFeature1 => idx == 0 ? FacialFeature1[face.Value].Item1 : FacialFeature1[face.Value].Item2,
CustomizeIndex.FacialFeature2 => idx == 0 ? FacialFeature2[face.Value].Item1 : FacialFeature2[face.Value].Item2,
CustomizeIndex.FacialFeature3 => idx == 0 ? FacialFeature3[face.Value].Item1 : FacialFeature3[face.Value].Item2,
CustomizeIndex.FacialFeature4 => idx == 0 ? FacialFeature4[face.Value].Item1 : FacialFeature4[face.Value].Item2,
CustomizeIndex.FacialFeature5 => idx == 0 ? FacialFeature5[face.Value].Item1 : FacialFeature5[face.Value].Item2,
CustomizeIndex.FacialFeature6 => idx == 0 ? FacialFeature6[face.Value].Item1 : FacialFeature6[face.Value].Item2,
CustomizeIndex.FacialFeature7 => idx == 0 ? FacialFeature7[face.Value].Item1 : FacialFeature7[face.Value].Item2,
CustomizeIndex.LegacyTattoo => idx == 0 ? LegacyTattoo.Item1 : LegacyTattoo.Item2,
CustomizeIndex.SkinColor => SkinColors[idx],
CustomizeIndex.EyeColorLeft => EyeColors[idx],
CustomizeIndex.EyeColorRight => EyeColors[idx],
CustomizeIndex.HairColor => HairColors[idx],
CustomizeIndex.HighlightsColor => HighlightColors[idx],
CustomizeIndex.TattooColor => TattooColors[idx],
CustomizeIndex.LipColor => idx < 96 ? LipColorsDark[idx] : LipColorsLight[idx - 96],
CustomizeIndex.FacePaintColor => idx < 96 ? FacePaintColorsDark[idx] : FacePaintColorsLight[idx - 96],
_ => new CustomizeData(0, CustomizeValue.Zero),
};
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public CharaMakeParams.MenuType Type(CustomizeIndex index)
=> Types[(int)index];
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int Count(CustomizeIndex index)
=> Count(index, CustomizeValue.Zero);
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public int Count(CustomizeIndex index, CustomizeValue face)
{
if (!IsAvailable(index))
return 0;
return Type(index) switch
{
CharaMakeParams.MenuType.Percentage => 101,
CharaMakeParams.MenuType.IconCheckmark => 2,
CharaMakeParams.MenuType.Checkmark => 2,
_ => index switch
{
CustomizeIndex.Face => Faces.Count,
CustomizeIndex.Hairstyle => (face = HrothgarFaceHack(face)) < HairByFace.Count
? HairByFace[face.Value].Count
: HairStyles.Count,
CustomizeIndex.SkinColor => SkinColors.Count,
CustomizeIndex.EyeColorRight => EyeColors.Count,
CustomizeIndex.HairColor => HairColors.Count,
CustomizeIndex.HighlightsColor => HighlightColors.Count,
CustomizeIndex.TattooColor => TattooColors.Count,
CustomizeIndex.Eyebrows => NumEyebrows,
CustomizeIndex.EyeColorLeft => EyeColors.Count,
CustomizeIndex.EyeShape => NumEyeShapes,
CustomizeIndex.Nose => NumNoseShapes,
CustomizeIndex.Jaw => NumJawShapes,
CustomizeIndex.Mouth => NumMouthShapes,
CustomizeIndex.LipColor => LipColorsLight.Count + LipColorsDark.Count,
CustomizeIndex.TailShape => TailEarShapes.Count,
CustomizeIndex.FacePaint => FacePaints.Count,
CustomizeIndex.FacePaintColor => FacePaintColorsLight.Count + FacePaintColorsDark.Count,
_ => throw new ArgumentOutOfRangeException(nameof(index), index, null),
},
};
}
private CustomizeValue HrothgarFaceHack(CustomizeValue value)
=> Race == Race.Hrothgar && value.Value is > 4 and < 9 ? value - 4 : value;
}
public static class CustomizationSetExtensions
{
/// <summary> Return only the available customizations in this set and Clan or Gender. </summary>
public static CustomizeFlag FixApplication(this CustomizeFlag flag, CustomizeSet set)
=> flag & (set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType);
}

View file

@ -0,0 +1,473 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Race = Penumbra.GameData.Enums.Race;
namespace Glamourer.GameData;
internal class CustomizeSetFactory(
IDataManager _gameData,
IPluginLog _log,
IconStorage _icons,
NpcCustomizeSet _npcCustomizeSet,
ColorParameters _colors)
{
public CustomizeSetFactory(IDataManager gameData, IPluginLog log, IconStorage icons, NpcCustomizeSet npcCustomizeSet)
: this(gameData, log, icons, npcCustomizeSet, new ColorParameters(gameData, log))
{ }
/// <summary> Create the set of all available customization options for a given clan and gender. </summary>
public CustomizeSet CreateSet(SubRace race, Gender gender)
{
var (skin, hair) = GetSkinHairColors(race, gender);
var row = _charaMakeSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var hrothgar = race.ToRace() == Race.Hrothgar;
// Create the initial set with all the easily accessible parameters available for anyone.
var set = new CustomizeSet(race, gender)
{
Name = GetName(race, gender),
Voices = row.Voices,
HairStyles = GetHairStyles(race, gender),
HairColors = hair,
SkinColors = skin,
EyeColors = _eyeColorPicker,
HighlightColors = _highlightPicker,
TattooColors = _tattooColorPicker,
LipColorsDark = hrothgar ? HrothgarFurPattern(row) : _lipColorPickerDark,
LipColorsLight = hrothgar ? [] : _lipColorPickerLight,
FacePaintColorsDark = _facePaintColorPickerDark,
FacePaintColorsLight = _facePaintColorPickerLight,
Faces = GetFaces(row),
NumEyebrows = GetListSize(row, CustomizeIndex.Eyebrows),
NumEyeShapes = GetListSize(row, CustomizeIndex.EyeShape),
NumNoseShapes = GetListSize(row, CustomizeIndex.Nose),
NumJawShapes = GetListSize(row, CustomizeIndex.Jaw),
NumMouthShapes = GetListSize(row, CustomizeIndex.Mouth),
FacePaints = GetFacePaints(race, gender),
TailEarShapes = GetTailEarShapes(row),
OptionName = GetOptionNames(row),
Types = GetMenuTypes(row),
};
SetPostProcessing(set, row);
return set;
}
/// <summary> Some data can not be set independently of the rest, so we need a post-processing step to finalize. </summary>
private void SetPostProcessing(CustomizeSet set, CharaMakeParams row)
{
SetAvailability(set, row);
SetFacialFeatures(set, row);
SetHairByFace(set);
SetNpcData(set, set.Clan, set.Gender);
SetOrder(set);
}
/// <summary> Given a customize set with filled data, find all customizations used by valid NPCs that are not regularly available. </summary>
private void SetNpcData(CustomizeSet set, SubRace race, Gender gender)
{
var customizeIndices = new[]
{
CustomizeIndex.Face,
CustomizeIndex.Hairstyle,
CustomizeIndex.LipColor,
CustomizeIndex.SkinColor,
CustomizeIndex.FacePaintColor,
CustomizeIndex.HighlightsColor,
CustomizeIndex.HairColor,
CustomizeIndex.FacePaint,
CustomizeIndex.TattooColor,
CustomizeIndex.EyeColorLeft,
CustomizeIndex.EyeColorRight,
};
var npcCustomizations = new HashSet<(CustomizeIndex, CustomizeValue)>();
_npcCustomizeSet.Awaiter.Wait();
foreach (var customize in _npcCustomizeSet.Select(s => s.Customize).Where(c => c.Clan == race && c.Gender == gender && c.BodyType.Value == 1))
{
foreach (var customizeIndex in customizeIndices)
{
var value = customize[customizeIndex];
if (value == CustomizeValue.Zero)
continue;
if (set.DataByValue(customizeIndex, value, out _, customize.Face) >= 0)
continue;
npcCustomizations.Add((customizeIndex, value));
}
}
set.NpcOptions = npcCustomizations.OrderBy(p => p.Item1).ThenBy(p => p.Item2.Value).ToArray();
}
private readonly ColorParameters _colorParameters = new(_gameData, _log);
private readonly ExcelSheet<CharaMakeCustomize> _customizeSheet = _gameData.GetExcelSheet<CharaMakeCustomize>(ClientLanguage.English)!;
private readonly ExcelSheet<Lobby> _lobbySheet = _gameData.GetExcelSheet<Lobby>(ClientLanguage.English)!;
private readonly ExcelSheet<HairMakeType> _hairSheet = _gameData.GetExcelSheet<HairMakeType>(ClientLanguage.English)!;
private readonly ExcelSheet<Tribe> _tribeSheet = _gameData.GetExcelSheet<Tribe>(ClientLanguage.English)!;
// Those color pickers are shared between all races.
private readonly CustomizeData[] _highlightPicker = CreateColors(_colors, CustomizeIndex.HighlightsColor, 256, 192);
private readonly CustomizeData[] _lipColorPickerDark = CreateColors(_colors, CustomizeIndex.LipColor, 512, 96);
private readonly CustomizeData[] _lipColorPickerLight = CreateColors(_colors, CustomizeIndex.LipColor, 1024, 96, true);
private readonly CustomizeData[] _eyeColorPicker = CreateColors(_colors, CustomizeIndex.EyeColorLeft, 0, 192);
private readonly CustomizeData[] _facePaintColorPickerDark = CreateColors(_colors, CustomizeIndex.FacePaintColor, 640, 96);
private readonly CustomizeData[] _facePaintColorPickerLight = CreateColors(_colors, CustomizeIndex.FacePaintColor, 1152, 96, true);
private readonly CustomizeData[] _tattooColorPicker = CreateColors(_colors, CustomizeIndex.TattooColor, 0, 192);
private readonly ExcelSheet<CharaMakeParams> _charaMakeSheet = _gameData.Excel
.GetType()
.GetMethod("GetSheet", BindingFlags.Instance | BindingFlags.NonPublic)?
.MakeGenericMethod(typeof(CharaMakeParams))
.Invoke(_gameData.Excel, ["charamaketype", _gameData.Language.ToLumina(), null])! as ExcelSheet<CharaMakeParams>
?? null!;
/// <summary> Obtain available skin and hair colors for the given clan and gender. </summary>
private (CustomizeData[] Skin, CustomizeData[] Hair) GetSkinHairColors(SubRace race, Gender gender)
{
if (race is > SubRace.Veena or SubRace.Unknown)
throw new ArgumentOutOfRangeException(nameof(race), race, null);
var gv = gender == Gender.Male ? 0 : 1;
var idx = ((int)race * 2 + gv) * 5 + 3;
return (CreateColors(_colorParameters, CustomizeIndex.SkinColor, idx << 8, 192),
CreateColors(_colorParameters, CustomizeIndex.HairColor, (idx + 1) << 8, 192));
}
/// <summary> Obtain the gender-specific clan name. </summary>
private string GetName(SubRace race, Gender gender)
=> gender switch
{
Gender.Male => _tribeSheet.GetRow((uint)race)?.Masculine.ToDalamudString().TextValue ?? race.ToName(),
Gender.Female => _tribeSheet.GetRow((uint)race)?.Feminine.ToDalamudString().TextValue ?? race.ToName(),
_ => "Unknown",
};
/// <summary> Obtain available hairstyles via reflection from the Hair sheet for the given subrace and gender. </summary>
private CustomizeData[] GetHairStyles(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
// Unknown30 is the number of available hairstyles.
var hairList = new List<CustomizeData>(row.Unknown30);
// Hairstyles can be found starting at Unknown66.
for (var i = 0; i < row.Unknown30; ++i)
{
var name = $"Unknown{66 + i * 9}";
var customizeIdx = (uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
// Hair Row from CustomizeSheet might not be set in case of unlockable hair.
var hairRow = _customizeSheet.GetRow(customizeIdx);
if (hairRow == null)
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)i, customizeIdx));
else if (_icons.IconExists(hairRow.Icon))
hairList.Add(new CustomizeData(CustomizeIndex.Hairstyle, (CustomizeValue)hairRow.FeatureID, hairRow.Icon,
(ushort)hairRow.RowId));
}
return [.. hairList.OrderBy(h => h.Value.Value)];
}
/// <summary> Specific icons for tails or ears. </summary>
private CustomizeData[] GetTailEarShapes(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.TailShape.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.TailShape, v, i)).ToArray()
?? [];
/// <summary> Specific icons for faces. </summary>
private CustomizeData[] GetFaces(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.Face.ToByteAndMask().ByteIdx)
?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.Face, v, i)).ToArray()
?? [];
/// <summary> Specific icons for Hrothgar patterns. </summary>
private CustomizeData[] HrothgarFurPattern(CharaMakeParams row)
=> row.Menus.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == CustomizeIndex.LipColor.ToByteAndMask().ByteIdx)?.Values
.Select((v, i) => FromValueAndIndex(CustomizeIndex.LipColor, v, i)).ToArray()
?? [];
/// <summary> Get face paints from the hair sheet via reflection since there are also unlockable face paints. </summary>
private CustomizeData[] GetFacePaints(SubRace race, Gender gender)
{
var row = _hairSheet.GetRow(((uint)race - 1) * 2 - 1 + (uint)gender)!;
var paintList = new List<CustomizeData>(row.Unknown37);
// Number of available face paints is at Unknown37.
for (var i = 0; i < row.Unknown37; ++i)
{
// Face paints start at Unknown73.
var name = $"Unknown{73 + i * 9}";
var customizeIdx =
(uint?)row.GetType().GetProperty(name, BindingFlags.Public | BindingFlags.Instance)?.GetValue(row)
?? uint.MaxValue;
if (customizeIdx == uint.MaxValue)
continue;
var paintRow = _customizeSheet.GetRow(customizeIdx);
// Face paint Row from CustomizeSheet might not be set in case of unlockable face paints.
if (paintRow != null)
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)paintRow.FeatureID, paintRow.Icon,
(ushort)paintRow.RowId));
else
paintList.Add(new CustomizeData(CustomizeIndex.FacePaint, (CustomizeValue)i, customizeIdx));
}
return [.. paintList.OrderBy(p => p.Value.Value)];
}
/// <summary> Get List sizes. </summary>
private static int GetListSize(CharaMakeParams row, CustomizeIndex index)
{
var gameId = index.ToByteAndMask().ByteIdx;
var menu = row.Menus.Cast<CharaMakeParams.Menu?>().FirstOrDefault(m => m!.Value.Customize == gameId);
return menu?.Size ?? 0;
}
/// <summary> Get generic Features. </summary>
private CustomizeData FromValueAndIndex(CustomizeIndex id, uint value, int index)
{
var row = _customizeSheet.GetRow(value);
return row == null
? new CustomizeData(id, (CustomizeValue)(index + 1), value)
: new CustomizeData(id, (CustomizeValue)row.FeatureID, row.Icon, (ushort)row.RowId);
}
/// <summary> Create generic color sets from the parameters. </summary>
private static CustomizeData[] CreateColors(ColorParameters colorParameters, CustomizeIndex index, int offset, int num,
bool light = false)
{
var ret = new CustomizeData[num];
var idx = 0;
foreach (var value in colorParameters.GetSlice(offset, num))
{
ret[idx] = new CustomizeData(index, (CustomizeValue)(light ? 128 + idx : idx), value, (ushort)(offset + idx));
++idx;
}
return ret;
}
/// <summary> Set the specific option names for the given set of parameters. </summary>
private string[] GetOptionNames(CharaMakeParams row)
{
var nameArray = Enum.GetValues<CustomizeIndex>().Select(c =>
{
// Find the first menu that corresponds to the Id.
var byteId = c.ToByteAndMask().ByteIdx;
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == byteId);
if (menu == null)
{
// If none exists and the id corresponds to highlights, set the Highlights name.
if (c == CustomizeIndex.Highlights)
return string.Intern(_lobbySheet.GetRow(237)?.Text.ToDalamudString().ToString() ?? "Highlights");
// Otherwise there is an error and we use the default name.
return c.ToDefaultName();
}
// Otherwise all is normal, get the menu name or if it does not work the default name.
var textRow = _lobbySheet.GetRow(menu.Value.Id);
return string.Intern(textRow?.Text.ToDalamudString().ToString() ?? c.ToDefaultName());
}).ToArray();
// Add names for both eye colors.
nameArray[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorLeft.ToDefaultName();
nameArray[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.EyeColorRight.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature1] = CustomizeIndex.FacialFeature1.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature2] = CustomizeIndex.FacialFeature2.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature3] = CustomizeIndex.FacialFeature3.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature4] = CustomizeIndex.FacialFeature4.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature5] = CustomizeIndex.FacialFeature5.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature6] = CustomizeIndex.FacialFeature6.ToDefaultName();
nameArray[(int)CustomizeIndex.FacialFeature7] = CustomizeIndex.FacialFeature7.ToDefaultName();
nameArray[(int)CustomizeIndex.LegacyTattoo] = CustomizeIndex.LegacyTattoo.ToDefaultName();
nameArray[(int)CustomizeIndex.SmallIris] = CustomizeIndex.SmallIris.ToDefaultName();
nameArray[(int)CustomizeIndex.Lipstick] = CustomizeIndex.Lipstick.ToDefaultName();
nameArray[(int)CustomizeIndex.FacePaintReversed] = CustomizeIndex.FacePaintReversed.ToDefaultName();
return nameArray;
}
/// <summary> Get the manu types for all available options. </summary>
private static CharaMakeParams.MenuType[] GetMenuTypes(CharaMakeParams row)
{
// Set up the menu types for all customizations.
return Enum.GetValues<CustomizeIndex>().Select(c =>
{
// Those types are not correctly given in the menu, so special case them to color pickers.
switch (c)
{
case CustomizeIndex.HighlightsColor:
case CustomizeIndex.EyeColorLeft:
case CustomizeIndex.EyeColorRight:
case CustomizeIndex.FacePaintColor:
return CharaMakeParams.MenuType.ColorPicker;
case CustomizeIndex.BodyType: return CharaMakeParams.MenuType.Nothing;
case CustomizeIndex.FacePaintReversed:
case CustomizeIndex.Highlights:
case CustomizeIndex.SmallIris:
case CustomizeIndex.Lipstick:
return CharaMakeParams.MenuType.Checkmark;
case CustomizeIndex.FacialFeature1:
case CustomizeIndex.FacialFeature2:
case CustomizeIndex.FacialFeature3:
case CustomizeIndex.FacialFeature4:
case CustomizeIndex.FacialFeature5:
case CustomizeIndex.FacialFeature6:
case CustomizeIndex.FacialFeature7:
case CustomizeIndex.LegacyTattoo:
return CharaMakeParams.MenuType.IconCheckmark;
}
var gameId = c.ToByteAndMask().ByteIdx;
// Otherwise find the first menu corresponding to the id.
// If there is none, assume a list.
var menu = row.Menus
.Cast<CharaMakeParams.Menu?>()
.FirstOrDefault(m => m!.Value.Customize == gameId);
var ret = menu?.Type ?? CharaMakeParams.MenuType.ListSelector;
if (c is CustomizeIndex.TailShape && ret is CharaMakeParams.MenuType.ListSelector)
ret = CharaMakeParams.MenuType.List1Selector;
return ret;
}).ToArray();
}
/// <summary> Set the availability of options according to actual availability. </summary>
private static void SetAvailability(CustomizeSet set, CharaMakeParams row)
{
// TODO: Hrothgar female
if (set is { Race: Race.Hrothgar, Gender: Gender.Female })
return;
Set(true, CustomizeIndex.Height);
Set(set.Faces.Count > 0, CustomizeIndex.Face);
Set(true, CustomizeIndex.Hairstyle);
Set(true, CustomizeIndex.Highlights);
Set(true, CustomizeIndex.SkinColor);
Set(true, CustomizeIndex.EyeColorRight);
Set(true, CustomizeIndex.HairColor);
Set(true, CustomizeIndex.HighlightsColor);
Set(true, CustomizeIndex.TattooColor);
Set(set.NumEyebrows > 0, CustomizeIndex.Eyebrows);
Set(true, CustomizeIndex.EyeColorLeft);
Set(set.NumEyeShapes > 0, CustomizeIndex.EyeShape);
Set(set.NumNoseShapes > 0, CustomizeIndex.Nose);
Set(set.NumJawShapes > 0, CustomizeIndex.Jaw);
Set(set.NumMouthShapes > 0, CustomizeIndex.Mouth);
Set(set.LipColorsDark.Count > 0, CustomizeIndex.LipColor);
Set(GetListSize(row, CustomizeIndex.MuscleMass) > 0, CustomizeIndex.MuscleMass);
Set(set.TailEarShapes.Count > 0, CustomizeIndex.TailShape);
Set(GetListSize(row, CustomizeIndex.BustSize) > 0, CustomizeIndex.BustSize);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaint);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintColor);
Set(true, CustomizeIndex.FacialFeature1);
Set(true, CustomizeIndex.FacialFeature2);
Set(true, CustomizeIndex.FacialFeature3);
Set(true, CustomizeIndex.FacialFeature4);
Set(true, CustomizeIndex.FacialFeature5);
Set(true, CustomizeIndex.FacialFeature6);
Set(true, CustomizeIndex.FacialFeature7);
Set(true, CustomizeIndex.LegacyTattoo);
Set(true, CustomizeIndex.SmallIris);
Set(set.Race != Race.Hrothgar, CustomizeIndex.Lipstick);
Set(set.FacePaints.Count > 0, CustomizeIndex.FacePaintReversed);
return;
void Set(bool available, CustomizeIndex flag)
{
if (available)
set.SetAvailable(flag);
}
}
internal static void SetOrder(CustomizeSet set)
{
var ret = Enum.GetValues<CustomizeIndex>().ToArray();
ret[(int)CustomizeIndex.TattooColor] = CustomizeIndex.EyeColorLeft;
ret[(int)CustomizeIndex.EyeColorLeft] = CustomizeIndex.EyeColorRight;
ret[(int)CustomizeIndex.EyeColorRight] = CustomizeIndex.TattooColor;
var dict = ret.Skip(2).Where(set.IsAvailable).GroupBy(set.Type).ToDictionary(k => k.Key, k => k.ToArray());
foreach (var type in Enum.GetValues<CharaMakeParams.MenuType>())
dict.TryAdd(type, []);
set.Order = dict;
}
/// <summary> Set hairstyles per face for Hrothgar and make it simple for non-Hrothgar. </summary>
private void SetHairByFace(CustomizeSet set)
{
if (set.Race != Race.Hrothgar)
{
set.HairByFace = Enumerable.Repeat(set.HairStyles, set.Faces.Count + 1).ToArray();
return;
}
var tmp = new IReadOnlyList<CustomizeData>[set.Faces.Count + 1];
tmp[0] = set.HairStyles;
for (var i = 1; i <= set.Faces.Count; ++i)
{
tmp[i] = set.HairStyles.Where(Valid).ToArray();
continue;
bool Valid(CustomizeData c)
{
var data = _customizeSheet.GetRow(c.CustomizeId)?.Unknown6 ?? 0;
return data == 0 || data == i + set.Faces.Count;
}
}
set.HairByFace = tmp;
}
/// <summary>
/// Create a list of lists of facial features and the legacy tattoo.
/// Facial Features are bools in a bitfield, so we supply an "off" and an "on" value for simplicity of use.
/// </summary>
private static void SetFacialFeatures(CustomizeSet set, CharaMakeParams row)
{
var count = set.Faces.Count;
set.FacialFeature1 = new List<(CustomizeData, CustomizeData)>(count);
set.LegacyTattoo = Create(CustomizeIndex.LegacyTattoo, 137905);
var tmp = Enumerable.Repeat(0, 7).Select(_ => new (CustomizeData, CustomizeData)[count + 1]).ToArray();
for (var i = 0; i < count; ++i)
{
var data = row.FacialFeatureByFace[i].Icons;
tmp[0][i + 1] = Create(CustomizeIndex.FacialFeature1, data[0]);
tmp[1][i + 1] = Create(CustomizeIndex.FacialFeature2, data[1]);
tmp[2][i + 1] = Create(CustomizeIndex.FacialFeature3, data[2]);
tmp[3][i + 1] = Create(CustomizeIndex.FacialFeature4, data[3]);
tmp[4][i + 1] = Create(CustomizeIndex.FacialFeature5, data[4]);
tmp[5][i + 1] = Create(CustomizeIndex.FacialFeature6, data[5]);
tmp[6][i + 1] = Create(CustomizeIndex.FacialFeature7, data[6]);
}
set.FacialFeature1 = tmp[0];
set.FacialFeature2 = tmp[1];
set.FacialFeature3 = tmp[2];
set.FacialFeature4 = tmp[3];
set.FacialFeature5 = tmp[4];
set.FacialFeature6 = tmp[5];
set.FacialFeature7 = tmp[6];
return;
static (CustomizeData, CustomizeData) Create(CustomizeIndex i, uint data)
=> (new CustomizeData(i, CustomizeValue.Zero, data), new CustomizeData(i, CustomizeValue.Max, data, 1));
}
}

View file

@ -0,0 +1,319 @@
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Lumina.Excel.GeneratedSheets;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
/// <summary> Contains a set of all human NPC appearances with their names. </summary>
public class NpcCustomizeSet : IAsyncDataContainer, IReadOnlyList<NpcData>
{
/// <inheritdoc/>
public string Name
=> nameof(NpcCustomizeSet);
/// <inheritdoc/>
public long Time { get; private set; }
/// <inheritdoc/>
public long Memory { get; private set; }
/// <inheritdoc/>
public int TotalCount
=> _data.Count;
/// <inheritdoc/>
public Task Awaiter { get; }
/// <inheritdoc/>
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
/// <summary> The list of data. </summary>
private readonly List<NpcData> _data = [];
/// <summary> Create the data when ready. </summary>
public NpcCustomizeSet(IDataManager data, DictENpc eNpcs, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var waitTask = Task.WhenAll(eNpcs.Awaiter, bNpcs.Awaiter, bNpcNames.Awaiter);
Awaiter = waitTask.ContinueWith(_ =>
{
var watch = Stopwatch.StartNew();
var eNpcTask = Task.Run(() => CreateEnpcData(data, eNpcs));
var bNpcTask = Task.Run(() => CreateBnpcData(data, bNpcs, bNpcNames));
FilterAndOrderNpcData(eNpcTask.Result, bNpcTask.Result);
Time = watch.ElapsedMilliseconds;
});
}
/// <summary> Create data from event NPCs. </summary>
private static List<NpcData> CreateEnpcData(IDataManager data, DictENpc eNpcs)
{
var enpcSheet = data.GetExcelSheet<ENpcBase>()!;
var list = new List<NpcData>(eNpcs.Count);
// Go through all event NPCs already collected into a dictionary.
foreach (var (id, name) in eNpcs)
{
var row = enpcSheet.GetRow(id.Id);
// We only accept NPCs with valid names.
if (row == null || name.IsNullOrWhitespace())
continue;
// Check if the customization is a valid human.
var (valid, customize) = FromEnpcBase(row);
if (!valid)
continue;
var ret = new NpcData
{
Name = name,
Customize = customize,
Id = id,
Kind = ObjectKind.EventNpc,
};
// Event NPCs have a reference to NpcEquip but also contain the appearance in their own row.
// Prefer the NpcEquip reference if it is set, otherwise use the own.
if (row.NpcEquip.Row != 0 && row.NpcEquip.Value is { } equip)
{
ApplyNpcEquip(ref ret, equip);
}
else
{
ret.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
ret.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
ret.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
ret.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
ret.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
ret.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
ret.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
ret.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
ret.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
ret.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
ret.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
ret.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
ret.VisorToggled = row.Visor;
}
list.Add(ret);
}
return list;
}
/// <summary> Create data from battle NPCs. </summary>
private static List<NpcData> CreateBnpcData(IDataManager data, DictBNpc bNpcs, DictBNpcNames bNpcNames)
{
var bnpcSheet = data.GetExcelSheet<BNpcBase>()!;
var list = new List<NpcData>((int)bnpcSheet.RowCount);
// We go through all battle NPCs in the sheet because the dictionary refers to names.
foreach (var baseRow in bnpcSheet)
{
// Only accept humans.
if (baseRow.ModelChara.Value!.Type != 1)
continue;
var bnpcNameIds = bNpcNames[baseRow.RowId];
// Only accept battle NPCs with known associated names.
if (bnpcNameIds.Count == 0)
continue;
// Check if the customization is a valid human.
var (valid, customize) = FromBnpcCustomize(baseRow.BNpcCustomize.Value!);
if (!valid)
continue;
var equip = baseRow.NpcEquip.Value!;
var ret = new NpcData
{
Customize = customize,
Id = baseRow.RowId,
Kind = ObjectKind.BattleNpc,
};
ApplyNpcEquip(ref ret, equip);
// Add the appearance for each associated name.
foreach (var bnpcNameId in bnpcNameIds)
{
if (bNpcs.TryGetValue(bnpcNameId.Id, out var name) && !name.IsNullOrWhitespace())
list.Add(ret with { Name = name });
}
}
return list;
}
/// <summary> Given the battle NPC and event NPC lists, order and deduplicate entries. </summary>
private void FilterAndOrderNpcData(IReadOnlyCollection<NpcData> eNpcEquip, IReadOnlyCollection<NpcData> bNpcEquip)
{
_data.Clear();
// This is a maximum since we deduplicate.
_data.EnsureCapacity(eNpcEquip.Count + bNpcEquip.Count);
// Convert the NPCs to a dictionary of lists grouped by name.
var groups = eNpcEquip.Concat(bNpcEquip).GroupBy(d => d.Name).ToDictionary(g => g.Key, g => g.ToList());
// Iterate through the sorted list.
foreach (var (_, duplicates) in groups.OrderBy(kvp => kvp.Key))
{
// Remove any duplicate entries for a name with identical data.
for (var i = 0; i < duplicates.Count; ++i)
{
var current = duplicates[i];
for (var j = 0; j < i; ++j)
{
if (current.DataEquals(duplicates[j]))
{
duplicates.RemoveAt(i--);
break;
}
}
}
// If there is only a single entry, add that.
if (duplicates.Count == 1)
{
_data.Add(duplicates[0]);
Memory += 96;
}
else
{
_data.AddRange(duplicates);
Memory += 96 * duplicates.Count;
}
}
// Sort non-alphanumeric entries at the end instead of the beginning.
var lastWeird = _data.FindIndex(d => char.IsAsciiLetterOrDigit(d.Name[0]));
if (lastWeird != -1)
{
_data.AddRange(_data.Take(lastWeird));
_data.RemoveRange(0, lastWeird);
}
// Reduce memory footprint.
_data.TrimExcess();
}
/// <summary> Apply equipment from a NpcEquip row. </summary>
private static void ApplyNpcEquip(ref NpcData data, NpcEquip row)
{
data.Set(0, row.ModelHead | (row.DyeHead.Row << 24));
data.Set(1, row.ModelBody | (row.DyeBody.Row << 24));
data.Set(2, row.ModelHands | (row.DyeHands.Row << 24));
data.Set(3, row.ModelLegs | (row.DyeLegs.Row << 24));
data.Set(4, row.ModelFeet | (row.DyeFeet.Row << 24));
data.Set(5, row.ModelEars | (row.DyeEars.Row << 24));
data.Set(6, row.ModelNeck | (row.DyeNeck.Row << 24));
data.Set(7, row.ModelWrists | (row.DyeWrists.Row << 24));
data.Set(8, row.ModelRightRing | (row.DyeRightRing.Row << 24));
data.Set(9, row.ModelLeftRing | (row.DyeLeftRing.Row << 24));
data.Mainhand = new CharacterWeapon(row.ModelMainHand | ((ulong)row.DyeMainHand.Row << 48));
data.Offhand = new CharacterWeapon(row.ModelOffHand | ((ulong)row.DyeOffHand.Row << 48));
data.VisorToggled = row.Visor;
}
/// <summary> Obtain customizations from a BNpcCustomize row and check if the human is valid. </summary>
private static (bool, CustomizeArray) FromBnpcCustomize(BNpcCustomize bnpcCustomize)
{
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue)(byte)bnpcCustomize.Race.Row);
customize.SetByIndex(1, (CustomizeValue)bnpcCustomize.Gender);
customize.SetByIndex(2, (CustomizeValue)bnpcCustomize.BodyType);
customize.SetByIndex(3, (CustomizeValue)bnpcCustomize.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)bnpcCustomize.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue)bnpcCustomize.Face);
customize.SetByIndex(6, (CustomizeValue)bnpcCustomize.HairStyle);
customize.SetByIndex(7, (CustomizeValue)bnpcCustomize.HairHighlight);
customize.SetByIndex(8, (CustomizeValue)bnpcCustomize.SkinColor);
customize.SetByIndex(9, (CustomizeValue)bnpcCustomize.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue)bnpcCustomize.HairColor);
customize.SetByIndex(11, (CustomizeValue)bnpcCustomize.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue)bnpcCustomize.FacialFeature);
customize.SetByIndex(13, (CustomizeValue)bnpcCustomize.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue)bnpcCustomize.Eyebrows);
customize.SetByIndex(15, (CustomizeValue)bnpcCustomize.EyeColor);
customize.SetByIndex(16, (CustomizeValue)bnpcCustomize.EyeShape);
customize.SetByIndex(17, (CustomizeValue)bnpcCustomize.Nose);
customize.SetByIndex(18, (CustomizeValue)bnpcCustomize.Jaw);
customize.SetByIndex(19, (CustomizeValue)bnpcCustomize.Mouth);
customize.SetByIndex(20, (CustomizeValue)bnpcCustomize.LipColor);
customize.SetByIndex(21, (CustomizeValue)bnpcCustomize.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue)bnpcCustomize.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue)bnpcCustomize.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue)bnpcCustomize.FacePaint);
customize.SetByIndex(25, (CustomizeValue)bnpcCustomize.FacePaintColor);
if (!CustomizeManager.Races.Contains(customize.Race)
|| !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
return (true, customize);
}
/// <summary> Obtain customizations from a ENpcBase row and check if the human is valid. </summary>
private static (bool, CustomizeArray) FromEnpcBase(ENpcBase enpcBase)
{
if (enpcBase.ModelChara.Value?.Type != 1)
return (false, CustomizeArray.Default);
var customize = new CustomizeArray();
customize.SetByIndex(0, (CustomizeValue)(byte)enpcBase.Race.Row);
customize.SetByIndex(1, (CustomizeValue)enpcBase.Gender);
customize.SetByIndex(2, (CustomizeValue)enpcBase.BodyType);
customize.SetByIndex(3, (CustomizeValue)enpcBase.Height);
customize.SetByIndex(4, (CustomizeValue)(byte)enpcBase.Tribe.Row);
customize.SetByIndex(5, (CustomizeValue)enpcBase.Face);
customize.SetByIndex(6, (CustomizeValue)enpcBase.HairStyle);
customize.SetByIndex(7, (CustomizeValue)enpcBase.HairHighlight);
customize.SetByIndex(8, (CustomizeValue)enpcBase.SkinColor);
customize.SetByIndex(9, (CustomizeValue)enpcBase.EyeHeterochromia);
customize.SetByIndex(10, (CustomizeValue)enpcBase.HairColor);
customize.SetByIndex(11, (CustomizeValue)enpcBase.HairHighlightColor);
customize.SetByIndex(12, (CustomizeValue)enpcBase.FacialFeature);
customize.SetByIndex(13, (CustomizeValue)enpcBase.FacialFeatureColor);
customize.SetByIndex(14, (CustomizeValue)enpcBase.Eyebrows);
customize.SetByIndex(15, (CustomizeValue)enpcBase.EyeColor);
customize.SetByIndex(16, (CustomizeValue)enpcBase.EyeShape);
customize.SetByIndex(17, (CustomizeValue)enpcBase.Nose);
customize.SetByIndex(18, (CustomizeValue)enpcBase.Jaw);
customize.SetByIndex(19, (CustomizeValue)enpcBase.Mouth);
customize.SetByIndex(20, (CustomizeValue)enpcBase.LipColor);
customize.SetByIndex(21, (CustomizeValue)enpcBase.BustOrTone1);
customize.SetByIndex(22, (CustomizeValue)enpcBase.ExtraFeature1);
customize.SetByIndex(23, (CustomizeValue)enpcBase.ExtraFeature2OrBust);
customize.SetByIndex(24, (CustomizeValue)enpcBase.FacePaint);
customize.SetByIndex(25, (CustomizeValue)enpcBase.FacePaintColor);
if (!CustomizeManager.Races.Contains(customize.Race)
|| !CustomizeManager.Clans.Contains(customize.Clan)
|| !CustomizeManager.Genders.Contains(customize.Gender))
return (false, CustomizeArray.Default);
return (true, customize);
}
/// <inheritdoc/>
public IEnumerator<NpcData> GetEnumerator()
=> _data.GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
public int Count
=> _data.Count;
/// <inheritdoc/>
public NpcData this[int index]
=> _data[index];
}

View file

@ -0,0 +1,109 @@
using System;
using System.Text;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Penumbra.GameData.Structs;
namespace Glamourer.GameData;
/// <summary> A struct containing everything to replicate the appearance of a human NPC. </summary>
public unsafe struct NpcData
{
/// <summary> The name of the NPC. </summary>
public string Name;
/// <summary> The customizations of the NPC. </summary>
public CustomizeArray Customize;
/// <summary> The equipment appearance of the NPC, 10 * CharacterArmor. </summary>
private fixed byte _equip[40];
/// <summary> The mainhand weapon appearance of the NPC. </summary>
public CharacterWeapon Mainhand;
/// <summary> The offhand weapon appearance of the NPC. </summary>
public CharacterWeapon Offhand;
/// <summary> The data ID of the NPC, either event NPC or battle NPC name. </summary>
public NpcId Id;
/// <summary> Whether the NPCs visor is toggled. </summary>
public bool VisorToggled;
/// <summary> Whether the NPC is an event NPC or a battle NPC. </summary>
public ObjectKind Kind;
/// <summary> Obtain the equipment as CharacterArmors. </summary>
public ReadOnlySpan<CharacterArmor> Equip
{
get
{
fixed (byte* ptr = _equip)
{
return new ReadOnlySpan<CharacterArmor>((CharacterArmor*)ptr, 10);
}
}
}
/// <summary> Write all the gear appearance to a single string. </summary>
public string WriteGear()
{
var sb = new StringBuilder(128);
var span = Equip;
for (var i = 0; i < 10; ++i)
{
sb.Append(span[i].Set.Id.ToString("D4"))
.Append('-')
.Append(span[i].Variant.Id.ToString("D3"))
.Append('-')
.Append(span[i].Stain.Id.ToString("D3"))
.Append(", ");
}
sb.Append(Mainhand.Skeleton.Id.ToString("D4"))
.Append('-')
.Append(Mainhand.Weapon.Id.ToString("D4"))
.Append('-')
.Append(Mainhand.Variant.Id.ToString("D3"))
.Append('-')
.Append(Mainhand.Stain.Id.ToString("D4"))
.Append(", ")
.Append(Offhand.Skeleton.Id.ToString("D4"))
.Append('-')
.Append(Offhand.Weapon.Id.ToString("D4"))
.Append('-')
.Append(Offhand.Variant.Id.ToString("D3"))
.Append('-')
.Append(Offhand.Stain.Id.ToString("D3"));
return sb.ToString();
}
/// <summary> Set an equipment piece to a given value. </summary>
internal void Set(int idx, uint value)
{
fixed (byte* ptr = _equip)
{
((uint*)ptr)[idx] = value;
}
}
/// <summary> Check if the appearance data, excluding ID and Name, of two NpcData is equal. </summary>
public bool DataEquals(in NpcData other)
{
if (VisorToggled != other.VisorToggled)
return false;
if (!Customize.Equals(other.Customize))
return false;
if (!Mainhand.Equals(other.Mainhand))
return false;
if (!Offhand.Equals(other.Offhand))
return false;
fixed (byte* ptr1 = _equip, ptr2 = other._equip)
{
return new ReadOnlySpan<byte>(ptr1, 40).SequenceEqual(new ReadOnlySpan<byte>(ptr2, 40));
}
}
}

View file

@ -8,6 +8,7 @@ using Glamourer.State;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Services;
namespace Glamourer;
@ -25,21 +26,21 @@ public class Glamourer : IDalamudPlugin
public static readonly Logger Log = new();
public static MessageService Messager { get; private set; } = null!;
private readonly ServiceProvider _services;
private readonly ServiceManager _services;
public Glamourer(DalamudPluginInterface pluginInterface)
{
try
{
_services = ServiceManager.CreateProvider(pluginInterface, Log);
Messager = _services.GetRequiredService<MessageService>();
_services.GetRequiredService<VisorService>();
_services.GetRequiredService<WeaponService>();
_services.GetRequiredService<ScalingService>();
_services.GetRequiredService<StateListener>(); // Initialize State Listener.
_services.GetRequiredService<GlamourerWindowSystem>(); // initialize ui.
_services.GetRequiredService<CommandService>(); // initialize commands.
_services.GetRequiredService<GlamourerIpc>(); // initialize IPC.
_services = ServiceManagerA.CreateProvider(pluginInterface, Log);
Messager = _services.GetService<MessageService>();
_services.GetService<VisorService>();
_services.GetService<WeaponService>();
_services.GetService<ScalingService>();
_services.GetService<StateListener>(); // Initialize State Listener.
_services.GetService<GlamourerWindowSystem>(); // initialize ui.
_services.GetService<CommandService>(); // initialize commands.
_services.GetService<GlamourerIpc>(); // initialize IPC.
Log.Information($"Glamourer v{Version} loaded successfully.");
}
catch

View file

@ -84,7 +84,6 @@
<ItemGroup>
<ProjectReference Include="..\OtterGui\OtterGui.csproj" />
<ProjectReference Include="..\Glamourer.GameData\Glamourer.GameData.csproj" />
<ProjectReference Include="..\Penumbra.Api\Penumbra.Api.csproj" />
<ProjectReference Include="..\Penumbra.String\Penumbra.string.csproj" />
<ProjectReference Include="..\Penumbra.GameData\Penumbra.GameData.csproj" />
@ -109,8 +108,8 @@
<Target Name="GetGitHash" BeforeTargets="GetAssemblyVersion" Returns="InformationalVersion">
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" StandardOutputImportance="low" ContinueOnError="true">
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess"/>
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0"/>
<Output TaskParameter="ExitCode" PropertyName="GitCommitHashSuccess" />
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitHash" Condition="$(GitCommitHashSuccess) == 0" />
</Exec>
<PropertyGroup>

View file

@ -28,6 +28,8 @@ public enum ColorId
TriStateCheck,
TriStateCross,
TriStateNeutral,
BattleNpc,
EventNpc,
}
public static class Colors
@ -60,7 +62,9 @@ public static class Colors
ColorId.QuickDesignBg => (0x00F0F0F0, "Quick Design Bar Window Background", "The color of the window background in the quick design bar." ),
ColorId.TriStateCheck => (0xFF00D000, "Checkmark in Tri-State Checkboxes", "The color of the checkmark indicating positive change in tri-state checkboxes." ),
ColorId.TriStateCross => (0xFF0000D0, "Cross in Tri-State Checkboxes", "The color of the cross indicating negative change in tri-state checkboxes." ),
ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes" ),
ColorId.TriStateNeutral => (0xFFD0D0D0, "Dot in Tri-State Checkboxes", "The color of the dot indicating no change in tri-state checkboxes." ),
ColorId.BattleNpc => (0xFFFFFFFF, "Battle NPC in NPC Tab", "The color of the names of battle NPCs in the NPC tab that do not have a more specific color assigned." ),
ColorId.EventNpc => (0xFFFFFFFF, "Event NPC in NPC Tab", "The color of the names of event NPCs in the NPC tab that do not have a more specific color assigned." ),
_ => (0x00000000, string.Empty, string.Empty ),
// @formatter:on
};

View file

@ -1,11 +1,11 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Customization;
@ -15,12 +15,12 @@ public partial class CustomizationDrawer
private void DrawColorPicker(CustomizeIndex index)
{
using var _ = SetId(index);
using var id = SetId(index);
var (current, custom) = GetCurrentCustomization(index);
var color = ImGui.ColorConvertU32ToFloat4(current < 0 ? ImGui.GetColorU32(ImGuiCol.FrameBg) : custom.Color);
using (var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, 2 * ImGuiHelpers.GlobalScale, current < 0))
{
if (ImGui.ColorButton($"{_customize[index].Value}##color", color, ImGuiColorEditFlags.None, _framedIconSize))
ImGui.OpenPopup(ColorPickerPopupName);
@ -39,7 +39,7 @@ public partial class CustomizationDrawer
ImGui.SameLine();
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
DataInputInt(current, npc);
if (_withApply)
@ -89,7 +89,7 @@ public partial class CustomizationDrawer
{
var current = _set.DataByValue(index, _customize[index], out var custom, _customize.Face);
if (_set.IsAvailable(index) && current < 0)
return (current, new CustomizeData(index, _customize[index], 0, 0));
return (current, new CustomizeData(index, _customize[index]));
return (current, custom!.Value);
}

View file

@ -1,11 +1,12 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
@ -75,4 +76,20 @@ public partial class CustomizationDrawer
if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
ImGui.SetTooltip("The race can not be changed as this requires a redraw of the character, which is not supported for this actor.");
}
private void DrawBodyType()
{
if (_customize.BodyType.Value == 1)
return;
var label = _lockedRedraw
? $"Body Type {_customize.BodyType.Value}"
: $"Reset Body Type {_customize.BodyType.Value} to Default";
if (!ImGuiUtil.DrawDisabledButton(label, new Vector2(_raceSelectorWidth + _framedIconSize.X + ImGui.GetStyle().ItemSpacing.X, 0),
string.Empty, _lockedRedraw))
return;
Changed |= CustomizeFlag.BodyType;
_customize.BodyType = (CustomizeValue)1;
}
}

View file

@ -1,10 +1,11 @@
using System;
using System.Numerics;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
@ -14,7 +15,7 @@ public partial class CustomizationDrawer
private void DrawIconSelector(CustomizeIndex index)
{
using var _ = SetId(index);
using var id = SetId(index);
using var bigGroup = ImRaii.Group();
var label = _currentOption;
@ -28,8 +29,8 @@ public partial class CustomizationDrawer
npc = true;
}
var icon = _service.AwaitedService.GetIcon(custom!.Value.IconId);
using (var disabled = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
var icon = _service.Manager.GetIcon(custom!.Value.IconId);
using (_ = ImRaii.Disabled(_locked || _currentIndex is CustomizeIndex.Face && _lockedRedraw))
{
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize))
ImGui.OpenPopup(IconSelectorPopup);
@ -38,7 +39,7 @@ public partial class CustomizationDrawer
ImGuiUtil.HoverIconTooltip(icon, _iconSize);
ImGui.SameLine();
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
DataInputInt(current, npc);
if (_lockedRedraw && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
@ -68,7 +69,7 @@ public partial class CustomizationDrawer
for (var i = 0; i < _currentCount; ++i)
{
var custom = _set.Data(_currentIndex, i, _customize.Face);
var icon = _service.AwaitedService.GetIcon(custom.IconId);
var icon = _service.Manager.GetIcon(custom.IconId);
using (var _ = ImRaii.Group())
{
using var frameColor = ImRaii.PushColor(ImGuiCol.Button, Colors.SelectedRed, current == i);
@ -119,16 +120,16 @@ public partial class CustomizationDrawer
ImGui.Dummy(new Vector2(ImGui.GetFrameHeight()));
}
var oldValue = _customize.Data.At(_currentIndex.ToByteAndMask().ByteIdx);
var tmp = (int)oldValue;
var oldValue = _customize.AtIndex(_currentIndex.ToByteAndMask().ByteIdx);
var tmp = (int)oldValue.Value;
ImGui.SetNextItemWidth(_inputIntSize);
if (ImGui.InputInt("##text", ref tmp, 1, 1))
{
tmp = Math.Clamp(tmp, 0, byte.MaxValue);
if (tmp != oldValue)
if (tmp != oldValue.Value)
{
_customize.Data.Set(_currentIndex.ToByteAndMask().ByteIdx, (byte)tmp);
var changes = (byte)tmp ^ oldValue;
_customize.SetByIndex(_currentIndex.ToByteAndMask().ByteIdx, (CustomizeValue)tmp);
var changes = (byte)tmp ^ oldValue.Value;
Changed |= ((changes & 0x01) == 0x01 ? CustomizeFlag.FacialFeature1 : 0)
| ((changes & 0x02) == 0x02 ? CustomizeFlag.FacialFeature2 : 0)
| ((changes & 0x04) == 0x04 ? CustomizeFlag.FacialFeature3 : 0)
@ -179,8 +180,8 @@ public partial class CustomizationDrawer
var enabled = _customize.Get(featureIdx) != CustomizeValue.Zero;
var feature = _set.Data(featureIdx, 0, face);
var icon = featureIdx == CustomizeIndex.LegacyTattoo
? _legacyTattoo ?? _service.AwaitedService.GetIcon(feature.IconId)
: _service.AwaitedService.GetIcon(feature.IconId);
? _legacyTattoo ?? _service.Manager.GetIcon(feature.IconId)
: _service.Manager.GetIcon(feature.IconId);
if (ImGui.ImageButton(icon.ImGuiHandle, _iconSize, Vector2.Zero, Vector2.One, (int)ImGui.GetStyle().FramePadding.X,
Vector4.Zero, enabled ? Vector4.One : _redTint))
{

View file

@ -1,10 +1,10 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
@ -91,10 +91,10 @@ public partial class CustomizationDrawer
private void DrawListSelector(CustomizeIndex index, bool indexedBy1)
{
using var _ = SetId(index);
using var id = SetId(index);
using var bigGroup = ImRaii.Group();
using (var disabled = ImRaii.Disabled(_locked))
using (_ = ImRaii.Disabled(_locked))
{
if (indexedBy1)
{
@ -210,7 +210,7 @@ public partial class CustomizationDrawer
}
else
{
using (var disabled = ImRaii.Disabled(_locked))
using (_ = ImRaii.Disabled(_locked))
{
if (ImGui.Checkbox("##toggle", ref tmp))
{

View file

@ -4,17 +4,17 @@ using System.Reflection;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Customization;
public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizationService _service, CodeService _codes, Configuration _config)
public partial class CustomizationDrawer(DalamudPluginInterface pi, CustomizeService _service, CodeService _codes, Configuration _config)
: IDisposable
{
private readonly Vector4 _redTint = new(0.6f, 0.3f, 0.3f, 1f);
@ -22,13 +22,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
private Exception? _terminate;
private Customize _customize = Customize.Default;
private CustomizationSet _set = null!;
private CustomizeArray _customize = CustomizeArray.Default;
private CustomizeSet _set = null!;
public Customize Customize
public CustomizeArray Customize
=> _customize;
public CustomizeFlag CurrentFlag { get; private set; }
public CustomizeFlag Changed { get; private set; }
public CustomizeFlag ChangeApply { get; private set; }
@ -47,18 +46,16 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
public void Dispose()
=> _legacyTattoo?.Dispose();
public bool Draw(Customize current, bool locked, bool lockedRedraw)
public bool Draw(CustomizeArray current, bool locked, bool lockedRedraw)
{
CurrentFlag = CustomizeFlagExtensions.All;
_withApply = false;
Init(current, locked, lockedRedraw);
return DrawInternal();
}
public bool Draw(Customize current, CustomizeFlag apply, bool locked, bool lockedRedraw)
public bool Draw(CustomizeArray current, CustomizeFlag apply, bool locked, bool lockedRedraw)
{
CurrentFlag = CustomizeFlagExtensions.All;
ChangeApply = apply;
_initialApply = apply;
_withApply = !_config.HideApplyCheckmarks;
@ -66,12 +63,12 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
return DrawInternal();
}
private void Init(Customize current, bool locked, bool lockedRedraw)
private void Init(CustomizeArray current, bool locked, bool lockedRedraw)
{
UpdateSizes();
_terminate = null;
Changed = 0;
_customize.Load(current);
_terminate = null;
Changed = 0;
_customize = current;
_locked = locked;
_lockedRedraw = lockedRedraw;
}
@ -116,11 +113,13 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
try
{
if (_codes.EnabledArtisan)
if (_codes.Enabled(CodeService.CodeFlag.Artisan))
return DrawArtisan();
DrawRaceGenderSelector();
_set = _service.AwaitedService.GetList(_customize.Clan, _customize.Gender);
DrawBodyType();
_set = _service.Manager.GetSet(_customize.Clan, _customize.Gender);
foreach (var id in _set.Order[CharaMakeParams.MenuType.Percentage])
PercentageSelector(id);
@ -153,23 +152,23 @@ public partial class CustomizationDrawer(DalamudPluginInterface pi, Customizatio
private unsafe bool DrawArtisan()
{
for (var i = 0; i < CustomizeData.Size; ++i)
for (var i = 0; i < CustomizeArray.Size; ++i)
{
using var id = ImRaii.PushId(i);
int value = _customize.Data.Data[i];
int value = _customize.Data[i];
ImGui.SetNextItemWidth(40 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt(string.Empty, ref value, 0, 0))
{
var newValue = (byte)Math.Clamp(value, 0, byte.MaxValue);
if (newValue != _customize.Data.Data[i])
if (newValue != _customize.Data[i])
foreach (var flag in Enum.GetValues<CustomizeIndex>())
{
var (j, mask) = flag.ToByteAndMask();
var (j, _) = flag.ToByteAndMask();
if (j == i)
Changed |= flag.ToFlag();
}
_customize.Data.Data[i] = newValue;
_customize.Data[i] = newValue;
}
}

View file

@ -4,7 +4,7 @@ using System.Linq;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
@ -13,6 +13,7 @@ using OtterGui;
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
@ -179,7 +180,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
private readonly AutoDesignManager _autoDesignManager;
public RevertDesignCombo(DesignManager designs, DesignFileSystem fileSystem, TabSelected tabSelected, DesignColors designColors,
ItemManager items, CustomizationService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager,
ItemManager items, CustomizeService customize, Logger log, DesignChanged designChanged, AutoDesignManager autoDesignManager,
EphemeralConfig config)
: this(designs, fileSystem, tabSelected, designColors, CreateRevertDesign(customize, items), log, designChanged, autoDesignManager,
config)
@ -209,7 +210,7 @@ public sealed class RevertDesignCombo : DesignComboBase, IDisposable
_autoDesignManager.AddDesign(set, CurrentSelection!.Item1 == RevertDesign ? null : CurrentSelection!.Item1);
}
private static Design CreateRevertDesign(CustomizationService customize, ItemManager items)
private static Design CreateRevertDesign(CustomizeService customize, ItemManager items)
=> new(customize, items)
{
Index = RevertDesignIndex,

View file

@ -1,6 +1,7 @@
using System;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Services;
using Glamourer.State;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -28,7 +29,9 @@ public ref struct EquipDrawData(EquipSlot slot, in DesignData designData)
public static EquipDrawData FromDesign(DesignManager manager, Design design, EquipSlot slot)
=> new(slot, design.DesignData)
{
ItemSetter = slot.IsEquipmentPiece() ? i => manager.ChangeEquip(design, slot, i) : i => manager.ChangeWeapon(design, slot, i),
ItemSetter = slot.IsEquipment() || slot.IsAccessory()
? i => manager.ChangeEquip(design, slot, i)
: i => manager.ChangeWeapon(design, slot, i),
StainSetter = i => manager.ChangeStain(design, slot, i),
ApplySetter = b => manager.ChangeApplyEquip(design, slot, b),
ApplyStainSetter = b => manager.ChangeApplyStain(design, slot, b),

View file

@ -12,7 +12,7 @@ using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -24,7 +24,7 @@ public class EquipmentDrawer
private readonly ItemManager _items;
private readonly GlamourerColorCombo _stainCombo;
private readonly StainData _stainData;
private readonly DictStain _stainData;
private readonly ItemCombo[] _itemCombo;
private readonly Dictionary<FullEquipType, WeaponCombo> _weaponCombo;
private readonly CodeService _codes;
@ -66,8 +66,8 @@ public class EquipmentDrawer
_iconSize = new Vector2(2 * ImGui.GetFrameHeight() + ImGui.GetStyle().ItemSpacing.Y);
_comboLength = DefaultWidth * ImGuiHelpers.GlobalScale;
if (_requiredComboWidthUnscaled == 0)
_requiredComboWidthUnscaled = _items.ItemService.AwaitedService.AllItems(true)
.Concat(_items.ItemService.AwaitedService.AllItems(false))
_requiredComboWidthUnscaled = _items.ItemData.AllItems(true)
.Concat(_items.ItemData.AllItems(false))
.Max(i => ImGui.CalcTextSize($"{i.Item2.Name} ({i.Item2.ModelString})").X)
/ ImGuiHelpers.GlobalScale;
@ -94,7 +94,7 @@ public class EquipmentDrawer
if (_config.SmallEquip)
DrawEquipSmall(equipDrawData);
else if (!equipDrawData.Locked && _codes.EnabledArtisan)
else if (!equipDrawData.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawEquipArtisan(equipDrawData);
else
DrawEquipNormal(equipDrawData);
@ -102,7 +102,7 @@ public class EquipmentDrawer
public void DrawWeapons(EquipDrawData mainhand, EquipDrawData offhand, bool allWeapons)
{
if (mainhand.CurrentItem.ModelId.Id == 0)
if (mainhand.CurrentItem.PrimaryId.Id == 0)
return;
if (_config.HideApplyCheckmarks)
@ -117,7 +117,7 @@ public class EquipmentDrawer
if (_config.SmallEquip)
DrawWeaponsSmall(mainhand, offhand, allWeapons);
else if (!mainhand.Locked && _codes.EnabledArtisan)
else if (!mainhand.Locked && _codes.Enabled(CodeService.CodeFlag.Artisan))
DrawWeaponsArtisan(mainhand, offhand);
else
DrawWeaponsNormal(mainhand, offhand, allWeapons);
@ -202,24 +202,24 @@ public class EquipmentDrawer
void DrawWeapon(in EquipDrawData current)
{
int setId = current.CurrentItem.ModelId.Id;
int type = current.CurrentItem.WeaponType.Id;
int setId = current.CurrentItem.PrimaryId.Id;
int type = current.CurrentItem.SecondaryId.Id;
int variant = current.CurrentItem.Variant.Id;
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##setId", ref setId, 0, 0))
{
var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != current.CurrentItem.ModelId.Id)
current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.WeaponType, current.CurrentItem.Variant));
var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != current.CurrentItem.PrimaryId.Id)
current.ItemSetter(_items.Identify(current.Slot, newSetId, current.CurrentItem.SecondaryId, current.CurrentItem.Variant));
}
ImGui.SameLine();
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##type", ref type, 0, 0))
{
var newType = (WeaponType)Math.Clamp(type, 0, ushort.MaxValue);
if (newType.Id != current.CurrentItem.WeaponType.Id)
current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, newType, current.CurrentItem.Variant));
var newType = (SecondaryId)Math.Clamp(type, 0, ushort.MaxValue);
if (newType.Id != current.CurrentItem.SecondaryId.Id)
current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, newType, current.CurrentItem.Variant));
}
ImGui.SameLine();
@ -228,7 +228,8 @@ public class EquipmentDrawer
{
var newVariant = (Variant)Math.Clamp(variant, 0, byte.MaxValue);
if (newVariant.Id != current.CurrentItem.Variant.Id)
current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.ModelId, current.CurrentItem.WeaponType, newVariant));
current.ItemSetter(_items.Identify(current.Slot, current.CurrentItem.PrimaryId, current.CurrentItem.SecondaryId,
newVariant));
}
}
}
@ -249,13 +250,13 @@ public class EquipmentDrawer
/// <summary> Draw an input for armor that can set arbitrary values instead of choosing items. </summary>
private void DrawArmorArtisan(EquipDrawData data)
{
int setId = data.CurrentItem.ModelId.Id;
int setId = data.CurrentItem.PrimaryId.Id;
int variant = data.CurrentItem.Variant.Id;
ImGui.SetNextItemWidth(80 * ImGuiHelpers.GlobalScale);
if (ImGui.InputInt("##setId", ref setId, 0, 0))
{
var newSetId = (SetId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != data.CurrentItem.ModelId.Id)
var newSetId = (PrimaryId)Math.Clamp(setId, 0, ushort.MaxValue);
if (newSetId.Id != data.CurrentItem.PrimaryId.Id)
data.ItemSetter(_items.Identify(data.Slot, newSetId, data.CurrentItem.Variant));
}
@ -265,7 +266,7 @@ public class EquipmentDrawer
{
var newVariant = (byte)Math.Clamp(variant, 0, byte.MaxValue);
if (newVariant != data.CurrentItem.Variant)
data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.ModelId, newVariant));
data.ItemSetter(_items.Identify(data.Slot, data.CurrentItem.PrimaryId, newVariant));
}
}
@ -454,7 +455,7 @@ public class EquipmentDrawer
else if (combo.CustomVariant.Id > 0)
data.ItemSetter(_items.Identify(data.Slot, combo.CustomSetId, combo.CustomVariant));
if (!data.Locked && data.CurrentItem.ModelId.Id != 0)
if (!data.Locked && data.CurrentItem.PrimaryId.Id != 0)
{
if (clear || ImGui.IsItemClicked(ImGuiMouseButton.Right))
data.ItemSetter(ItemManager.NothingItem(data.Slot));

View file

@ -8,12 +8,12 @@ using Dalamud.Interface.Utility.Raii;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Equipment;
public sealed class GlamourerColorCombo(float _comboWidth, StainData _stains, FavoriteManager _favorites)
public sealed class GlamourerColorCombo(float _comboWidth, DictStain _stains, FavoriteManager _favorites)
: FilterComboColors(_comboWidth, CreateFunc(_stains, _favorites), Glamourer.Log)
{
protected override bool DrawSelectable(int globalIdx, bool selected)
@ -40,8 +40,9 @@ public sealed class GlamourerColorCombo(float _comboWidth, StainData _stains, Fa
return base.DrawSelectable(globalIdx, selected);
}
private static Func<IReadOnlyList<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>> CreateFunc(StainData stains,
private static Func<IReadOnlyList<KeyValuePair<byte, (string Name, uint Color, bool Gloss)>>> CreateFunc(DictStain stains,
FavoriteManager favorites)
=> () => stains.Data.Select(kvp => (kvp, favorites.Contains((StainId)kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp)
.Prepend(new KeyValuePair<byte, (string Name, uint Dye, bool Gloss)>(0, ("None", 0, false))).ToList();
=> () => stains.Select(kvp => (kvp, favorites.Contains(kvp.Key))).OrderBy(p => !p.Item2).Select(p => p.kvp)
.Prepend(new KeyValuePair<StainId, Stain>(Stain.None.RowIndex, Stain.None)).Select(kvp
=> new KeyValuePair<byte, (string, uint, bool)>(kvp.Key.Id, (kvp.Value.Name, kvp.Value.RgbaColor, kvp.Value.Gloss))).ToList();
}

View file

@ -23,8 +23,8 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
private ItemId _currentItem;
private float _innerWidth;
public SetId CustomSetId { get; private set; }
public Variant CustomVariant { get; private set; }
public PrimaryId CustomSetId { get; private set; }
public Variant CustomVariant { get; private set; }
public ItemCombo(IDataManager gameData, ItemManager items, EquipSlot slot, Logger log, FavoriteManager favorites)
: base(() => GetItems(favorites, items, slot), log)
@ -83,7 +83,7 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString());
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString());
protected override string ToString(EquipItem obj)
=> obj.Name;
@ -111,7 +111,7 @@ public sealed class ItemCombo : FilterComboCache<EquipItem>
private static IReadOnlyList<EquipItem> GetItems(FavoriteManager favorites, ItemManager items, EquipSlot slot)
{
var nothing = ItemManager.NothingItem(slot);
if (!items.ItemService.AwaitedService.TryGetValue(slot.ToEquipType(), out var list))
if (!items.ItemData.ByType.TryGetValue(slot.ToEquipType(), out var list))
return new[]
{
nothing,

View file

@ -60,12 +60,12 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
var ret = ImGui.Selectable(name, selected);
ImGui.SameLine();
using var color = ImRaii.PushColor(ImGuiCol.Text, 0xFF808080);
ImGuiUtil.RightAlign($"({obj.ModelId.Id}-{obj.WeaponType.Id}-{obj.Variant})");
ImGuiUtil.RightAlign($"({obj.PrimaryId.Id}-{obj.SecondaryId.Id}-{obj.Variant})");
return ret;
}
protected override bool IsVisible(int globalIndex, LowerString filter)
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].ModelId.Id.ToString());
=> base.IsVisible(globalIndex, filter) || filter.IsContained(Items[globalIndex].PrimaryId.Id.ToString());
protected override string ToString(EquipItem obj)
=> obj.Name;
@ -80,14 +80,14 @@ public sealed class WeaponCombo : FilterComboCache<EquipItem>
var enumerable = Array.Empty<EquipItem>().AsEnumerable();
foreach (var t in Enum.GetValues<FullEquipType>().Where(e => e.ToSlot() is EquipSlot.MainHand))
{
if (items.ItemService.AwaitedService.TryGetValue(t, out var l))
if (items.ItemData.ByType.TryGetValue(t, out var l))
enumerable = enumerable.Concat(l);
}
return enumerable.OrderBy(e => e.Name).ToList();
}
if (!items.ItemService.AwaitedService.TryGetValue(type, out var list))
if (!items.ItemData.ByType.TryGetValue(type, out var list))
return Array.Empty<EquipItem>();
if (type.AllowsNothing())

View file

@ -1,4 +1,5 @@
using System.Numerics;
using System;
using System.Numerics;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
@ -61,7 +62,7 @@ public class GenericPopupWindow : Window
private void DrawFestivalPopup()
{
var viewportSize = ImGui.GetWindowViewport().Size;
ImGui.SetNextWindowSize(new Vector2(viewportSize.X / 5, viewportSize.Y / 7));
ImGui.SetNextWindowSize(new Vector2(Math.Max(viewportSize.X / 5, 400), Math.Max(viewportSize.Y / 7, 150)));
ImGui.SetNextWindowPos(viewportSize / 2, ImGuiCond.Always, new Vector2(0.5f));
using var popup = ImRaii.Popup("FestivalPopup", ImGuiWindowFlags.Modal);
if (!popup)

View file

@ -10,6 +10,7 @@ using Glamourer.Gui.Tabs.ActorTab;
using Glamourer.Gui.Tabs.AutomationTab;
using Glamourer.Gui.Tabs.DebugTab;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Gui.Tabs.NpcTab;
using Glamourer.Gui.Tabs.UnlocksTab;
using ImGuiNET;
using OtterGui.Custom;
@ -29,6 +30,7 @@ public class MainWindow : Window, IDisposable
Automation = 4,
Unlocks = 5,
Messages = 6,
Npcs = 7,
}
private readonly Configuration _config;
@ -42,12 +44,14 @@ public class MainWindow : Window, IDisposable
public readonly DesignTab Designs;
public readonly AutomationTab Automation;
public readonly UnlocksTab Unlocks;
public readonly NpcTab Npcs;
public readonly MessagesTab Messages;
public TabType SelectTab = TabType.None;
public MainWindow(DalamudPluginInterface pi, Configuration config, SettingsTab settings, ActorTab actors, DesignTab designs,
DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar)
DebugTab debugTab, AutomationTab automation, UnlocksTab unlocks, TabSelected @event, MessagesTab messages, DesignQuickBar quickBar,
NpcTab npcs)
: base(GetLabel())
{
pi.UiBuilder.DisableGposeUiHide = true;
@ -65,17 +69,19 @@ public class MainWindow : Window, IDisposable
_event = @event;
Messages = messages;
_quickBar = quickBar;
Npcs = npcs;
_config = config;
_tabs = new ITab[]
{
_tabs =
[
settings,
actors,
designs,
automation,
unlocks,
npcs,
messages,
debugTab,
};
];
_event.Subscribe(OnTabSelected, TabSelected.Priority.MainWindow);
IsOpen = _config.OpenWindowAtStart;
}
@ -117,6 +123,7 @@ public class MainWindow : Window, IDisposable
TabType.Automation => Automation.Label,
TabType.Unlocks => Unlocks.Label,
TabType.Messages => Messages.Label,
TabType.Npcs => Npcs.Label,
_ => ReadOnlySpan<byte>.Empty,
};
@ -128,6 +135,7 @@ public class MainWindow : Window, IDisposable
if (label == Settings.Label) return TabType.Settings;
if (label == Automation.Label) return TabType.Automation;
if (label == Unlocks.Label) return TabType.Unlocks;
if (label == Npcs.Label) return TabType.Npcs;
if (label == Messages.Label) return TabType.Messages;
if (label == Debug.Label) return TabType.Debug;
// @formatter:on

View file

@ -7,7 +7,6 @@ using Glamourer.Interop;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.Api.Enums;
@ -76,7 +75,7 @@ public class PenumbraChangedItemTooltip : IDisposable
case EquipSlot.OffHand when !CanApplyWeapon(EquipSlot.OffHand, item):
break;
case EquipSlot.RFinger:
using (var tt = !openTooltip ? null : ImRaii.Tooltip())
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor (Right Finger).");
ImGui.TextUnformatted($"{prefix}Shift + Right-Click to apply to current actor (Left Finger).");
@ -92,7 +91,7 @@ public class PenumbraChangedItemTooltip : IDisposable
break;
default:
using (var tt = !openTooltip ? null : ImRaii.Tooltip())
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor.");
if (last.Valid)
@ -166,7 +165,7 @@ public class PenumbraChangedItemTooltip : IDisposable
{
case ChangedItemType.ItemOffhand:
case ChangedItemType.Item:
if (!_items.ItemService.AwaitedService.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
return;
CreateTooltip(item, "[Glamourer] ", false);
@ -177,7 +176,7 @@ public class PenumbraChangedItemTooltip : IDisposable
private bool CanApplyWeapon(EquipSlot slot, EquipItem item)
{
var main = _objects.Player.GetMainhand();
var mainItem = _items.Identify(slot, main.Set, main.Type, main.Variant);
var mainItem = _items.Identify(slot, main.Skeleton, main.Weapon, main.Variant);
if (slot == EquipSlot.MainHand)
return item.Type == mainItem.Type;
@ -197,7 +196,7 @@ public class PenumbraChangedItemTooltip : IDisposable
if (!Player(out var state))
return;
if (!_items.ItemService.AwaitedService.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
if (!_items.ItemData.TryGetValue(id, type is ChangedItemType.Item ? EquipSlot.MainHand : EquipSlot.OffHand, out var item))
return;
ApplyItem(state, item);

View file

@ -6,29 +6,36 @@ using Dalamud.Interface.Internal.Notifications;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Actors;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.ActorTab;
public class ActorPanel(ActorSelector _selector, StateManager _stateManager, CustomizationDrawer _customizationDrawer,
EquipmentDrawer _equipmentDrawer, IdentifierService _identification, AutoDesignApplier _autoDesignApplier,
Configuration _config, DesignConverter _converter, ObjectManager _objects, DesignManager _designManager, ImportService _importService,
ICondition _conditions)
public class ActorPanel(
ActorSelector _selector,
StateManager _stateManager,
CustomizationDrawer _customizationDrawer,
EquipmentDrawer _equipmentDrawer,
AutoDesignApplier _autoDesignApplier,
Configuration _config,
DesignConverter _converter,
ObjectManager _objects,
DesignManager _designManager,
ImportService _importService,
ICondition _conditions,
DictModelChara _modelChara)
{
private ActorIdentifier _identifier;
private string _actorName = string.Empty;
@ -154,7 +161,7 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
}
var mainhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.MainHand);
var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand);
var offhand = EquipDrawData.FromState(_stateManager, _state, EquipSlot.OffHand);
_equipmentDrawer.DrawWeapons(mainhand, offhand, GameMain.IsInGPose());
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
@ -164,21 +171,21 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
private void DrawEquipmentMetaToggles()
{
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.HatState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Head, _stateManager, _state!));
}
ImGui.SameLine();
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.VisorState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.Body, _stateManager, _state!));
}
ImGui.SameLine();
using (var _ = ImRaii.Group())
using (_ = ImRaii.Group())
{
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromState(ActorState.MetaIndex.WeaponState, _stateManager, _state!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromState(CrestFlag.OffHand, _stateManager, _state!));
@ -187,10 +194,10 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
private void DrawMonsterPanel()
{
var names = _identification.AwaitedService.ModelCharaNames(_state!.ModelData.ModelId);
var names = _modelChara[_state!.ModelData.ModelId];
var turnHuman = ImGui.Button("Turn Human");
ImGui.Separator();
using (var box = ImRaii.ListBox("##MonsterList",
using (_ = ImRaii.ListBox("##MonsterList",
new Vector2(ImGui.GetContentRegionAvail().X, 10 * ImGui.GetTextLineHeightWithSpacing())))
{
if (names.Count == 0)
@ -202,14 +209,14 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
ImGui.Separator();
ImGui.TextUnformatted("Customization Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
using (_ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.Customize.Data)
foreach (var b in _state.ModelData.Customize)
{
using (var g = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
ImGui.TextUnformatted($" {b.Value:X2}");
ImGui.TextUnformatted($"{b.Value,3}");
}
ImGui.SameLine();
@ -223,11 +230,11 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
ImGui.Separator();
ImGui.TextUnformatted("Equipment Data");
using (var font = ImRaii.PushFont(UiBuilder.MonoFont))
using (_ = ImRaii.PushFont(UiBuilder.MonoFont))
{
foreach (var b in _state.ModelData.GetEquipmentBytes())
{
using (var g = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted($" {b:X2}");
ImGui.TextUnformatted($"{b,3}");
@ -289,15 +296,15 @@ public class ActorPanel(ActorSelector _selector, StateManager _stateManager, Cus
BorderColor = ColorId.ActorUnavailable.Value(),
};
private string _newName = string.Empty;
private DesignBase? _newDesign = null;
private string _newName = string.Empty;
private DesignBase? _newDesign;
private void SaveDesignOpen()
{
ImGui.OpenPopup("Save as Design");
_newName = _state!.Identifier.ToName();
_newName = _state!.Identifier.ToName();
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
_newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest);
_newDesign = _converter.Convert(_state, applyGear, applyCustomize, applyCrest);
}
private void SaveDesignDrawPopup()

View file

@ -17,11 +17,11 @@ public class ActorSelector
{
private readonly EphemeralConfig _config;
private readonly ObjectManager _objects;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private ActorIdentifier _identifier = ActorIdentifier.Invalid;
public ActorSelector(ObjectManager objects, ActorService actors, EphemeralConfig config)
public ActorSelector(ObjectManager objects, ActorManager actors, EphemeralConfig config)
{
_objects = objects;
_actors = actors;
@ -93,7 +93,7 @@ public class ActorSelector
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.UserCircle.ToIconString(), buttonWidth
, "Select the local player character.", !_objects.Player, true))
_identifier = _objects.Player.GetIdentifier(_actors.AwaitedService);
_identifier = _objects.Player.GetIdentifier(_actors);
ImGui.SameLine();
var (id, data) = _objects.TargetData;

View file

@ -3,23 +3,23 @@ using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Enums;
using Dalamud.Utility;
using Glamourer.Services;
using ImGuiNET;
using OtterGui.Custom;
using OtterGui.Log;
using OtterGui.Widgets;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
namespace Glamourer.Gui.Tabs.AutomationTab;
public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>
public sealed class HumanNpcCombo(
string label,
DictModelChara modelCharaDict,
DictBNpcNames bNpcNames,
DictBNpc bNpcs,
HumanModelList humans,
Logger log)
: FilterComboCache<(string Name, ObjectKind Kind, uint[] Ids)>(() => CreateList(modelCharaDict, bNpcNames, bNpcs, humans), log)
{
private readonly string _label;
public HumanNpcCombo(string label, IdentifierService service, HumanModelList humans, Logger log)
: base(() => CreateList(service, humans), log)
=> _label = label;
protected override string ToString((string Name, ObjectKind Kind, uint[] Ids) obj)
=> obj.Name;
@ -36,7 +36,8 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki
}
public bool Draw(float width)
=> Draw(_label, CurrentSelection.Name.IsNullOrEmpty() ? "Human Non-Player-Characters..." : CurrentSelection.Name, string.Empty, width, ImGui.GetTextLineHeightWithSpacing());
=> Draw(label, CurrentSelection.Name.IsNullOrEmpty() ? "Human Non-Player-Characters..." : CurrentSelection.Name, string.Empty, width,
ImGui.GetTextLineHeightWithSpacing());
/// <summary> Compare strings in a way that letters and numbers are sorted before any special symbols. </summary>
@ -61,15 +62,16 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki
}
}
private static IReadOnlyList<(string Name, ObjectKind Kind, uint[] Ids)> CreateList(IdentifierService service, HumanModelList humans)
private static IReadOnlyList<(string Name, ObjectKind Kind, uint[] Ids)> CreateList(DictModelChara modelCharaDict, DictBNpcNames bNpcNames,
DictBNpc bNpcs, HumanModelList humans)
{
var ret = new List<(string Name, ObjectKind Kind, uint Id)>(1024);
for (var modelChara = 0u; modelChara < service.AwaitedService.NumModelChara; ++modelChara)
for (var modelChara = 0u; modelChara < modelCharaDict.Count; ++modelChara)
{
if (!humans.IsHuman(modelChara))
continue;
var list = service.AwaitedService.ModelCharaNames(modelChara);
var list = modelCharaDict[modelChara];
if (list.Count == 0)
continue;
@ -78,8 +80,8 @@ public sealed class HumanNpcCombo : FilterComboCache<(string Name, ObjectKind Ki
switch (kind)
{
case ObjectKind.BattleNpc:
var nameIds = service.AwaitedService.GetBnpcNames(id);
ret.AddRange(nameIds.Select(nameId => (service.AwaitedService.Name(ObjectKind.BattleNpc, nameId), kind, nameId.Id)));
var nameIds = bNpcNames[id];
ret.AddRange(nameIds.Select(nameId => (bNpcs[nameId], kind, nameId.Id)));
break;
case ObjectKind.EventNpc:
ret.Add((name, kind, id));

View file

@ -1,9 +1,9 @@
using Dalamud.Game.ClientState.Objects.Enums;
using Glamourer.Services;
using ImGuiNET;
using OtterGui.Custom;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Gui;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Gui.Tabs.AutomationTab;
@ -12,7 +12,7 @@ public class IdentifierDrawer
{
private readonly WorldCombo _worldCombo;
private readonly HumanNpcCombo _humanNpcCombo;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private string _characterName = string.Empty;
@ -21,11 +21,12 @@ public class IdentifierDrawer
public ActorIdentifier RetainerIdentifier { get; private set; } = ActorIdentifier.Invalid;
public ActorIdentifier MannequinIdentifier { get; private set; } = ActorIdentifier.Invalid;
public IdentifierDrawer(ActorService actors, IdentifierService identifier, HumanModelList humans)
public IdentifierDrawer(ActorManager actors, DictWorld dictWorld, DictModelChara dictModelChara, DictBNpcNames bNpcNames, DictBNpc bNpc,
HumanModelList humans)
{
_actors = actors;
_worldCombo = new WorldCombo(actors.AwaitedService.Data.Worlds, Glamourer.Log);
_humanNpcCombo = new HumanNpcCombo("##npcs", identifier, humans, Glamourer.Log);
_worldCombo = new WorldCombo(dictWorld, Glamourer.Log);
_humanNpcCombo = new HumanNpcCombo("##npcs", dictModelChara, bNpcNames, bNpc, humans, Glamourer.Log);
}
public void DrawName(float width)
@ -63,13 +64,13 @@ public class IdentifierDrawer
{
if (ByteString.FromString(_characterName, out var byteName))
{
PlayerIdentifier = _actors.AwaitedService.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key);
RetainerIdentifier = _actors.AwaitedService.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell);
MannequinIdentifier = _actors.AwaitedService.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin);
PlayerIdentifier = _actors.CreatePlayer(byteName, _worldCombo.CurrentSelection.Key);
RetainerIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Bell);
MannequinIdentifier = _actors.CreateRetainer(byteName, ActorIdentifier.RetainerType.Mannequin);
}
NpcIdentifier = _humanNpcCombo.CurrentSelection.Kind is ObjectKind.EventNpc or ObjectKind.BattleNpc
? _actors.AwaitedService.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0])
? _actors.CreateNpc(_humanNpcCombo.CurrentSelection.Kind, _humanNpcCombo.CurrentSelection.Ids[0])
: ActorIdentifier.Invalid;
}
}

View file

@ -6,10 +6,8 @@ using System.Text;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
@ -17,44 +15,29 @@ using OtterGui.Log;
using OtterGui.Raii;
using OtterGui.Widgets;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Action = System.Action;
using CustomizeIndex = Glamourer.Customization.CustomizeIndex;
namespace Glamourer.Gui.Tabs.AutomationTab;
public class SetPanel
public class SetPanel(
SetSelector _selector,
AutoDesignManager _manager,
JobService _jobs,
ItemUnlockManager _itemUnlocks,
RevertDesignCombo _designCombo,
CustomizeUnlockManager _customizeUnlocks,
CustomizeService _customizations,
IdentifierDrawer _identifierDrawer,
Configuration _config)
{
private readonly AutoDesignManager _manager;
private readonly SetSelector _selector;
private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly CustomizationService _customizations;
private readonly Configuration _config;
private readonly RevertDesignCombo _designCombo;
private readonly JobGroupCombo _jobGroupCombo;
private readonly IdentifierDrawer _identifierDrawer;
private readonly JobGroupCombo _jobGroupCombo = new(_manager, _jobs, Glamourer.Log);
private string? _tempName;
private int _dragIndex = -1;
private Action? _endAction;
public SetPanel(SetSelector selector, AutoDesignManager manager, JobService jobs, ItemUnlockManager itemUnlocks,
RevertDesignCombo designCombo,
CustomizeUnlockManager customizeUnlocks, CustomizationService customizations, IdentifierDrawer identifierDrawer, Configuration config)
{
_selector = selector;
_manager = manager;
_itemUnlocks = itemUnlocks;
_customizeUnlocks = customizeUnlocks;
_customizations = customizations;
_identifierDrawer = identifierDrawer;
_config = config;
_designCombo = designCombo;
_jobGroupCombo = new JobGroupCombo(manager, jobs, Glamourer.Log);
}
private AutoDesignSet Selection
=> _selector.Selection!;
@ -77,7 +60,7 @@ public class SetPanel
var spacing = ImGui.GetStyle().ItemInnerSpacing with { Y = ImGui.GetStyle().ItemSpacing.Y };
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var enabled = Selection.Enabled;
if (ImGui.Checkbox("##Enabled", ref enabled))
@ -87,7 +70,7 @@ public class SetPanel
}
ImGui.SameLine();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var useGame = _selector.Selection!.BaseState is AutoDesignSet.Base.Game;
if (ImGui.Checkbox("##gameState", ref useGame))
@ -98,7 +81,7 @@ public class SetPanel
}
ImGui.SameLine();
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing))
{
var editing = _config.ShowAutomationSetEditing;
if (ImGui.Checkbox("##Show Editing", ref editing))
@ -230,7 +213,7 @@ public class SetPanel
if (_config.ShowUnlockedItemWarnings)
{
ImGui.TableNextColumn();
DrawWarnings(design, idx);
DrawWarnings(design);
}
}
@ -278,7 +261,7 @@ public class SetPanel
}
}
private void DrawWarnings(AutoDesign design, int idx)
private void DrawWarnings(AutoDesign design)
{
if (design.Revert)
return;
@ -301,27 +284,6 @@ public class SetPanel
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(2 * ImGuiHelpers.GlobalScale, 0));
static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
if (sb.Length > 0)
{
sb.Append(suffix);
using (var font = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color);
}
ImGuiUtil.HoverTooltip(sb.ToString());
}
else
{
ImGuiUtil.DrawTextButton(string.Empty, size, 0);
ImGuiUtil.HoverTooltip(good);
}
}
var tt = _config.UnlockedItemMode
? "\nThese items will be skipped when applied automatically.\n\nTo change this, disable the Obtained Item Mode setting."
: string.Empty;
@ -333,7 +295,7 @@ public class SetPanel
if (!design.Design.DesignData.IsHuman)
sb.AppendLine("The base model id can not be changed automatically to something non-human.");
var set = _customizations.AwaitedService.GetList(customize.Clan, customize.Gender);
var set = _customizations.Manager.GetSet(customize.Clan, customize.Gender);
foreach (var type in CustomizationExtensions.All)
{
var flag = type.ToFlag();
@ -355,6 +317,27 @@ public class SetPanel
: string.Empty;
DrawWarning(sb2, _config.UnlockedItemMode ? 0xA03030F0 : 0x0, size, tt, "All customizations to be applied are unlocked.");
ImGui.SameLine();
return;
static void DrawWarning(StringBuilder sb, uint color, Vector2 size, string suffix, string good)
{
using var style = ImRaii.PushStyle(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
if (sb.Length > 0)
{
sb.Append(suffix);
using (_ = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, color);
}
ImGuiUtil.HoverTooltip(sb.ToString());
}
else
{
ImGuiUtil.DrawTextButton(string.Empty, size, 0);
ImGuiUtil.HoverTooltip(good);
}
}
}
private void DrawDragDrop(AutoDesignSet set, int index)
@ -394,7 +377,7 @@ public class SetPanel
var newType = design.ApplicationType;
var newTypeInt = (uint)newType;
style.Push(ImGuiStyleVar.FrameBorderSize, ImGuiHelpers.GlobalScale);
using (var c = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()))
using (_ = ImRaii.PushColor(ImGuiCol.Border, ColorId.FolderLine.Value()))
{
if (ImGui.CheckboxFlags("##all", ref newTypeInt, (uint)AutoDesign.Type.All))
newType = (AutoDesign.Type)newTypeInt;

View file

@ -7,11 +7,11 @@ using Dalamud.Interface.Utility;
using Glamourer.Automation;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Actors;
using Penumbra.String;
using ImGuiClip = OtterGui.ImGuiClip;
@ -22,9 +22,9 @@ public class SetSelector : IDisposable
private readonly Configuration _config;
private readonly AutoDesignManager _manager;
private readonly AutomationChanged _event;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private readonly ObjectManager _objects;
private readonly List<(AutoDesignSet, int)> _list = new();
private readonly List<(AutoDesignSet, int)> _list = [];
public AutoDesignSet? Selection { get; private set; }
public int SelectionIndex { get; private set; } = -1;
@ -44,7 +44,7 @@ public class SetSelector : IDisposable
internal int _dragDesignIndex = -1;
public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorService actors, ObjectManager objects)
public SetSelector(AutoDesignManager manager, AutomationChanged @event, Configuration config, ActorManager actors, ObjectManager objects)
{
_manager = manager;
_event = @event;
@ -289,9 +289,9 @@ public class SetSelector : IDisposable
private void NewSetButton(Vector2 size)
{
var id = _actors.AwaitedService.GetCurrentPlayer();
var id = _actors.GetCurrentPlayer();
if (!id.IsValid)
id = _actors.AwaitedService.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue);
id = _actors.CreatePlayer(ByteString.FromSpanUnsafe("New Design"u8, true, false, true), ushort.MaxValue);
if (ImGuiUtil.DrawDisabledButton(FontAwesomeIcon.Plus.ToIconString(), size,
$"Create a new Automatic Design Set for {id}. The associated player can be changed later.", !id.IsValid, true))
_manager.AddDesignSet("New Design", id);

View file

@ -2,7 +2,7 @@
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Interop;
@ -12,10 +12,11 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IDebugTabTree
public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer
{
public string Label
=> $"Active Actors ({_stateManager.Count})###Active Actors";
@ -69,7 +70,7 @@ public class ActiveStatePanel(StateManager _stateManager, ObjectManager _objectM
static string ItemString(in DesignData data, EquipSlot slot)
{
var item = data.Item(slot);
return $"{item.Name} ({item.ModelId.Id}{(item.WeaponType != 0 ? $"-{item.WeaponType.Id}" : string.Empty)}-{item.Variant})";
return $"{item.Name} ({item.PrimaryId.Id}{(item.SecondaryId != 0 ? $"-{item.SecondaryId.Id}" : string.Empty)}-{item.Variant})";
}
PrintRow("Model ID", state.BaseData.ModelId, state.ModelData.ModelId, state[ActorState.MetaIndex.ModelId]);

View file

@ -1,73 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public class ActorServicePanel(ActorService _actors, ItemManager _items) : IDebugTabTree
{
public string Label
=> "Actor Service";
public bool Disabled
=> !_actors.Valid;
private string _bnpcFilter = string.Empty;
private string _enpcFilter = string.Empty;
private string _companionFilter = string.Empty;
private string _mountFilter = string.Empty;
private string _ornamentFilter = string.Empty;
private string _worldFilter = string.Empty;
public void Draw()
{
DrawBnpcTable();
DebugTab.DrawNameTable("ENPCs", ref _enpcFilter, _actors.AwaitedService.Data.ENpcs.Select(kvp => (kvp.Key, kvp.Value)));
DebugTab.DrawNameTable("Companions", ref _companionFilter, _actors.AwaitedService.Data.Companions.Select(kvp => (kvp.Key, kvp.Value)));
DebugTab.DrawNameTable("Mounts", ref _mountFilter, _actors.AwaitedService.Data.Mounts.Select(kvp => (kvp.Key, kvp.Value)));
DebugTab.DrawNameTable("Ornaments", ref _ornamentFilter, _actors.AwaitedService.Data.Ornaments.Select(kvp => (kvp.Key, kvp.Value)));
DebugTab.DrawNameTable("Worlds", ref _worldFilter, _actors.AwaitedService.Data.Worlds.Select(kvp => ((uint)kvp.Key, kvp.Value)));
}
private void DrawBnpcTable()
{
using var _ = ImRaii.PushId(1);
using var tree = ImRaii.TreeNode("BNPCs");
if (!tree)
return;
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _bnpcFilter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = ImRaii.Table("##table", 3, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
new Vector2(-1, 10 * height));
if (!table)
return;
if (resetScroll)
ImGui.SetScrollY(0);
ImGui.TableSetupColumn("1", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("3", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
ImGui.TableNextRow();
var data = _actors.AwaitedService.Data.BNpcs.Select(kvp => (kvp.Key, kvp.Key.ToString("D5"), kvp.Value));
var remainder = ImGuiClip.FilteredClippedDraw(data, skips,
p => p.Item2.Contains(_bnpcFilter) || p.Item3.Contains(_bnpcFilter, StringComparison.OrdinalIgnoreCase),
p =>
{
ImGuiUtil.DrawTableColumn(p.Item2);
ImGuiUtil.DrawTableColumn(p.Item3);
var bnpcs = _items.IdentifierService.AwaitedService.GetBnpcsFromName(p.Item1);
ImGuiUtil.DrawTableColumn(string.Join(", ", bnpcs.Select(b => b.Id.ToString())));
});
ImGuiClip.DrawEndDummy(remainder, height);
}
}

View file

@ -2,10 +2,11 @@
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IDebugTabTree
public class AutoDesignPanel(AutoDesignManager _autoDesignManager) : IGameDataDrawer
{
public string Label
=> "Auto Designs";

View file

@ -1,36 +1,35 @@
using System;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class CustomizationServicePanel(CustomizationService _customization) : IDebugTabTree
public class CustomizationServicePanel(CustomizeService customize) : IGameDataDrawer
{
public string Label
=> "Customization Service";
public bool Disabled
=> !_customization.Valid;
=> !customize.Finished;
public void Draw()
{
foreach (var clan in _customization.AwaitedService.Clans)
foreach (var (clan, gender) in CustomizeManager.AllSets())
{
foreach (var gender in _customization.AwaitedService.Genders)
{
var set = _customization.AwaitedService.GetList(clan, gender);
DrawCustomizationInfo(set);
DrawNpcCustomizationInfo(set);
}
var set = customize.Manager.GetSet(clan, gender);
DrawCustomizationInfo(set);
DrawNpcCustomizationInfo(set);
}
}
private void DrawCustomizationInfo(CustomizationSet set)
private void DrawCustomizationInfo(CustomizeSet set)
{
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender}");
using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender}");
if (!tree)
return;
@ -48,9 +47,9 @@ public class CustomizationServicePanel(CustomizationService _customization) : ID
}
}
private void DrawNpcCustomizationInfo(CustomizationSet set)
private void DrawNpcCustomizationInfo(CustomizeSet set)
{
using var tree = ImRaii.TreeNode($"{_customization.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)");
using var tree = ImRaii.TreeNode($"{customize.ClanName(set.Clan, set.Gender)} {set.Gender} (NPC Options)");
if (!tree)
return;

View file

@ -1,14 +1,15 @@
using System;
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class CustomizationUnlockPanel(CustomizeUnlockManager _customizeUnlocks) : IDebugTabTree
public class CustomizationUnlockPanel(CustomizeUnlockManager _customizeUnlocks) : IGameDataDrawer
{
public string Label
=> "Customizations";

View file

@ -1,14 +1,14 @@
using System.IO;
using System.Numerics;
using Glamourer.Customization;
using Glamourer.Interop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Files;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class DatFilePanel(ImportService _importService) : IDebugTabTree
public class DatFilePanel(ImportService _importService) : IGameDataDrawer
{
public string Label
=> "Character Dat File";
@ -35,7 +35,7 @@ public class DatFilePanel(ImportService _importService) : IDebugTabTree
ImGui.TextUnformatted(_datFile.Value.Version.ToString());
ImGui.TextUnformatted(_datFile.Value.Time.LocalDateTime.ToString("g"));
ImGui.TextUnformatted(_datFile.Value.Voice.ToString());
ImGui.TextUnformatted(_datFile.Value.Customize.Data.ToString());
ImGui.TextUnformatted(_datFile.Value.Customize.ToString());
ImGui.TextUnformatted(_datFile.Value.Description);
}
}

View file

@ -1,20 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Services;
using OtterGui.Widgets;
using ImGuiClip = Dalamud.Interface.Utility.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public unsafe class DebugTab(IServiceProvider _provider) : ITab
public unsafe class DebugTab(ServiceManager manager) : ITab
{
private readonly Configuration _config = _provider.GetRequiredService<Configuration>();
private readonly Configuration _config = manager.GetService<Configuration>();
public bool IsVisible
=> _config.DebugMode;
@ -24,11 +18,11 @@ public unsafe class DebugTab(IServiceProvider _provider) : ITab
private readonly DebugTabHeader[] _headers =
[
DebugTabHeader.CreateInterop(_provider),
DebugTabHeader.CreateGameData(_provider),
DebugTabHeader.CreateDesigns(_provider),
DebugTabHeader.CreateState(_provider),
DebugTabHeader.CreateUnlocks(_provider),
DebugTabHeader.CreateInterop(manager.Provider!),
DebugTabHeader.CreateGameData(manager.Provider!),
DebugTabHeader.CreateDesigns(manager.Provider!),
DebugTabHeader.CreateState(manager.Provider!),
DebugTabHeader.CreateUnlocks(manager.Provider!),
];
public void DrawContent()
@ -37,55 +31,12 @@ public unsafe class DebugTab(IServiceProvider _provider) : ITab
if (!child)
return;
if (ImGui.CollapsingHeader("General"))
{
manager.Timers.Draw("Timers");
}
foreach (var header in _headers)
header.Draw();
}
public static void DrawInputModelSet(bool withWeapon, ref int setId, ref int secondaryId, ref int variant)
{
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##SetId", ref setId, 0, 0);
if (withWeapon)
{
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##TypeId", ref secondaryId, 0, 0);
}
ImGui.SameLine();
ImGui.SetNextItemWidth(100 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##Variant", ref variant, 0, 0);
}
public static void DrawNameTable(string label, ref string filter, IEnumerable<(uint, string)> names)
{
using var _ = ImRaii.PushId(label);
using var tree = ImRaii.TreeNode(label);
if (!tree)
return;
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref filter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = ImRaii.Table("##table", 2, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter,
new Vector2(-1, 10 * height));
if (!table)
return;
if (resetScroll)
ImGui.SetScrollY(0);
ImGui.TableSetupColumn("1", ImGuiTableColumnFlags.WidthFixed, 50 * ImGuiHelpers.GlobalScale);
ImGui.TableSetupColumn("2", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
ImGui.TableNextColumn();
var f = filter;
var remainder = ImGuiClip.FilteredClippedDraw(names.Select(p => (p.Item1.ToString("D5"), p.Item2)), skips,
p => p.Item1.Contains(f) || p.Item2.Contains(f, StringComparison.OrdinalIgnoreCase),
p =>
{
ImGuiUtil.DrawTableColumn(p.Item1);
ImGuiUtil.DrawTableColumn(p.Item2);
});
ImGuiClip.DrawEndDummy(remainder, height);
}
}

View file

@ -3,21 +3,14 @@ using System.Collections.Generic;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using OtterGui.Raii;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public interface IDebugTabTree
public class DebugTabHeader(string label, params IGameDataDrawer[] subTrees)
{
public string Label { get; }
public void Draw();
public bool Disabled { get; }
}
public class DebugTabHeader(string label, params IDebugTabTree[] subTrees)
{
public string Label { get; } = label;
public IReadOnlyList<IDebugTabTree> SubTrees { get; } = subTrees;
public string Label { get; } = label;
public IReadOnlyList<IGameDataDrawer> SubTrees { get; } = subTrees;
public void Draw()
{
@ -26,14 +19,13 @@ public class DebugTabHeader(string label, params IDebugTabTree[] subTrees)
foreach (var subTree in SubTrees)
{
using (var disabled = ImRaii.Disabled(subTree.Disabled))
using var disabled = ImRaii.Disabled(subTree.Disabled);
using var tree = ImRaii.TreeNode(subTree.Label);
if (tree)
{
using var tree = ImRaii.TreeNode(subTree.Label);
if (!tree)
continue;
disabled.Dispose();
subTree.Draw();
}
subTree.Draw();
}
}
@ -52,13 +44,15 @@ public class DebugTabHeader(string label, params IDebugTabTree[] subTrees)
=> new
(
"Game Data",
provider.GetRequiredService<IdentifierPanel>(),
provider.GetRequiredService<RestrictedGearPanel>(),
provider.GetRequiredService<ActorServicePanel>(),
provider.GetRequiredService<ItemManagerPanel>(),
provider.GetRequiredService<StainPanel>(),
provider.GetRequiredService<DataServiceDiagnosticsDrawer>(),
provider.GetRequiredService<IdentificationDrawer>(),
provider.GetRequiredService<RestrictedGearDrawer>(),
provider.GetRequiredService<ActorDataDrawer>(),
provider.GetRequiredService<ItemDataDrawer>(),
provider.GetRequiredService<DictStainDrawer>(),
provider.GetRequiredService<CustomizationServicePanel>(),
provider.GetRequiredService<JobPanel>(),
provider.GetRequiredService<DictJobDrawer>(),
provider.GetRequiredService<DictJobGroupDrawer>(),
provider.GetRequiredService<NpcAppearancePanel>()
);

View file

@ -8,10 +8,11 @@ using ImGuiNET;
using Newtonsoft.Json.Linq;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class DesignConverterPanel(DesignConverter _designConverter) : IDebugTabTree
public class DesignConverterPanel(DesignConverter _designConverter) : IGameDataDrawer
{
public string Label
=> "Design Converter";

View file

@ -1,17 +1,16 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _designFileSystem) : IDebugTabTree
public class DesignManagerPanel(DesignManager _designManager, DesignFileSystem _designFileSystem) : IGameDataDrawer
{
public string Label
=> $"Design Manager ({_designManager.Designs.Count} Designs)###Design Manager";

View file

@ -1,19 +1,18 @@
using System;
using System.Linq;
using Dalamud.Interface;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Data;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDebugTabTree
public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IGameDataDrawer
{
public string Label
=> "Base64 Design Tester";
@ -85,7 +84,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
DrawDesignData(_parse64);
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TextUnformatted(_base64);
using (var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
using (_ = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemSpacing with { X = 0 }))
{
foreach (var (c1, c2) in _restore.Zip(_base64))
{
@ -99,7 +98,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
foreach (var ((b1, b2), idx) in _base64Bytes.Zip(_restoreBytes).WithIndex())
{
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b1.ToString("X2"));
@ -121,7 +120,7 @@ public class DesignTesterPanel(ItemManager _items, HumanModelList _humans) : IDe
using var font = ImRaii.PushFont(UiBuilder.MonoFont);
foreach (var (b, idx) in _base64Bytes.WithIndex())
{
using (var group = ImRaii.Group())
using (_ = ImRaii.Group())
{
ImGui.TextUnformatted(idx.ToString("D2"));
ImGui.TextUnformatted(b.ToString("X2"));

View file

@ -1,9 +1,10 @@
using Glamourer.State;
using ImGuiNET;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class FunPanel(FunModule _funModule, Configuration _config) : IDebugTabTree
public class FunPanel(FunModule _funModule, Configuration _config) : IGameDataDrawer
{
public string Label
=> "Fun Module";

View file

@ -1,61 +0,0 @@
using System.Linq;
using Dalamud.Interface.Utility;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.DebugTab;
public class IdentifierPanel(ItemManager _items) : IDebugTabTree
{
public string Label
=> "Identifier Service";
public bool Disabled
=> !_items.IdentifierService.Valid;
private string _gamePath = string.Empty;
private int _setId;
private int _secondaryId;
private int _variant;
public void Draw()
{
static void Text(string text)
{
if (text.Length > 0)
ImGui.TextUnformatted(text);
}
ImGui.TextUnformatted("Parse Game Path");
ImGui.SameLine();
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
ImGui.InputTextWithHint("##gamePath", "Enter game path...", ref _gamePath, 256);
var fileInfo = _items.IdentifierService.AwaitedService.GamePathParser.GetFileInfo(_gamePath);
ImGui.TextUnformatted(
$"{fileInfo.ObjectType} {fileInfo.EquipSlot} {fileInfo.PrimaryId} {fileInfo.SecondaryId} {fileInfo.Variant} {fileInfo.BodySlot} {fileInfo.CustomizationType}");
Text(string.Join("\n", _items.IdentifierService.AwaitedService.Identify(_gamePath).Keys));
ImGui.Separator();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Identify Model");
ImGui.SameLine();
DebugTab.DrawInputModelSet(true, ref _setId, ref _secondaryId, ref _variant);
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var identified = _items.Identify(slot, (SetId)_setId, (Variant)_variant);
Text(identified.Name);
ImGuiUtil.HoverTooltip(string.Join("\n",
_items.IdentifierService.AwaitedService.Identify((SetId)_setId, (Variant)_variant, slot)
.Select(i => $"{i.Name} {i.Id} {i.ItemId} {i.IconId}")));
}
var weapon = _items.Identify(EquipSlot.MainHand, (SetId)_setId, (WeaponType)_secondaryId, (Variant)_variant);
Text(weapon.Name);
ImGuiUtil.HoverTooltip(string.Join("\n",
_items.IdentifierService.AwaitedService.Identify((SetId)_setId, (WeaponType)_secondaryId, (Variant)_variant, EquipSlot.MainHand)));
}
}

View file

@ -2,10 +2,11 @@
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public unsafe class InventoryPanel : IDebugTabTree
public unsafe class InventoryPanel : IGameDataDrawer
{
public string Label
=> "Inventory";

View file

@ -1,16 +1,19 @@
using Dalamud.Game.ClientState.Objects.Types;
using System;
using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Plugin;
using Glamourer.Api;
using Glamourer.Interop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using System;
using System.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui;
using Penumbra.GameData.Gui.Debug;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.DebugTab;
public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManager _objectManager) : IDebugTabTree
public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManager _objectManager) : IGameDataDrawer
{
public string Label
=> "IPC Tester";
@ -18,17 +21,23 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
public bool Disabled
=> false;
private int _gameObjectIndex;
private string _gameObjectName = string.Empty;
private string _base64Apply = string.Empty;
private string _designIdentifier = string.Empty;
private int _gameObjectIndex;
private CustomItemId _customItemId;
private StainId _stainId;
private EquipSlot _slot = EquipSlot.Head;
private string _gameObjectName = string.Empty;
private string _base64Apply = string.Empty;
private string _designIdentifier = string.Empty;
private GlamourerIpc.GlamourerErrorCode _setItemEc;
private GlamourerIpc.GlamourerErrorCode _setItemByActorNameEc;
public void Draw()
public unsafe void Draw()
{
ImGui.InputInt("Game Object Index", ref _gameObjectIndex, 0, 0);
ImGui.InputTextWithHint("##gameObject", "Character Name...", ref _gameObjectName, 64);
ImGui.InputTextWithHint("##base64", "Design Base64...", ref _base64Apply, 2047);
ImGui.InputTextWithHint("##identifier", "Design identifier...", ref _designIdentifier, 36);
ImGui.InputTextWithHint("##gameObject", "Character Name...", ref _gameObjectName, 64);
ImGui.InputTextWithHint("##base64", "Design Base64...", ref _base64Apply, 2047);
ImGui.InputTextWithHint("##identifier", "Design identifier...", ref _designIdentifier, 36);
DrawItemInput();
using var table = ImRaii.Table("##ipc", 2, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
@ -54,15 +63,6 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
else
ImGui.TextUnformatted("Error");
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetDesignList);
ImGui.TableNextColumn();
var designList = GlamourerIpc.GetDesignListSubscriber(_pluginInterface)
.Invoke();
if (designList != null)
ImGuiUtil.CopyOnClickSelectable(string.Join(", ", designList));
else
ImGui.TextUnformatted("Error");
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelRevert);
ImGui.TableNextColumn();
if (ImGui.Button("Revert##Name"))
@ -106,6 +106,19 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
GlamourerIpc.ApplyOnlyCustomizationToCharacterSubscriber(_pluginInterface)
.Invoke(_base64Apply, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuid);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##ByGuidName") && Guid.TryParse(_designIdentifier, out var guid1))
GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(guid1, _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##ByGuidCharacter") && Guid.TryParse(_designIdentifier, out var guid2))
GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface)
.Invoke(guid2, _objectManager.Objects[_gameObjectIndex] as Character);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelUnlock);
ImGui.TableNextColumn();
if (ImGui.Button("Unlock##CustomizeCharacter"))
@ -118,15 +131,52 @@ public class IpcTesterPanel(DalamudPluginInterface _pluginInterface, ObjectManag
GlamourerIpc.RevertToAutomationCharacterSubscriber(_pluginInterface)
.Invoke(_objectManager.Objects[_gameObjectIndex] as Character, 1337);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuid);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##ByGuidName"))
GlamourerIpc.ApplyByGuidSubscriber(_pluginInterface).Invoke(Guid.Parse(_designIdentifier), _gameObjectName);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelApplyByGuidToCharacter);
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelGetDesignList);
ImGui.TableNextColumn();
if (ImGui.Button("Apply##ByGuidCharacter"))
GlamourerIpc.ApplyByGuidToCharacterSubscriber(_pluginInterface)
.Invoke(Guid.Parse(_designIdentifier), _objectManager.Objects[_gameObjectIndex] as Character);
var designList = GlamourerIpc.GetDesignListSubscriber(_pluginInterface)
.Invoke();
if (ImGui.Button($"Copy {designList.Length} Designs to Clipboard###CopyDesignList"))
ImGui.SetClipboardText(string.Join("\n", designList));
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItem);
ImGui.TableNextColumn();
if (ImGui.Button("Set##SetItem"))
_setItemEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemSubscriber(_pluginInterface)
.Invoke(_objectManager.Objects[_gameObjectIndex] as Character, (byte)_slot, _customItemId.Id, _stainId.Id, 1337);
if (_setItemEc != GlamourerIpc.GlamourerErrorCode.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted(_setItemEc.ToString());
}
ImGuiUtil.DrawTableColumn(GlamourerIpc.LabelSetItemByActorName);
ImGui.TableNextColumn();
if (ImGui.Button("Set##SetItemByActorName"))
_setItemByActorNameEc = (GlamourerIpc.GlamourerErrorCode)GlamourerIpc.SetItemByActorNameSubscriber(_pluginInterface)
.Invoke(_gameObjectName, (byte)_slot, _customItemId.Id, _stainId.Id, 1337);
if (_setItemByActorNameEc != GlamourerIpc.GlamourerErrorCode.Success)
{
ImGui.SameLine();
ImGui.TextUnformatted(_setItemByActorNameEc.ToString());
}
}
private void DrawItemInput()
{
var tmp = _customItemId.Id;
if (ImGuiUtil.InputUlong("Custom Item ID", ref tmp))
_customItemId = (CustomItemId)tmp;
var width = ImGui.GetContentRegionAvail().X;
EquipSlotCombo.Draw("Equip Slot", string.Empty, ref _slot);
var value = (int)_stainId.Id;
ImGui.SameLine();
width -= ImGui.GetContentRegionAvail().X;
ImGui.SetNextItemWidth(width);
if (ImGui.InputInt("Stain ID", ref value, 1, 3))
{
value = Math.Clamp(value, 0, byte.MaxValue);
_stainId = (StainId)value;
}
}
}

View file

@ -1,39 +0,0 @@
using System;
using System.Linq;
using Glamourer.Services;
using ImGuiNET;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.DebugTab;
public class ItemManagerPanel(ItemManager _items) : IDebugTabTree
{
public string Label
=> "Item Manager";
public bool Disabled
=> !_items.ItemService.Valid;
private string _itemFilter = string.Empty;
public void Draw()
{
ImRaii.TreeNode($"Default Sword: {_items.DefaultSword.Name} ({_items.DefaultSword.ItemId}) ({_items.DefaultSword.Weapon()})",
ImGuiTreeNodeFlags.Leaf).Dispose();
DebugTab.DrawNameTable("All Items (Main)", ref _itemFilter,
_items.ItemService.AwaitedService.AllItems(true).Select(p => (p.Item1.Id,
$"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})"))
.OrderBy(p => p.Item1));
DebugTab.DrawNameTable("All Items (Off)", ref _itemFilter,
_items.ItemService.AwaitedService.AllItems(false).Select(p => (p.Item1.Id,
$"{p.Item2.Name} ({(p.Item2.WeaponType == 0 ? p.Item2.Armor().ToString() : p.Item2.Weapon().ToString())})"))
.OrderBy(p => p.Item1));
foreach (var type in Enum.GetValues<FullEquipType>().Skip(1))
{
DebugTab.DrawNameTable(type.ToName(), ref _itemFilter,
_items.ItemService.AwaitedService[type]
.Select(p => (p.ItemId.Id, $"{p.Name} ({(p.WeaponType == 0 ? p.Armor().ToString() : p.Weapon().ToString())})")));
}
}
}

View file

@ -7,11 +7,12 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IDebugTabTree
public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer
{
public string Label
=> "Unlocked Items";
@ -39,7 +40,7 @@ public class ItemUnlockPanel(ItemUnlockManager _itemUnlocks, ItemManager _items)
var remainder = ImGuiClip.ClippedDraw(_itemUnlocks, skips, t =>
{
ImGuiUtil.DrawTableColumn(t.Key.ToString());
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
{
ImGuiUtil.DrawTableColumn(equip.Name);
ImGuiUtil.DrawTableColumn(equip.Type.ToName());

View file

@ -1,76 +0,0 @@
using Glamourer.Interop;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
namespace Glamourer.Gui.Tabs.DebugTab;
public class JobPanel(JobService _jobs) : IDebugTabTree
{
public string Label
=> "Job Service";
public bool Disabled
=> false;
public void Draw()
{
DrawJobs();
DrawJobGroups();
DrawValidJobGroups();
}
private void DrawJobs()
{
using var t = ImRaii.TreeNode("Jobs");
if (!t)
return;
using var table = ImRaii.Table("##jobs", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var (id, job) in _jobs.Jobs)
{
ImGuiUtil.DrawTableColumn(id.ToString("D3"));
ImGuiUtil.DrawTableColumn(job.Name);
ImGuiUtil.DrawTableColumn(job.Abbreviation);
}
}
private void DrawJobGroups()
{
using var t = ImRaii.TreeNode("All Job Groups");
if (!t)
return;
using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var (group, idx) in _jobs.AllJobGroups.WithIndex())
{
ImGuiUtil.DrawTableColumn(idx.ToString("D3"));
ImGuiUtil.DrawTableColumn(group.Name);
ImGuiUtil.DrawTableColumn(group.Count.ToString());
}
}
private void DrawValidJobGroups()
{
using var t = ImRaii.TreeNode("Valid Job Groups");
if (!t)
return;
using var table = ImRaii.Table("##groups", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
foreach (var (id, group) in _jobs.JobGroups)
{
ImGuiUtil.DrawTableColumn(id.ToString("D3"));
ImGuiUtil.DrawTableColumn(group.Name);
ImGuiUtil.DrawTableColumn(group.Count.ToString());
}
}
}

View file

@ -1,14 +1,13 @@
using System;
using System.Numerics;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.DebugTab;
@ -18,7 +17,7 @@ public unsafe class ModelEvaluationPanel(
VisorService _visorService,
UpdateSlotService _updateSlotService,
ChangeCustomizeService _changeCustomizeService,
CrestService _crestService) : IDebugTabTree
CrestService _crestService) : IGameDataDrawer
{
public string Label
=> "Model Evaluation";
@ -126,15 +125,14 @@ public unsafe class ModelEvaluationPanel(
model.AsHuman->Head.Value == 0 ? actor.GetArmor(EquipSlot.Head) : CharacterArmor.Empty);
}
private void DrawWeaponState(Actor actor, Model model)
private static void DrawWeaponState(Actor actor, Model model)
{
using var id = ImRaii.PushId("WeaponState");
ImGuiUtil.DrawTableColumn("Weapon State");
ImGuiUtil.DrawTableColumn(actor.IsCharacter
? actor.AsCharacter->DrawData.IsWeaponHidden ? "Hidden" : "Visible"
: "No Character");
var text = string.Empty;
string text;
if (!model.IsHuman)
{
text = "No Model";
@ -146,19 +144,14 @@ public unsafe class ModelEvaluationPanel(
else
{
var weapon = (DrawObject*)model.AsDrawObject->Object.ChildObject;
if ((weapon->Flags & 0x09) == 0x09)
text = "Visible";
else
text = "Hidden";
text = (weapon->Flags & 0x09) == 0x09 ? "Visible" : "Hidden";
}
ImGuiUtil.DrawTableColumn(text);
ImGui.TableNextColumn();
if (!model.IsHuman)
return;
}
private void DrawWetness(Actor actor, Model model)
private static void DrawWetness(Actor actor, Model model)
{
using var id = ImRaii.PushId("Wetness");
ImGuiUtil.DrawTableColumn("Wetness");
@ -199,7 +192,7 @@ public unsafe class ModelEvaluationPanel(
if (ImGui.SmallButton("Change Piece"))
_updateSlotService.UpdateArmor(model, slot,
new CharacterArmor((SetId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0));
new CharacterArmor((PrimaryId)(slot == EquipSlot.Hands ? 6064 : slot == EquipSlot.Head ? 6072 : 1), 1, 0));
ImGui.SameLine();
if (ImGui.SmallButton("Change Stain"))
_updateSlotService.UpdateStain(model, slot, 5);
@ -212,12 +205,12 @@ public unsafe class ModelEvaluationPanel(
private void DrawCustomize(Actor actor, Model model)
{
using var id = ImRaii.PushId("Customize");
var actorCustomize = new Customize(actor.IsCharacter
? *(Penumbra.GameData.Structs.CustomizeData*)&actor.AsCharacter->DrawData.CustomizeData
: new Penumbra.GameData.Structs.CustomizeData());
var modelCustomize = new Customize(model.IsHuman
? *(Penumbra.GameData.Structs.CustomizeData*)model.AsHuman->Customize.Data
: new Penumbra.GameData.Structs.CustomizeData());
var actorCustomize = actor.IsCharacter
? *(CustomizeArray*)&actor.AsCharacter->DrawData.CustomizeData
: new CustomizeArray();
var modelCustomize = model.IsHuman
? *(CustomizeArray*)model.AsHuman->Customize.Data
: new CustomizeArray();
foreach (var type in Enum.GetValues<CustomizeIndex>())
{
using var id2 = ImRaii.PushId((int)type);
@ -235,7 +228,7 @@ public unsafe class ModelEvaluationPanel(
var shift = BitOperations.TrailingZeroCount(mask);
var newValue = value + (1 << shift);
modelCustomize.Set(type, (CustomizeValue)newValue);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
ImGui.SameLine();
@ -246,14 +239,14 @@ public unsafe class ModelEvaluationPanel(
var shift = BitOperations.TrailingZeroCount(mask);
var newValue = value - (1 << shift);
modelCustomize.Set(type, (CustomizeValue)newValue);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
ImGui.SameLine();
if (ImGui.SmallButton("Reset"))
{
modelCustomize.Set(type, actorCustomize[type]);
_changeCustomizeService.UpdateCustomize(model, modelCustomize.Data);
_changeCustomizeService.UpdateCustomize(model, modelCustomize);
}
}
}

View file

@ -3,19 +3,22 @@ using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectManager _objectManager, DesignConverter _designConverter) : IDebugTabTree
public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectManager _objectManager, DesignConverter _designConverter)
: IGameDataDrawer
{
public string Label
=> "NPC Appearance";
@ -23,23 +26,27 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
public bool Disabled
=> false;
private string _npcFilter = string.Empty;
private bool _customizeOrGear = false;
private string _npcFilter = string.Empty;
private bool _customizeOrGear;
public void Draw()
{
ImGui.Checkbox("Compare Customize (or Gear)", ref _customizeOrGear);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64);
var resetScroll = ImGui.InputTextWithHint("##npcFilter", "Filter...", ref _npcFilter, 64);
using var table = ImRaii.Table("npcs", 5, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
using var table = ImRaii.Table("npcs", 6, ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 400 * ImGuiHelpers.GlobalScale));
if (!table)
return;
if (resetScroll)
ImGui.SetScrollY(0);
ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthFixed, ImGuiHelpers.GlobalScale * 300);
ImGui.TableSetupColumn("Kind", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Id", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Visor", ImGuiTableColumnFlags.WidthFixed);
ImGui.TableSetupColumn("Compare", ImGuiTableColumnFlags.WidthStretch);
@ -48,21 +55,19 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.TableNextRow();
var idx = 0;
var remainder = ImGuiClip.FilteredClippedDraw(_npcCombo.Items, skips,
d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), Draw);
d => d.Name.Contains(_npcFilter, StringComparison.OrdinalIgnoreCase), DrawData);
ImGui.TableNextColumn();
ImGuiClip.DrawEndDummy(remainder, ImGui.GetFrameHeightWithSpacing());
return;
void Draw(CustomizationNpcOptions.NpcData data)
void DrawData(NpcData data)
{
using var id = ImRaii.PushId(idx++);
var disabled = !_state.GetOrCreate(_objectManager.Player, out var state);
ImGui.TableNextColumn();
if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled, false))
if (ImGuiUtil.DrawDisabledButton("Apply", Vector2.Zero, string.Empty, disabled))
{
foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand))
foreach (var (slot, item, stain) in _designConverter.FromDrawData(data.Equip.ToArray(), data.Mainhand, data.Offhand, true))
_state.ChangeEquip(state!, slot, item, stain, StateChanged.Source.Manual);
_state.ChangeVisorState(state!, data.VisorToggled, StateChanged.Source.Manual);
_state.ChangeCustomize(state!, data.Customize, CustomizeFlagExtensions.All, StateChanged.Source.Manual);
@ -76,7 +81,11 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(data.Kind is ObjectKind.BattleNpc ? "B" : "E");
using (var icon = ImRaii.PushFont(UiBuilder.IconFont))
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(data.Id.Id.ToString());
using (_ = ImRaii.PushFont(UiBuilder.IconFont))
{
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
@ -86,7 +95,7 @@ public class NpcAppearancePanel(NpcCombo _npcCombo, StateManager _state, ObjectM
using var mono = ImRaii.PushFont(UiBuilder.MonoFont);
ImGui.TableNextColumn();
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(_customizeOrGear ? data.Customize.Data.ToString() : data.WriteGear());
ImGui.TextUnformatted(_customizeOrGear ? data.Customize.ToString() : data.WriteGear());
}
}
}

View file

@ -3,14 +3,15 @@ using System.Globalization;
using System.Linq;
using System.Numerics;
using Glamourer.Interop;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class ObjectManagerPanel(ObjectManager _objectManager, ActorService _actors) : IDebugTabTree
public class ObjectManagerPanel(ObjectManager _objectManager, ActorManager _actors) : IGameDataDrawer
{
public string Label
=> "Object Manager";
@ -33,7 +34,7 @@ public class ObjectManagerPanel(ObjectManager _objectManager, ActorService _acto
ImGui.TableNextColumn();
ImGuiUtil.DrawTableColumn("World");
ImGuiUtil.DrawTableColumn(_actors.Valid ? _actors.AwaitedService.Data.ToWorldName(_objectManager.World) : "Service Missing");
ImGuiUtil.DrawTableColumn(_actors.Finished ? _actors.Data.ToWorldName(_objectManager.World) : "Service Missing");
ImGuiUtil.DrawTableColumn(_objectManager.World.ToString());
ImGuiUtil.DrawTableColumn("Player Character");

View file

@ -1,8 +1,5 @@
using System;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Gui.Tabs.DebugTab;
using Glamourer.Gui;
using Glamourer.Interop.Penumbra;
using Glamourer.Interop.Structs;
using ImGuiNET;
@ -10,11 +7,12 @@ using OtterGui;
using OtterGui.Raii;
using Penumbra.Api.Enums;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.DebugTab;
public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItemTooltip _penumbraTooltip) : IDebugTabTree
public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItemTooltip _penumbraTooltip) : IGameDataDrawer
{
public string Label
=> "Penumbra Interop";
@ -27,9 +25,6 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem
public void Draw()
{
if (!ImGui.CollapsingHeader("Penumbra"))
return;
using var table = ImRaii.Table("##PenumbraTable", 3, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.RowBg);
if (!table)
return;
@ -59,7 +54,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##CutsceneIndex", ref _gameObjectIndex, 0, 0);
ImGuiUtil.DrawTableColumn(_penumbra.Available
? _penumbra.CutsceneParent(_gameObjectIndex).ToString()
? _penumbra.CutsceneParent((ushort) _gameObjectIndex).ToString()
: "Penumbra Unavailable");
ImGuiUtil.DrawTableColumn("Redraw Object");
@ -67,7 +62,7 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem
ImGui.SetNextItemWidth(200 * ImGuiHelpers.GlobalScale);
ImGui.InputInt("##redrawObject", ref _gameObjectIndex, 0, 0);
ImGui.TableNextColumn();
using (var disabled = ImRaii.Disabled(!_penumbra.Available))
using (_ = ImRaii.Disabled(!_penumbra.Available))
{
if (ImGui.SmallButton("Redraw"))
_penumbra.RedrawObject((ObjectIndex)_gameObjectIndex, RedrawType.Redraw);

View file

@ -1,42 +0,0 @@
using System;
using System.Linq;
using Glamourer.Services;
using ImGuiNET;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Gui.Tabs.DebugTab;
public class RestrictedGearPanel(ItemManager _items) : IDebugTabTree
{
public string Label
=> "Restricted Gear Service";
public bool Disabled
=> false;
private int _setId;
private int _secondaryId;
private int _variant;
public void Draw()
{
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted("Resolve Model");
DebugTab.DrawInputModelSet(false, ref _setId, ref _secondaryId, ref _variant);
foreach (var race in Enum.GetValues<Race>().Skip(1))
{
ReadOnlySpan<Gender> genders = [Gender.Male, Gender.Female];
foreach (var gender in genders)
{
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var (replaced, model) =
_items.RestrictedGear.ResolveRestricted(new CharacterArmor((SetId)_setId, (Variant)_variant, 0), slot, race, gender);
if (replaced)
ImGui.TextUnformatted($"{race.ToName()} - {gender} - {slot.ToName()} resolves to {model}.");
}
}
}
}
}

View file

@ -3,10 +3,11 @@ using Glamourer.Interop;
using Glamourer.Interop.Structs;
using Glamourer.State;
using OtterGui.Raii;
using Penumbra.GameData.Gui.Debug;
namespace Glamourer.Gui.Tabs.DebugTab;
public class RetainedStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IDebugTabTree
public class RetainedStatePanel(StateManager _stateManager, ObjectManager _objectManager) : IGameDataDrawer
{
public string Label
=> "Retained States (Inactive Actors)";

View file

@ -1,53 +0,0 @@
using System;
using System.Numerics;
using Dalamud.Interface.Utility;
using Glamourer.Services;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public class StainPanel(ItemManager _items) : IDebugTabTree
{
public string Label
=> "Stain Service";
public bool Disabled
=> false;
private string _stainFilter = string.Empty;
public void Draw()
{
var resetScroll = ImGui.InputTextWithHint("##filter", "Filter...", ref _stainFilter, 256);
var height = ImGui.GetTextLineHeightWithSpacing() + 2 * ImGui.GetStyle().CellPadding.Y;
using var table = ImRaii.Table("##table", 4,
ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.BordersOuter | ImGuiTableFlags.SizingFixedFit,
new Vector2(-1, 10 * height));
if (!table)
return;
if (resetScroll)
ImGui.SetScrollY(0);
ImGui.TableNextColumn();
var skips = ImGuiClip.GetNecessarySkips(height);
ImGui.TableNextRow();
var remainder = ImGuiClip.FilteredClippedDraw(_items.Stains, skips,
p => p.Key.Id.ToString().Contains(_stainFilter) || p.Value.Name.Contains(_stainFilter, StringComparison.OrdinalIgnoreCase),
p =>
{
ImGuiUtil.DrawTableColumn(p.Key.Id.ToString("D3"));
ImGui.TableNextColumn();
ImGui.GetWindowDrawList().AddRectFilled(ImGui.GetCursorScreenPos(),
ImGui.GetCursorScreenPos() + new Vector2(ImGui.GetTextLineHeight()),
p.Value.RgbaColor, 5 * ImGuiHelpers.GlobalScale);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight()));
ImGuiUtil.DrawTableColumn(p.Value.Name);
ImGuiUtil.DrawTableColumn($"#{p.Value.R:X2}{p.Value.G:X2}{p.Value.B:X2}{(p.Value.Gloss ? ", Glossy" : string.Empty)}");
});
ImGuiClip.DrawEndDummy(remainder, height);
}
}

View file

@ -7,11 +7,12 @@ using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Gui.Debug;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.DebugTab;
public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IDebugTabTree
public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _items) : IGameDataDrawer
{
public string Label
=> "Unlockable Items";
@ -40,7 +41,7 @@ public class UnlockableItemsPanel(ItemUnlockManager _itemUnlocks, ItemManager _i
var remainder = ImGuiClip.ClippedDraw(_itemUnlocks.Unlockable, skips, t =>
{
ImGuiUtil.DrawTableColumn(t.Key.ToString());
if (_items.ItemService.AwaitedService.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
if (_items.ItemData.TryGetValue(t.Key, EquipSlot.MainHand, out var equip))
{
ImGuiUtil.DrawTableColumn(equip.Name);
ImGuiUtil.DrawTableColumn(equip.Type.ToName());

View file

@ -7,14 +7,12 @@ using Dalamud.Interface.ImGuiFileDialog;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Interop;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
@ -156,9 +154,9 @@ public class DesignPanel(DesignFileSystemSelector _selector, CustomizationDrawer
private void DrawCustomizeApplication()
{
var set = _selector.Selected!.CustomizationSet;
var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender;
var flags = _selector.Selected!.ApplyCustomize == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1;
var set = _selector.Selected!.CustomizeSet;
var available = set.SettingAvailable | CustomizeFlag.Clan | CustomizeFlag.Gender | CustomizeFlag.BodyType;
var flags = _selector.Selected!.ApplyCustomizeExcludingBodyType == 0 ? 0 : (_selector.Selected!.ApplyCustomize & available) == available ? 3 : 1;
if (ImGui.CheckboxFlags("Apply All Customizations", ref flags, 3))
{
var newFlags = flags == 3;

View file

@ -1,36 +1,11 @@
using System.Collections;
using System.Collections.Generic;
using System.Threading.Tasks;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.GameData;
using OtterGui.Widgets;
using Penumbra.GameData;
namespace Glamourer.Gui.Tabs;
public class NpcCombo(ActorService actorManager, IdentifierService identifier, IDataManager data)
: FilterComboBase<CustomizationNpcOptions.NpcData>(new LazyList(actorManager, identifier, data), false, Glamourer.Log)
public class NpcCombo(NpcCustomizeSet npcCustomizeSet)
: FilterComboCache<NpcData>(npcCustomizeSet, Glamourer.Log)
{
private class LazyList(ActorService actorManager, IdentifierService identifier, IDataManager data)
: IReadOnlyList<CustomizationNpcOptions.NpcData>
{
private readonly Task<IReadOnlyList<CustomizationNpcOptions.NpcData>> _task
= Task.Run(() => CustomizationNpcOptions.CreateNpcData(actorManager.AwaitedService.Data.ENpcs, actorManager.AwaitedService.Data.BNpcs, identifier.AwaitedService, data));
public IEnumerator<CustomizationNpcOptions.NpcData> GetEnumerator()
=> _task.Result.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _task.Result.Count;
public CustomizationNpcOptions.NpcData this[int index]
=> _task.Result[index];
}
protected override string ToString(CustomizationNpcOptions.NpcData obj)
protected override string ToString(NpcData obj)
=> obj.Name;
}

View file

@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.IO;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Designs;
using Glamourer.GameData;
using Glamourer.Services;
using ImGuiNET;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Glamourer.Gui.Tabs.NpcTab;
public class LocalNpcAppearanceData : ISavable
{
private readonly DesignColors _colors;
public record struct Data(string Color = "", bool Favorite = false);
private readonly Dictionary<ulong, Data> _data = [];
public LocalNpcAppearanceData(DesignColors colors, SaveService saveService)
{
_colors = colors;
Load(saveService);
DataChanged += () => saveService.QueueSave(this);
}
public bool IsFavorite(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var tuple) && tuple.Favorite;
public (uint Color, bool Favorite) GetData(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var t)
? (GetColor(t.Color, t.Favorite, data.Kind), t.Favorite)
: (GetColor(string.Empty, false, data.Kind), false);
public string GetColor(in NpcData data)
=> _data.TryGetValue(ToKey(data), out var t) ? t.Color : string.Empty;
private uint GetColor(string color, bool favorite, ObjectKind kind)
{
if (color.Length == 0)
{
if (favorite)
return ColorId.FavoriteStarOn.Value();
return kind is ObjectKind.BattleNpc
? ColorId.BattleNpc.Value()
: ColorId.EventNpc.Value();
}
if (_colors.TryGetValue(color, out var value))
return value == 0 ? ImGui.GetColorU32(ImGuiCol.Text) : value;
return _colors.MissingColor;
}
public void ToggleFavorite(in NpcData data)
{
var key = ToKey(data);
if (_data.TryGetValue(key, out var t))
{
if (t is { Color: "", Favorite: true })
_data.Remove(key);
else
_data[key] = t with { Favorite = !t.Favorite };
}
else
{
_data[key] = new Data(string.Empty, true);
}
DataChanged.Invoke();
}
public void SetColor(in NpcData data, string color)
{
var key = ToKey(data);
if (_data.TryGetValue(key, out var t))
{
if (!t.Favorite && color.Length == 0)
_data.Remove(key);
else
_data[key] = t with { Color = color };
}
else if (color.Length != 0)
{
_data[key] = new Data(color);
}
DataChanged.Invoke();
}
private static ulong ToKey(in NpcData data)
=> (byte)data.Kind | ((ulong)data.Id.Id << 8);
public event Action DataChanged = null!;
public string ToFilename(FilenameService fileNames)
=> fileNames.NpcAppearanceFile;
public void Save(StreamWriter writer)
{
var jObj = new JObject()
{
["Version"] = 1,
["Data"] = JToken.FromObject(_data),
};
using var j = new JsonTextWriter(writer);
j.Formatting = Formatting.Indented;
jObj.WriteTo(j);
}
private void Load(SaveService save)
{
var file = save.FileNames.NpcAppearanceFile;
if (!File.Exists(file))
return;
try
{
var text = File.ReadAllText(file);
var jObj = JObject.Parse(text);
var version = jObj["Version"]?.ToObject<int>() ?? 0;
switch (version)
{
case 1:
var data = jObj["Data"]?.ToObject<Dictionary<ulong, Data>>() ?? [];
_data.EnsureCapacity(data.Count);
foreach (var kvp in data)
_data.Add(kvp.Key, kvp.Value);
return;
default: throw new Exception("Invalid version {version}.");
}
}
catch (Exception ex)
{
Glamourer.Log.Error($"Could not read local NPC appearance data:\n{ex}");
}
}
}

View file

@ -0,0 +1,45 @@
using System;
using Glamourer.Designs;
using Glamourer.GameData;
using OtterGui.Classes;
namespace Glamourer.Gui.Tabs.NpcTab;
public sealed class NpcFilter(LocalNpcAppearanceData _favorites) : FilterUtility<NpcData>
{
protected override string Tooltip
=> "Filter NPC appearances for those where their names contain the given substring.\n"
+ "Enter i:[number] to filter for NPCs of certain IDs.\n"
+ "Enter c:[string] to filter for NPC appearances set to specific colors.";
protected override (LowerString, long, int) FilterChange(string input)
=> input.Length switch
{
0 => (LowerString.Empty, 0, -1),
> 1 when input[1] == ':' =>
input[0] switch
{
'i' or 'I' => input.Length == 2 ? (LowerString.Empty, 0, -1) :
long.TryParse(input.AsSpan(2), out var r) ? (LowerString.Empty, r, 1) : (LowerString.Empty, 0, -1),
'c' or 'C' => input.Length == 2 ? (LowerString.Empty, 0, -1) : (new LowerString(input[2..]), 0, 2),
_ => (new LowerString(input), 0, 0),
},
_ => (new LowerString(input), 0, 0),
};
public override bool ApplyFilter(in NpcData value)
=> FilterMode switch
{
-1 => false,
0 => Filter.IsContained(value.Name),
1 => value.Id.Id == NumericalFilter,
2 => Filter.IsContained(GetColor(value)),
_ => false, // Should never happen
};
private string GetColor(in NpcData value)
{
var color = _favorites.GetColor(value);
return color.Length == 0 ? DesignColors.AutomaticName : color;
}
}

View file

@ -0,0 +1,294 @@
using System;
using System.Linq;
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Internal.Notifications;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui.Customization;
using Glamourer.Gui.Equipment;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop;
using Glamourer.State;
using ImGuiNET;
using Lumina.Data.Parsing.Scd;
using OtterGui;
using OtterGui.Classes;
using OtterGui.Raii;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcPanel(
NpcSelector _selector,
LocalNpcAppearanceData _favorites,
CustomizationDrawer _customizeDrawer,
EquipmentDrawer _equipDrawer,
DesignConverter _converter,
DesignManager _designManager,
StateManager _state,
ObjectManager _objects,
DesignColors _colors)
{
private readonly DesignColorCombo _colorCombo = new(_colors, true);
private string _newName = string.Empty;
private DesignBase? _newDesign;
public void Draw()
{
using var group = ImRaii.Group();
DrawHeader();
DrawPanel();
}
private void DrawHeader()
{
HeaderDrawer.Draw(_selector.HasSelection ? _selector.Selection.Name : "No Selection", ColorId.NormalDesign.Value(),
ImGui.GetColorU32(ImGuiCol.FrameBg), 2, ExportToClipboardButton(), SaveAsDesignButton(), FavoriteButton());
SaveDesignDrawPopup();
}
private HeaderDrawer.Button FavoriteButton()
{
var (desc, color) = _favorites.IsFavorite(_selector.Selection)
? ("Remove this NPC appearance from your favorites.", ColorId.FavoriteStarOn.Value())
: ("Add this NPC Appearance to your favorites.", 0x80000000);
return new HeaderDrawer.Button
{
Icon = FontAwesomeIcon.Star,
OnClick = () => _favorites.ToggleFavorite(_selector.Selection),
Visible = _selector.HasSelection,
Description = desc,
TextColor = color,
};
}
private HeaderDrawer.Button ExportToClipboardButton()
=> new()
{
Description =
"Copy the current NPCs appearance to your clipboard.\nHold Control to disable applying of customizations for the copied design.\nHold Shift to disable applying of gear for the copied design.",
Icon = FontAwesomeIcon.Copy,
OnClick = ExportToClipboard,
Visible = _selector.HasSelection,
};
private HeaderDrawer.Button SaveAsDesignButton()
=> new()
{
Description =
"Save this NPCs appearance as a design.\nHold Control to disable applying of customizations for the saved design.\nHold Shift to disable applying of gear for the saved design.",
Icon = FontAwesomeIcon.Save,
OnClick = SaveDesignOpen,
Visible = _selector.HasSelection,
};
private void ExportToClipboard()
{
try
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var data = ToDesignData();
var text = _converter.ShareBase64(data, applyGear, applyCustomize, applyCrest);
ImGui.SetClipboardText(text);
}
catch (Exception ex)
{
Glamourer.Messager.NotificationMessage(ex, $"Could not copy {_selector.Selection.Name}'s data to clipboard.",
$"Could not copy data from NPC appearance {_selector.Selection.Kind} {_selector.Selection.Id.Id} to clipboard",
NotificationType.Error);
}
}
private void SaveDesignOpen()
{
ImGui.OpenPopup("Save as Design");
_newName = _selector.Selection.Name;
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var data = ToDesignData();
_newDesign = _converter.Convert(data, applyGear, applyCustomize, applyCrest);
}
private void SaveDesignDrawPopup()
{
if (!ImGuiUtil.OpenNameField("Save as Design", ref _newName))
return;
if (_newDesign != null && _newName.Length > 0)
_designManager.CreateClone(_newDesign, _newName, true);
_newDesign = null;
_newName = string.Empty;
}
private void DrawPanel()
{
using var child = ImRaii.Child("##Panel", -Vector2.One, true);
if (!child || !_selector.HasSelection)
return;
DrawButtonRow();
DrawCustomization();
DrawEquipment();
DrawAppearanceInfo();
}
private void DrawButtonRow()
{
DrawApplyToSelf();
ImGui.SameLine();
DrawApplyToTarget();
}
private void DrawCustomization()
{
if (!ImGui.CollapsingHeader("Customization"))
return;
_customizeDrawer.Draw(_selector.Selection.Customize, true, true);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private void DrawEquipment()
{
if (!ImGui.CollapsingHeader("Equipment"))
return;
_equipDrawer.Prepare();
var designData = ToDesignData();
foreach (var slot in EquipSlotExtensions.EqdpSlots)
{
var data = new EquipDrawData(slot, designData) { Locked = true };
_equipDrawer.DrawEquip(data);
}
var mainhandData = new EquipDrawData(EquipSlot.MainHand, designData) { Locked = true };
var offhandData = new EquipDrawData(EquipSlot.OffHand, designData) { Locked = true };
_equipDrawer.DrawWeapons(mainhandData, offhandData, false);
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromValue(ActorState.MetaIndex.VisorState, _selector.Selection.VisorToggled));
ImGui.Dummy(new Vector2(ImGui.GetTextLineHeight() / 2));
}
private DesignData ToDesignData()
{
var selection = _selector.Selection;
var items = _converter.FromDrawData(selection.Equip.ToArray(), selection.Mainhand, selection.Offhand, true).ToArray();
var designData = new DesignData { Customize = selection.Customize };
foreach (var (slot, item, stain) in items)
{
designData.SetItem(slot, item);
designData.SetStain(slot, stain);
}
return designData;
}
private void DrawApplyToSelf()
{
var (id, data) = _objects.PlayerData;
if (!ImGuiUtil.DrawDisabledButton("Apply to Yourself", Vector2.Zero,
"Apply the current NPC appearance to your character.\nHold Control to only apply gear.\nHold Shift to only apply customizations.",
!data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(design, state, StateChanged.Source.Manual);
}
}
private void DrawApplyToTarget()
{
var (id, data) = _objects.TargetData;
var tt = id.IsValid
? data.Valid
? "Apply the current NPC appearance to your current target.\nHold Control to only apply gear.\nHold Shift to only apply customizations."
: "The current target can not be manipulated."
: "No valid target selected.";
if (!ImGuiUtil.DrawDisabledButton("Apply to Target", Vector2.Zero, tt, !data.Valid))
return;
if (_state.GetOrCreate(id, data.Objects[0], out var state))
{
var (applyGear, applyCustomize, applyCrest) = UiHelpers.ConvertKeysToFlags();
var design = _converter.Convert(ToDesignData(), applyGear, applyCustomize, applyCrest);
_state.ApplyDesign(design, state, StateChanged.Source.Manual);
}
}
private void DrawAppearanceInfo()
{
if (!ImGui.CollapsingHeader("Appearance Details"))
return;
using var table = ImRaii.Table("Details", 2);
if (!table)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ButtonTextAlign, new Vector2(0, 0.5f));
ImGui.TableSetupColumn("Type", ImGuiTableColumnFlags.WidthFixed, ImGui.CalcTextSize("Last Update Datem").X);
ImGui.TableSetupColumn("Data", ImGuiTableColumnFlags.WidthStretch);
var selection = _selector.Selection;
CopyButton("NPC Name", selection.Name);
CopyButton("NPC ID", selection.Id.Id.ToString());
ImGuiUtil.DrawFrameColumn("NPC Type");
ImGui.TableNextColumn();
var width = ImGui.GetContentRegionAvail().X;
ImGuiUtil.DrawTextButton(selection.Kind is ObjectKind.BattleNpc ? "Battle NPC" : "Event NPC", new Vector2(width, 0),
ImGui.GetColorU32(ImGuiCol.FrameBg));
ImGuiUtil.DrawFrameColumn("Color");
var color = _favorites.GetColor(selection);
var colorName = color.Length == 0 ? DesignColors.AutomaticName : color;
ImGui.TableNextColumn();
if (_colorCombo.Draw("##colorCombo", colorName,
"Associate a color with this NPC appearance. Right-Click to revert to automatic coloring.",
width - ImGui.GetStyle().ItemSpacing.X - ImGui.GetFrameHeight(), ImGui.GetTextLineHeight())
&& _colorCombo.CurrentSelection != null)
{
color = _colorCombo.CurrentSelection is DesignColors.AutomaticName ? string.Empty : _colorCombo.CurrentSelection;
_favorites.SetColor(selection, color);
}
if (ImGui.IsItemClicked(ImGuiMouseButton.Right))
{
_favorites.SetColor(selection, string.Empty);
color = string.Empty;
}
if (_colors.TryGetValue(color, out var currentColor))
{
ImGui.SameLine();
if (DesignColorUi.DrawColorButton($"Color associated with {color}", currentColor, out var newColor))
_colors.SetColor(color, newColor);
}
else if (color.Length != 0)
{
ImGui.SameLine();
var size = new Vector2(ImGui.GetFrameHeight());
using var font = ImRaii.PushFont(UiBuilder.IconFont);
ImGuiUtil.DrawTextButton(FontAwesomeIcon.ExclamationCircle.ToIconString(), size, 0, _colors.MissingColor);
ImGuiUtil.HoverTooltip("The color associated with this design does not exist.");
}
return;
static void CopyButton(string label, string text)
{
ImGuiUtil.DrawFrameColumn(label);
ImGui.TableNextColumn();
if (ImGui.Button(text, new Vector2(ImGui.GetContentRegionAvail().X, 0)))
ImGui.SetClipboardText(text);
ImGuiUtil.HoverTooltip("Click to copy to clipboard.");
}
}
}

View file

@ -0,0 +1,95 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Glamourer.GameData;
using ImGuiNET;
using OtterGui;
using OtterGui.Raii;
using ImGuiClip = OtterGui.ImGuiClip;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcSelector : IDisposable
{
private readonly NpcCustomizeSet _npcs;
private readonly LocalNpcAppearanceData _favorites;
private NpcFilter _filter;
private readonly List<int> _visibleOrdered = [];
private int _selectedGlobalIndex;
private bool _listDirty = true;
private Vector2 _defaultItemSpacing;
private float _width;
public NpcSelector(NpcCustomizeSet npcs, LocalNpcAppearanceData favorites)
{
_npcs = npcs;
_favorites = favorites;
_filter = new NpcFilter(_favorites);
_favorites.DataChanged += OnFavoriteChange;
}
public void Dispose()
{
_favorites.DataChanged -= OnFavoriteChange;
}
private void OnFavoriteChange()
=> _listDirty = true;
public void UpdateList()
{
if (!_listDirty)
return;
_listDirty = false;
_visibleOrdered.Clear();
var enumerable = _npcs.WithIndex();
if (!_filter.IsEmpty)
enumerable = enumerable.Where(d => _filter.ApplyFilter(d.Value));
var range = enumerable.OrderByDescending(d => _favorites.IsFavorite(d.Value))
.ThenBy(d => d.Index)
.Select(d => d.Index);
_visibleOrdered.AddRange(range);
}
public bool HasSelection
=> _selectedGlobalIndex >= 0 && _selectedGlobalIndex < _npcs.Count;
public NpcData Selection
=> HasSelection ? _npcs[_selectedGlobalIndex] : default;
public void Draw(float width)
{
_width = width;
using var group = ImRaii.Group();
_defaultItemSpacing = ImGui.GetStyle().ItemSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, Vector2.Zero)
.Push(ImGuiStyleVar.FrameRounding, 0);
if (_filter.Draw(width))
_listDirty = true;
UpdateList();
DrawSelector();
}
private void DrawSelector()
{
using var child = ImRaii.Child("##Selector", new Vector2(_width, ImGui.GetContentRegionAvail().Y), true);
if (!child)
return;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, _defaultItemSpacing);
ImGuiClip.ClippedDraw(_visibleOrdered, DrawSelectable, ImGui.GetTextLineHeight());
}
private void DrawSelectable(int globalIndex)
{
using var id = ImRaii.PushId(globalIndex);
using var color = ImRaii.PushColor(ImGuiCol.Text, _favorites.GetData(_npcs[globalIndex]).Color);
if (ImGui.Selectable(_npcs[globalIndex].Name, _selectedGlobalIndex == globalIndex, ImGuiSelectableFlags.AllowItemOverlap))
_selectedGlobalIndex = globalIndex;
}
}

View file

@ -0,0 +1,19 @@
using System;
using Dalamud.Interface.Utility;
using ImGuiNET;
using OtterGui.Widgets;
namespace Glamourer.Gui.Tabs.NpcTab;
public class NpcTab(NpcSelector _selector, NpcPanel _panel) : ITab
{
public ReadOnlySpan<byte> Label
=> "NPCs"u8;
public void DrawContent()
{
_selector.Draw(200 * ImGuiHelpers.GlobalScale);
ImGui.SameLine();
_panel.Draw();
}
}

View file

@ -60,7 +60,7 @@ public class SettingsTab : ITab
if (!child)
return;
Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters to be applied automatically.",
Checkbox("Enable Auto Designs", "Enable the application of designs associated to characters in the Automation tab to be applied automatically.",
_config.EnableAutoDesigns, v => _config.EnableAutoDesigns = v);
ImGui.NewLine();
ImGui.NewLine();
@ -247,22 +247,17 @@ public class SettingsTab : ITab
if (ImGui.Checkbox(code, ref state))
{
action(state);
_config.Codes[i] = (code, state);
_codeService.VerifyState();
_config.Save();
_codeService.SaveState();
}
}
if (_codeService.EnabledCaptain)
{
if (ImGui.Button("Who am I?!?"))
_funModule.WhoAmI();
if (ImGui.Button("Who am I?!?"))
_funModule.WhoAmI();
ImGui.SameLine();
ImGui.SameLine();
if (ImGui.Button("Who is that!?!"))
_funModule.WhoIsThat();
}
if (ImGui.Button("Who is that!?!"))
_funModule.WhoIsThat();
}
private void DrawCodeHints()

View file

@ -3,7 +3,7 @@ using System.Linq;
using System.Numerics;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.GameData;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Unlocks;
@ -19,7 +19,7 @@ public class UnlockOverview
{
private readonly ItemManager _items;
private readonly ItemUnlockManager _itemUnlocks;
private readonly CustomizationService _customizations;
private readonly CustomizeService _customizations;
private readonly CustomizeUnlockManager _customizeUnlocks;
private readonly PenumbraChangedItemTooltip _tooltip;
private readonly TextureService _textures;
@ -41,7 +41,7 @@ public class UnlockOverview
foreach (var type in Enum.GetValues<FullEquipType>())
{
if (type.IsOffhandType() || !_items.ItemService.AwaitedService.TryGetValue(type, out var items) || items.Count == 0)
if (type.IsOffhandType() || !_items.ItemData.ByType.TryGetValue(type, out var items) || items.Count == 0)
continue;
if (ImGui.Selectable(type.ToName(), _selected1 == type))
@ -52,25 +52,22 @@ public class UnlockOverview
}
}
foreach (var clan in _customizations.AwaitedService.Clans)
foreach (var (clan, gender) in CustomizeManager.AllSets())
{
foreach (var gender in _customizations.AwaitedService.Genders)
{
if (_customizations.AwaitedService.GetList(clan, gender).HairStyles.Count == 0)
continue;
if (_customizations.Manager.GetSet(clan, gender).HairStyles.Count == 0)
continue;
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
}
if (ImGui.Selectable($"{(gender is Gender.Male ? '♂' : '♀')} {clan.ToShortName()} Hair & Paint",
_selected2 == clan && _selected3 == gender))
{
_selected1 = FullEquipType.Unknown;
_selected2 = clan;
_selected3 = gender;
}
}
}
public UnlockOverview(ItemManager items, CustomizationService customizations, ItemUnlockManager itemUnlocks,
public UnlockOverview(ItemManager items, CustomizeService customizations, ItemUnlockManager itemUnlocks,
CustomizeUnlockManager customizeUnlocks, PenumbraChangedItemTooltip tooltip, TextureService textures, CodeService codes,
JobService jobs, FavoriteManager favorites)
{
@ -107,7 +104,7 @@ public class UnlockOverview
private void DrawCustomizations()
{
var set = _customizations.AwaitedService.GetList(_selected2, _selected3);
var set = _customizations.Manager.GetSet(_selected2, _selected3);
var spacing = IconSpacing;
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, spacing);
@ -121,10 +118,10 @@ public class UnlockOverview
continue;
var unlocked = _customizeUnlocks.IsUnlocked(customize, out var time);
var icon = _customizations.AwaitedService.GetIcon(customize.IconId);
var icon = _customizations.Manager.GetIcon(customize.IconId);
ImGui.Image(icon.ImGuiHandle, iconSize, Vector2.Zero, Vector2.One,
unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint);
unlocked || _codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint);
if (ImGui.IsItemHovered())
{
using var tt = ImRaii.Tooltip();
@ -150,7 +147,7 @@ public class UnlockOverview
private void DrawItems()
{
if (!_items.ItemService.AwaitedService.TryGetValue(_selected1, out var items))
if (!_items.ItemData.ByType.TryGetValue(_selected1, out var items))
return;
var spacing = IconSpacing;
@ -160,6 +157,30 @@ public class UnlockOverview
var numRows = (items.Count + iconsPerRow - 1) / iconsPerRow;
var numVisibleRows = (int)(Math.Ceiling(ImGui.GetContentRegionAvail().Y / (iconSize.Y + spacing.Y)) + 0.5f) + 1;
var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y);
var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.Count);
var counter = 0;
for (var idx = skips * iconsPerRow; idx < end; ++idx)
{
DrawItem(items[idx]);
if (counter != iconsPerRow - 1)
{
ImGui.SameLine();
++counter;
}
else
{
counter = 0;
}
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
var remainder = numRows - numVisibleRows - skips;
if (remainder > 0)
ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y);
return;
void DrawItem(EquipItem item)
{
var unlocked = _itemUnlocks.IsUnlocked(item.Id, out var time);
@ -168,7 +189,7 @@ public class UnlockOverview
var (icon, size) = (iconHandle.ImGuiHandle, new Vector2(iconHandle.Width, iconHandle.Height));
ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, unlocked || _codes.EnabledShirts ? Vector4.One : UnavailableTint);
ImGui.Image(icon, iconSize, Vector2.Zero, Vector2.One, unlocked || _codes.Enabled(CodeService.CodeFlag.Shirts) ? Vector4.One : UnavailableTint);
if (_favorites.Contains(item))
ImGui.GetWindowDrawList().AddRect(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), ColorId.FavoriteStarOn.Value(),
2 * ImGuiHelpers.GlobalScale, ImDrawFlags.RoundCornersAll, 4 * ImGuiHelpers.GlobalScale);
@ -189,7 +210,7 @@ public class UnlockOverview
ImGui.TextUnformatted($"{item.Type.ToName()} ({slot.ToName()})");
if (item.Type.ValidOffhand().IsOffhandType())
ImGui.TextUnformatted(
$"{item.Weapon()}{(_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}");
$"{item.Weapon()}{(_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand) ? $" | {offhand.Weapon()}" : string.Empty)}");
else
ImGui.TextUnformatted(slot is EquipSlot.MainHand ? $"{item.Weapon()}" : $"{item.Armor()}");
ImGui.TextUnformatted(
@ -219,29 +240,6 @@ public class UnlockOverview
_tooltip.CreateTooltip(item, string.Empty, false);
}
}
var skips = ImGuiClip.GetNecessarySkips(iconSize.Y + spacing.Y);
var end = Math.Min(numVisibleRows * iconsPerRow + skips * iconsPerRow, items.Count);
var counter = 0;
for (var idx = skips * iconsPerRow; idx < end; ++idx)
{
DrawItem(items[idx]);
if (counter != iconsPerRow - 1)
{
ImGui.SameLine();
++counter;
}
else
{
counter = 0;
}
}
if (ImGui.GetCursorPosX() != 0)
ImGui.NewLine();
var remainder = numRows - numVisibleRows - skips;
if (remainder > 0)
ImGuiClip.DrawEndDummy(remainder, iconSize.Y + spacing.Y);
}
private static Vector2 IconSpacing

View file

@ -8,7 +8,6 @@ using Dalamud.Interface.Utility;
using Glamourer.Events;
using Glamourer.Interop;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using OtterGui;
@ -278,7 +277,7 @@ public class UnlockTable : Table<EquipItem>, IDisposable
ImGuiUtil.RightAlign(item.ModelString);
if (ImGui.IsItemHovered()
&& item.Type.ValidOffhand().IsOffhandType()
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
&& _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
{
using var tt = ImRaii.Tooltip();
ImGui.TextUnformatted("Offhand: " + offhand.ModelString);
@ -297,7 +296,7 @@ public class UnlockTable : Table<EquipItem>, IDisposable
return true;
if (item.Type.ValidOffhand().IsOffhandType()
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
&& _items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
return FilterRegex?.IsMatch(offhand.ModelString)
?? offhand.ModelString.Contains(FilterValue, StringComparison.OrdinalIgnoreCase);
@ -411,21 +410,16 @@ public class UnlockTable : Table<EquipItem>, IDisposable
=> item.Flags.HasFlag(ItemFlags.IsCrestWorthy);
}
private sealed class ItemList : IReadOnlyCollection<EquipItem>
private sealed class ItemList(ItemManager items) : IReadOnlyCollection<EquipItem>
{
private readonly ItemManager _items;
public ItemList(ItemManager items)
=> _items = items;
public IEnumerator<EquipItem> GetEnumerator()
=> _items.ItemService.AwaitedService.AllItems(true).Select(i => i.Item2).GetEnumerator();
=> items.ItemData.AllItems(true).Select(i => i.Item2).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
public int Count
=> _items.ItemService.AwaitedService.TotalItemCount(true);
=> items.ItemData.Primary.Count;
}
private void OnObjectUnlock(ObjectUnlocked.Type _1, uint _2, DateTimeOffset _3)

View file

@ -2,7 +2,6 @@
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.State;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
namespace Glamourer.Gui;
@ -101,4 +100,23 @@ public ref struct ToggleDrawData
SetValue = setValue,
};
}
public static ToggleDrawData FromValue(ActorState.MetaIndex index, bool value)
{
var (label, tooltip) = index switch
{
ActorState.MetaIndex.HatState => ("Hat Visible", "Hide or show the characters head gear."),
ActorState.MetaIndex.VisorState => ("Visor Toggled", "Toggle the visor state of the characters head gear."),
ActorState.MetaIndex.WeaponState => ("Weapon Visible", "Hide or show the characters weapons when not drawn."),
ActorState.MetaIndex.Wetness => ("Force Wetness", "Force the character to be wet or not."),
_ => throw new Exception("Unsupported meta index."),
};
return new ToggleDrawData
{
Label = label,
Tooltip = tooltip,
Locked = true,
CurrentValue = value,
};
}
}

View file

@ -1,9 +1,7 @@
using System.Numerics;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Glamourer.Customization;
using Glamourer.Services;
using Glamourer.Structs;
using Glamourer.Unlocks;
using ImGuiNET;
using Lumina.Misc;
@ -27,7 +25,7 @@ public static class UiHelpers
public static void DrawIcon(this EquipItem item, TextureService textures, Vector2 size, EquipSlot slot)
{
var isEmpty = item.ModelId.Id == 0;
var isEmpty = item.PrimaryId.Id == 0;
var (ptr, textureSize, empty) = textures.GetIcon(item, slot);
if (empty)
{
@ -49,9 +47,21 @@ public static class UiHelpers
public static bool DrawCheckbox(string label, string tooltip, bool value, out bool on, bool locked)
{
using var disabled = ImRaii.Disabled(locked);
var ret = ImGuiUtil.Checkbox(label, string.Empty, value, v => value = v);
ImGuiUtil.HoverTooltip(tooltip);
var startsWithHash = label.StartsWith("##");
bool ret;
using (_ = ImRaii.Disabled(locked))
{
ret = ImGuiUtil.Checkbox(startsWithHash ? label : "##" + label, string.Empty, value, v => value = v);
}
if (!startsWithHash)
{
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(label);
}
ImGuiUtil.HoverTooltip(tooltip, ImGuiHoveredFlags.AllowWhenDisabled);
on = value;
return ret;
}
@ -59,9 +69,8 @@ public static class UiHelpers
public static (bool, bool) DrawMetaToggle(string label, bool currentValue, bool currentApply, out bool newValue,
out bool newApply, bool locked)
{
var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0);
using var style = ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, ImGui.GetStyle().ItemInnerSpacing);
using (var disabled = ImRaii.Disabled(locked))
var flags = (sbyte)(currentApply ? currentValue ? 1 : -1 : 0);
using (_ = ImRaii.Disabled(locked))
{
if (new TristateCheckbox(ColorId.TriStateCross.Value(), ColorId.TriStateCheck.Value(), ColorId.TriStateNeutral.Value()).Draw(
"##" + label, flags, out flags))
@ -82,7 +91,8 @@ public static class UiHelpers
ImGuiUtil.HoverTooltip($"This attribute will be {(currentApply ? currentValue ? "enabled." : "disabled." : "kept as is.")}");
ImGui.SameLine();
ImGui.SameLine(0, ImGui.GetStyle().ItemInnerSpacing.X);
ImGui.AlignTextToFramePadding();
ImGui.TextUnformatted(label);
return (currentValue != newValue, currentApply != newApply);

View file

@ -1,13 +1,11 @@
using System;
using System.Threading;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using OtterGui.Classes;
using CustomizeData = Penumbra.GameData.Structs.CustomizeData;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
@ -16,7 +14,7 @@ namespace Glamourer.Interop;
/// Changes in Race, body type or Gender are probably ignored.
/// This operates on draw objects, not game objects.
/// </summary>
public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Customize>>, ChangeCustomizeService.Priority>
public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<CustomizeArray>>, ChangeCustomizeService.Priority>
{
private readonly PenumbraReloaded _penumbraReloaded;
private readonly IGameInteropProvider _interop;
@ -64,7 +62,7 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
private Hook<ChangeCustomizeDelegate> _changeCustomizeHook;
public bool UpdateCustomize(Model model, CustomizeData customize)
public bool UpdateCustomize(Model model, CustomizeArray customize)
{
if (!model.IsHuman)
return false;
@ -75,17 +73,18 @@ public unsafe class ChangeCustomizeService : EventWrapper<Action<Model, Ref<Cust
return ret;
}
public bool UpdateCustomize(Actor actor, CustomizeData customize)
public bool UpdateCustomize(Actor actor, CustomizeArray customize)
=> UpdateCustomize(actor.Model, customize);
private bool ChangeCustomizeDetour(Human* human, byte* data, byte skipEquipment)
{
if (!InUpdate.InMethod)
{
var customize = new Ref<Customize>(new Customize(*(CustomizeData*)data));
var customize = new Ref<CustomizeArray>(*(CustomizeArray*)data);
Invoke(this, (Model)human, customize);
((Customize*)data)->Load(customize.Value);
*(CustomizeArray*)data = customize.Value;
}
return _changeCustomizeHook.Original(human, data, skipEquipment);
}
}

View file

@ -1,8 +1,6 @@
using System;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Services;
using Glamourer.Structs;
using Newtonsoft.Json.Linq;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -70,7 +68,7 @@ public sealed class CharaFile
return;
data.SetItem(slot, item);
data.SetStain(slot, (StainId)dye);
data.SetStain(slot, dye);
flags |= slot.ToFlag();
flags |= slot.ToStainFlag();
}
@ -94,7 +92,7 @@ public sealed class CharaFile
flags |= slot.ToStainFlag();
}
private static CustomizeFlag ParseCustomize(JObject jObj, ref Customize customize)
private static CustomizeFlag ParseCustomize(JObject jObj, ref CustomizeArray customize)
{
CustomizeFlag ret = 0;
customize.Race = ParseRace(jObj, ref ret);
@ -149,7 +147,7 @@ public sealed class CharaFile
return id;
}
private static void ParseFacial(JObject jObj, ref Customize customize, ref CustomizeFlag application)
private static void ParseFacial(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application)
{
var jTok = jObj["FacialFeatures"];
if (jTok == null)
@ -186,7 +184,7 @@ public sealed class CharaFile
customize[CustomizeIndex.LegacyTattoo] = CustomizeValue.Max;
}
private static void ParseHighlights(JObject jObj, ref Customize customize, ref CustomizeFlag application)
private static void ParseHighlights(JObject jObj, ref CustomizeArray customize, ref CustomizeFlag application)
{
var jTok = jObj["EnableHighlights"];
if (jTok == null)
@ -242,15 +240,15 @@ public sealed class CharaFile
throw new Exception($"Age {age} != Normal is not supported.");
}
private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref Customize customize,
private static unsafe void ParseByte(JObject jObj, string property, CustomizeIndex idx, ref CustomizeArray customize,
ref CustomizeFlag application)
{
var jTok = jObj[property];
if (jTok == null)
return;
customize.Data.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject<byte>();
application |= idx.ToFlag();
customize.Data[idx.ToByteAndMask().ByteIdx] = jTok.ToObject<byte>();
application |= idx.ToFlag();
}
private static SubRace ParseTribe(JObject jObj, ref CustomizeFlag application)

View file

@ -42,7 +42,7 @@ public sealed class CmaFile
var byteData = Convert.FromHexString(bytes);
fixed (byte* ptr = byteData)
{
data.Customize.Data.Read(ptr);
data.Customize.Read(ptr);
}
}
@ -64,7 +64,7 @@ public sealed class CmaFile
data.SetStain(slot, armor.Stain);
}
data.Customize.Data.Read(ptr);
data.Customize.Read(ptr);
}
}
@ -78,8 +78,8 @@ public sealed class CmaFile
return;
}
var set = mainhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.ModelId;
var type = mainhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.WeaponType;
var set = mainhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.PrimaryId;
var type = mainhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.SecondaryId;
var variant = mainhand["Item3"]?.ToObject<byte>() ?? items.DefaultSword.Variant;
var stain = mainhand["Item4"]?.ToObject<byte>() ?? 0;
var item = items.Identify(EquipSlot.MainHand, set, type, variant);
@ -95,17 +95,17 @@ public sealed class CmaFile
if (offhand == null)
{
data.SetItem(EquipSlot.MainHand, defaultOffhand);
data.SetStain(EquipSlot.MainHand, defaultOffhand.ModelId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand));
data.SetStain(EquipSlot.MainHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : data.Stain(EquipSlot.MainHand));
return;
}
var set = offhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.ModelId;
var type = offhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.WeaponType;
var set = offhand["Item1"]?.ToObject<ushort>() ?? items.DefaultSword.PrimaryId;
var type = offhand["Item2"]?.ToObject<ushort>() ?? items.DefaultSword.SecondaryId;
var variant = offhand["Item3"]?.ToObject<byte>() ?? items.DefaultSword.Variant;
var stain = offhand["Item4"]?.ToObject<byte>() ?? 0;
var item = items.Identify(EquipSlot.OffHand, set, type, variant, data.MainhandType);
data.SetItem(EquipSlot.OffHand, item.Valid ? item : defaultOffhand);
data.SetStain(EquipSlot.OffHand, defaultOffhand.ModelId.Id == 0 ? 0 : (StainId)stain);
data.SetStain(EquipSlot.OffHand, defaultOffhand.PrimaryId.Id == 0 ? 0 : (StainId)stain);
}
}

View file

@ -68,7 +68,7 @@ public class ContextMenuService : IDisposable
if (itemId > 500000)
itemId -= 500000;
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item))
if (!_items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out var item))
return null;
return new InventoryContextMenuItem(TryOnString, GetInventoryAction(item));
@ -80,7 +80,7 @@ public class ContextMenuService : IDisposable
if (itemId > 500000)
itemId -= 500000;
if (!_items.ItemService.AwaitedService.TryGetValue(itemId, EquipSlot.MainHand, out var item))
if (!_items.ItemData.TryGetValue(itemId, EquipSlot.MainHand, out var item))
return null;
return new GameObjectContextMenuItem(TryOnString, GetGameObjectAction(item));
@ -122,10 +122,10 @@ public class ContextMenuService : IDisposable
_state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual);
if (item.Type.ValidOffhand().IsOffhandType())
{
if (item.ModelId.Id is > 1600 and < 1651
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
if (item.PrimaryId.Id is > 1600 and < 1651
&& _items.ItemData.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual);
if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
if (_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual);
}
};
@ -146,10 +146,10 @@ public class ContextMenuService : IDisposable
_state.ChangeEquip(state, slot, item, 0, StateChanged.Source.Manual);
if (item.Type.ValidOffhand().IsOffhandType())
{
if (item.ModelId.Id is > 1600 and < 1651
&& _items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
if (item.PrimaryId.Id is > 1600 and < 1651
&& _items.ItemData.TryGetValue(item.ItemId, EquipSlot.Hands, out var gauntlets))
_state.ChangeEquip(state, EquipSlot.Hands, gauntlets, 0, StateChanged.Source.Manual);
if (_items.ItemService.AwaitedService.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
if (_items.ItemData.TryGetValue(item.ItemId, EquipSlot.OffHand, out var offhand))
_state.ChangeEquip(state, EquipSlot.OffHand, offhand, 0, StateChanged.Source.Manual);
}
};

View file

@ -1,12 +1,10 @@
using System;
using System.Linq;
using Dalamud.Hooking;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
@ -79,7 +77,7 @@ public sealed unsafe class CrestService : EventWrapper<Action<Actor, CrestFlag,
crestFlags = (byte)(newValue.Value ? crestFlags | (byte)slot : crestFlags & (byte)~slot);
}
Glamourer.Log.Information(
Glamourer.Log.Verbose(
$"Called CrestChange on {(ulong)character:X} with {crestFlags:X} and prior flags {((Actor)character).CrestBitfield}.");
using var _ = _inUpdate.EnterMethod();
_crestChangeHook.Original(character, crestFlags);

View file

@ -4,17 +4,18 @@ using System.IO;
using System.Linq;
using Dalamud.Interface.DragDrop;
using Dalamud.Interface.Internal.Notifications;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Interop.CharaFile;
using Glamourer.Services;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui.Classes;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Files;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
public class ImportService(CustomizationService _customizations, IDragDropManager _dragDropManager, ItemManager _items)
public class ImportService(CustomizeService _customizations, IDragDropManager _dragDropManager, ItemManager _items)
{
public void CreateDatSource()
=> _dragDropManager.CreateImGuiSource("DatDragger", m => m.Files.Count == 1 && m.Extensions.Contains(".dat"), m =>
@ -139,7 +140,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
return true;
}
public bool SaveDesignAsDat(string path, in Customize input, string description)
public bool SaveDesignAsDat(string path, in CustomizeArray input, string description)
{
if (!Verify(input, out var voice))
return false;
@ -168,7 +169,7 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
}
}
public bool Verify(in Customize input, out byte voice, byte? inputVoice = null)
public bool Verify(in CustomizeArray input, out byte voice, byte? inputVoice = null)
{
voice = 0;
if (_customizations.ValidateClan(input.Clan, input.Race, out _, out _).Length > 0)
@ -178,14 +179,14 @@ public class ImportService(CustomizationService _customizations, IDragDropManage
if (input.BodyType.Value != 1)
return false;
var set = _customizations.AwaitedService.GetList(input.Clan, input.Gender);
var set = _customizations.Manager.GetSet(input.Clan, input.Gender);
voice = set.Voices[0];
if (inputVoice.HasValue && !set.Voices.Contains(inputVoice.Value))
return false;
foreach (var index in CustomizationExtensions.AllBasic)
{
if (!CustomizationService.IsCustomizationValid(set, input.Face, index, input[index]))
if (!CustomizeService.IsCustomizationValid(set, input.Face, index, input[index]))
return false;
}

View file

@ -6,7 +6,9 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Structs;
namespace Glamourer.Interop;
@ -14,19 +16,20 @@ public class JobService : IDisposable
{
private readonly nint _characterDataOffset;
public readonly IReadOnlyDictionary<byte, Job> Jobs;
public readonly IReadOnlyDictionary<ushort, JobGroup> JobGroups;
public readonly IReadOnlyList<JobGroup> AllJobGroups;
public readonly DictJob Jobs;
public readonly DictJobGroup JobGroups;
public IReadOnlyList<JobGroup> AllJobGroups
=> JobGroups.AllJobGroups;
public event Action<Actor, Job, Job>? JobChanged;
public JobService(IDataManager gameData, IGameInteropProvider interop)
public JobService(DictJob jobs, DictJobGroup jobGroups, IDataManager gameData, IGameInteropProvider interop)
{
interop.InitializeFromAttributes(this);
_characterDataOffset = Marshal.OffsetOf<Character>(nameof(Character.CharacterData));
Jobs = GameData.Jobs(gameData);
AllJobGroups = GameData.AllJobGroups(gameData);
JobGroups = GameData.JobGroups(gameData);
Jobs = jobs;
JobGroups = jobGroups;
_changeJobHook.Enable();
}

View file

@ -5,8 +5,8 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Control;
using Glamourer.Interop.Structs;
using Glamourer.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
namespace Glamourer.Interop;
@ -15,13 +15,13 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
private readonly IFramework _framework;
private readonly IClientState _clientState;
private readonly IObjectTable _objects;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private readonly ITargetManager _targets;
public IObjectTable Objects
=> _objects;
public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorService actors, ITargetManager targets)
public ObjectManager(IFramework framework, IClientState clientState, IObjectTable objects, ActorManager actors, ITargetManager targets)
{
_framework = framework;
_clientState = clientState;
@ -57,7 +57,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
for (var i = 0; i < (int)ScreenActor.CutsceneStart; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
if (character.Identifier(_actors, out var identifier))
HandleIdentifier(identifier, character);
}
@ -70,13 +70,13 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
if (!character.Valid && i == (int)ScreenActor.CutsceneStart)
break;
HandleIdentifier(character.GetIdentifier(_actors.AwaitedService), character);
HandleIdentifier(character.GetIdentifier(_actors), character);
}
void AddSpecial(ScreenActor idx, string label)
{
Actor actor = _objects.GetObjectAddress((int)idx);
if (actor.Identifier(_actors.AwaitedService, out var ident))
if (actor.Identifier(_actors, out var ident))
{
var data = new ActorData(actor, label);
_identifiers.Add(ident, data);
@ -95,7 +95,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
for (var i = (int)ScreenActor.ScreenEnd; i < _objects.Length; ++i)
{
Actor character = _objects.GetObjectAddress(i);
if (character.Identifier(_actors.AwaitedService, out var identifier))
if (character.Identifier(_actors, out var identifier))
HandleIdentifier(identifier, character);
}
@ -120,7 +120,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
if (identifier.Type is IdentifierType.Player or IdentifierType.Owned)
{
var allWorld = _actors.AwaitedService.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
var allWorld = _actors.CreateIndividualUnchecked(identifier.Type, identifier.PlayerName, ushort.MaxValue,
identifier.Kind,
identifier.DataId);
@ -137,7 +137,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
if (identifier.Type is IdentifierType.Owned)
{
var nonOwned = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId);
var nonOwned = _actors.CreateNpc(identifier.Kind, identifier.DataId);
if (!_nonOwnedIdentifiers.TryGetValue(nonOwned, out var nonOwnedData))
{
nonOwnedData = new ActorData(character, nonOwned.ToString());
@ -170,7 +170,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
get
{
Update();
return Player.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
return Player.Identifier(_actors, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ident, ActorData.Invalid);
}
@ -181,7 +181,7 @@ public class ObjectManager : IReadOnlyDictionary<ActorIdentifier, ActorData>
get
{
Update();
return Target.Identifier(_actors.AwaitedService, out var ident) && _identifiers.TryGetValue(ident, out var data)
return Target.Identifier(_actors, out var ident) && _identifiers.TryGetValue(ident, out var data)
? (ident, data)
: (ident, ActorData.Invalid);
}

View file

@ -221,8 +221,8 @@ public unsafe class PenumbraService : IDisposable
=> Available ? _drawObjectInfo.Invoke(drawObject.Address).Item1 : Actor.Null;
/// <summary> Obtain the parent of a cutscene actor if it is known. </summary>
public int CutsceneParent(int idx)
=> Available ? _cutsceneParent.Invoke(idx) : -1;
public short CutsceneParent(ushort idx)
=> (short) (Available ? _cutsceneParent.Invoke(idx) : -1);
/// <summary> Try to redraw the given actor. </summary>
public void RedrawObject(Actor actor, RedrawType settings)

View file

@ -2,8 +2,6 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Game.Object;
using Glamourer.Customization;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Penumbra.String;
@ -116,8 +114,8 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public CharacterWeapon GetOffhand()
=> new(AsCharacter->DrawData.Weapon(DrawDataContainer.WeaponSlot.OffHand).ModelId.Value);
public Customize GetCustomize()
=> *(Customize*)&AsCharacter->DrawData.CustomizeData;
public CustomizeArray GetCustomize()
=> *(CustomizeArray*)&AsCharacter->DrawData.CustomizeData;
// TODO remove this when available in ClientStructs
internal ref CrestFlag CrestBitfield
@ -125,4 +123,21 @@ public readonly unsafe struct Actor : IEquatable<Actor>
public override string ToString()
=> $"0x{Address:X}";
public OnlineStatus OnlineStatus
=> (OnlineStatus)AsCharacter->CharacterData.OnlineStatus;
}
public enum OnlineStatus : byte
{
Normal = 0x00,
Mentor = 0x1B,
PvEMentor = 0x1C,
TradeMentor = 0x1D,
PvPMentor = 0x1E,
Busy = 0x0C,
Away = 0x11,
MeldMateria = 0x15,
RolePlaying = 0x16,
LookingForGroup = 0x17,
}

View file

@ -1,7 +1,6 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using FFXIVClientStructs.FFXIV.Client.Graphics.Scene;
using Glamourer.Customization;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
using Object = FFXIVClientStructs.FFXIV.Client.Graphics.Scene.Object;
@ -91,8 +90,8 @@ public readonly unsafe struct Model : IEquatable<Model>
public CharacterArmor GetArmor(EquipSlot slot)
=> ((CharacterArmor*)&AsHuman->Head)[slot.ToIndex()];
public Customize GetCustomize()
=> *(Customize*)&AsHuman->Customize;
public CustomizeArray GetCustomize()
=> *(CustomizeArray*)&AsHuman->Customize;
public (Model Address, CharacterWeapon Data) GetMainhand()
{

View file

@ -4,6 +4,7 @@ using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Penumbra.GameData;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;

View file

@ -18,7 +18,6 @@ public class VisorService : IDisposable
{
Event = visorStateChanged;
_setupVisorHook = interop.HookFromAddress<UpdateVisorDelegateInternal>((nint)Human.MemberFunctionPointers.SetupVisor, SetupVisorDetour);
interop.InitializeFromAttributes(this);
_setupVisorHook.Enable();
}

View file

@ -5,7 +5,6 @@ using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.Character;
using Glamourer.Events;
using Glamourer.Interop.Structs;
using Glamourer.Structs;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
@ -14,18 +13,15 @@ namespace Glamourer.Interop;
public unsafe class WeaponService : IDisposable
{
private readonly WeaponLoading _event;
private readonly CrestService _crestService;
private readonly ThreadLocal<bool> _inUpdate = new(() => false);
private readonly delegate* unmanaged[Stdcall]<DrawDataContainer*, uint, ulong, byte, byte, byte, byte, void>
_original;
public WeaponService(WeaponLoading @event, IGameInteropProvider interop, CrestService crestService)
public WeaponService(WeaponLoading @event, IGameInteropProvider interop)
{
_event = @event;
_crestService = crestService;
_event = @event;
_loadWeaponHook =
interop.HookFromAddress<LoadWeaponDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_original =
@ -73,10 +69,10 @@ public unsafe class WeaponService : IDisposable
_event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
if (tmpWeapon.Value != weapon.Value)
{
if (tmpWeapon.Set.Id == 0)
if (tmpWeapon.Skeleton.Id == 0)
tmpWeapon.Stain = 0;
_loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4);
}
@ -119,7 +115,7 @@ public unsafe class WeaponService : IDisposable
var mdl = character.Model;
var (_, _, mh, oh) = mdl.GetWeapons(character);
var value = slot == EquipSlot.OffHand ? oh : mh;
var weapon = value.With(value.Set.Id == 0 ? 0 : stain);
var weapon = value.With(value.Skeleton.Id == 0 ? 0 : stain);
LoadWeapon(character, slot, weapon);
}
}

View file

@ -12,23 +12,67 @@ public class CodeService
private readonly Configuration _config;
private readonly SHA256 _hasher = SHA256.Create();
public enum Sizing
[Flags]
public enum CodeFlag : ulong
{
None,
Dwarf,
Giant,
Clown = 0x000001,
Emperor = 0x000002,
Individual = 0x000004,
Dwarf = 0x000008,
Giant = 0x000010,
OopsHyur = 0x000020,
OopsElezen = 0x000040,
OopsLalafell = 0x000080,
OopsMiqote = 0x000100,
OopsRoegadyn = 0x000200,
OopsAuRa = 0x000400,
OopsHrothgar = 0x000800,
OopsViera = 0x001000,
Artisan = 0x002000,
SixtyThree = 0x004000,
Shirts = 0x008000,
World = 0x010000,
Elephants = 0x020000,
Crown = 0x040000,
}
public bool EnabledClown { get; private set; }
public bool EnabledEmperor { get; private set; }
public bool EnabledIndividual { get; private set; }
public Sizing EnabledSizing { get; private set; }
public Race EnabledOops { get; private set; }
public bool EnabledArtisan { get; private set; }
public bool EnabledCaptain { get; private set; }
public bool Enabled63 { get; private set; }
public bool EnabledShirts { get; private set; }
public bool EnabledWorld { get; private set; }
public const CodeFlag DyeCodes = CodeFlag.Clown | CodeFlag.World | CodeFlag.Elephants;
public const CodeFlag GearCodes = CodeFlag.Emperor | CodeFlag.World | CodeFlag.Elephants;
public const CodeFlag RaceCodes = CodeFlag.OopsHyur
| CodeFlag.OopsElezen
| CodeFlag.OopsLalafell
| CodeFlag.OopsMiqote
| CodeFlag.OopsRoegadyn
| CodeFlag.OopsAuRa
| CodeFlag.OopsHrothgar;
public const CodeFlag SizeCodes = CodeFlag.Dwarf | CodeFlag.Giant;
private CodeFlag _enabled;
public bool Enabled(CodeFlag flag)
=> _enabled.HasFlag(flag);
public bool AnyEnabled(CodeFlag flag)
=> (_enabled & flag) != 0;
public CodeFlag Masked(CodeFlag mask)
=> _enabled & mask;
public Race GetRace()
=> (_enabled & RaceCodes) switch
{
CodeFlag.OopsHyur => Race.Hyur,
CodeFlag.OopsElezen => Race.Elezen,
CodeFlag.OopsLalafell => Race.Lalafell,
CodeFlag.OopsMiqote => Race.Miqote,
CodeFlag.OopsRoegadyn => Race.Roegadyn,
CodeFlag.OopsAuRa => Race.AuRa,
CodeFlag.OopsHrothgar => Race.Hrothgar,
CodeFlag.OopsViera => Race.Viera,
_ => Race.Unknown,
};
public CodeService(Configuration config)
{
@ -69,92 +113,96 @@ public class CodeService
}
public Action<bool>? CheckCode(string name)
{
var flag = GetCode(name);
if (flag == 0)
return null;
var badFlags = ~GetMutuallyExclusive(flag);
return v => _enabled = v ? (_enabled | flag) & badFlags : _enabled & ~flag;;
}
public CodeFlag GetCode(string name)
{
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(name));
var sha = (ReadOnlySpan<byte>)_hasher.ComputeHash(stream);
return sha switch
{
_ when CodeClown.SequenceEqual(sha) => v => EnabledClown = v,
_ when CodeEmperor.SequenceEqual(sha) => v => EnabledEmperor = v,
_ when CodeIndividual.SequenceEqual(sha) => v => EnabledIndividual = v,
_ when CodeCaptain.SequenceEqual(sha) => v => EnabledCaptain = v,
_ when Code63.SequenceEqual(sha) => v => Enabled63 = v,
_ when CodeDwarf.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Dwarf : Sizing.None,
_ when CodeGiant.SequenceEqual(sha) => v => EnabledSizing = v ? Sizing.Giant : Sizing.None,
_ when CodeOops1.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hyur : Race.Unknown,
_ when CodeOops2.SequenceEqual(sha) => v => EnabledOops = v ? Race.Elezen : Race.Unknown,
_ when CodeOops3.SequenceEqual(sha) => v => EnabledOops = v ? Race.Lalafell : Race.Unknown,
_ when CodeOops4.SequenceEqual(sha) => v => EnabledOops = v ? Race.Miqote : Race.Unknown,
_ when CodeOops5.SequenceEqual(sha) => v => EnabledOops = v ? Race.Roegadyn : Race.Unknown,
_ when CodeOops6.SequenceEqual(sha) => v => EnabledOops = v ? Race.AuRa : Race.Unknown,
_ when CodeOops7.SequenceEqual(sha) => v => EnabledOops = v ? Race.Hrothgar : Race.Unknown,
_ when CodeOops8.SequenceEqual(sha) => v => EnabledOops = v ? Race.Viera : Race.Unknown,
_ when CodeArtisan.SequenceEqual(sha) => v => EnabledArtisan = v,
_ when CodeShirts.SequenceEqual(sha) => v => EnabledShirts = v,
_ when CodeWorld.SequenceEqual(sha) => v => EnabledWorld = v,
_ => null,
};
foreach (var flag in Enum.GetValues<CodeFlag>())
{
if (sha.SequenceEqual(GetSha(flag)))
return flag;
}
return 0;
}
public void VerifyState()
/// <summary> Update all enabled states in the config. </summary>
public void SaveState()
{
if (EnabledSizing == Sizing.None && EnabledOops == Race.Unknown)
return;
for (var i = 0; i < _config.Codes.Count; ++i)
{
var (code, enabled) = _config.Codes[i];
if (!enabled)
continue;
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(code));
var sha = (ReadOnlySpan<byte>)_hasher.ComputeHash(stream);
var _ = EnabledSizing switch
var name = _config.Codes[i].Code;
var flag = GetCode(name);
if (flag == 0)
{
Sizing.Dwarf when sha.SequenceEqual(CodeGiant) => _config.Codes[i] = (code, false),
Sizing.Giant when sha.SequenceEqual(CodeDwarf) => _config.Codes[i] = (code, false),
_ => (string.Empty, false),
};
_config.Codes.RemoveAt(i--);
continue;
}
var race = OopsRace(sha);
if (race is not Race.Unknown && race != EnabledOops)
_config.Codes[i] = (code, false);
_config.Codes[i] = (name, Enabled(flag));
}
_config.Save();
}
private static Race OopsRace(ReadOnlySpan<byte> sha)
=> sha switch
{
_ when CodeOops1.SequenceEqual(sha) => Race.Hyur,
_ when CodeOops2.SequenceEqual(sha) => Race.Elezen,
_ when CodeOops3.SequenceEqual(sha) => Race.Lalafell,
_ when CodeOops4.SequenceEqual(sha) => Race.Miqote,
_ when CodeOops5.SequenceEqual(sha) => Race.Roegadyn,
_ when CodeOops6.SequenceEqual(sha) => Race.AuRa,
_ when CodeOops7.SequenceEqual(sha) => Race.Hrothgar,
_ when CodeOops8.SequenceEqual(sha) => Race.Viera,
_ => Race.Unknown,
};
// @formatter:off
private static ReadOnlySpan<byte> CodeClown => new byte[] { 0xC4, 0xEE, 0x1D, 0x6F, 0xC5, 0x5D, 0x47, 0xBE, 0x78, 0x63, 0x66, 0x86, 0x81, 0x15, 0xEB, 0xFA, 0xF6, 0x4A, 0x90, 0xEA, 0xC0, 0xE4, 0xEE, 0x86, 0x69, 0x01, 0x8E, 0xDB, 0xCC, 0x69, 0xD1, 0xBD };
private static ReadOnlySpan<byte> CodeEmperor => new byte[] { 0xE2, 0x2D, 0x3E, 0x57, 0x16, 0x82, 0x65, 0x98, 0x7E, 0xE6, 0x8F, 0x45, 0x14, 0x7D, 0x65, 0x31, 0xE9, 0xD8, 0xDB, 0xEA, 0xDC, 0xBF, 0xEE, 0x2A, 0xBA, 0xD5, 0x69, 0x96, 0x78, 0x34, 0x3B, 0x57 };
private static ReadOnlySpan<byte> CodeIndividual => new byte[] { 0x95, 0xA4, 0x71, 0xAC, 0xA3, 0xC2, 0x34, 0x94, 0xC1, 0x65, 0x07, 0xF3, 0x7F, 0x93, 0x57, 0xEE, 0xE3, 0x04, 0xC0, 0xE8, 0x1B, 0xA0, 0xE2, 0x08, 0x68, 0x02, 0x8D, 0xAD, 0x76, 0x03, 0x9B, 0xC5 };
private static ReadOnlySpan<byte> CodeDwarf => new byte[] { 0x55, 0x97, 0xFE, 0xE9, 0x78, 0x64, 0xE8, 0x2F, 0xCD, 0x25, 0xD1, 0xAE, 0xDF, 0x35, 0xE6, 0xED, 0x03, 0x78, 0x54, 0x1D, 0x56, 0x22, 0x34, 0x75, 0x4B, 0x96, 0x6F, 0xBA, 0xAC, 0xEC, 0x00, 0x46 };
private static ReadOnlySpan<byte> CodeGiant => new byte[] { 0x6E, 0xBB, 0x91, 0x1D, 0x67, 0xE3, 0x00, 0x07, 0xA1, 0x0F, 0x2A, 0xF0, 0x26, 0x91, 0x38, 0x63, 0xD3, 0x52, 0x82, 0xF7, 0x5D, 0x93, 0xE8, 0x83, 0xB1, 0xF6, 0xB9, 0x69, 0x78, 0x20, 0xC4, 0xCE };
private static ReadOnlySpan<byte> CodeOops1 => new byte[] { 0x4C, 0x51, 0xE2, 0x38, 0xEF, 0xAD, 0x84, 0x0E, 0x4E, 0x11, 0x0F, 0x5E, 0xDE, 0x45, 0x41, 0x9F, 0x6A, 0xF6, 0x5F, 0x5B, 0xA8, 0x91, 0x64, 0x22, 0xEE, 0x62, 0x97, 0x3C, 0x78, 0x18, 0xCD, 0xAF };
private static ReadOnlySpan<byte> CodeOops2 => new byte[] { 0x3D, 0x5B, 0xA9, 0x62, 0xCE, 0xBE, 0x52, 0xF5, 0x94, 0x2A, 0xF9, 0xB7, 0xCF, 0xD9, 0x24, 0x2B, 0x38, 0xC7, 0x4F, 0x28, 0x97, 0x29, 0x1D, 0x01, 0x13, 0x53, 0x44, 0x11, 0x15, 0x6F, 0x9B, 0x56 };
private static ReadOnlySpan<byte> CodeOops3 => new byte[] { 0x85, 0x8D, 0x5B, 0xC2, 0x66, 0x53, 0x2E, 0xB9, 0xE9, 0x85, 0xE5, 0xF8, 0xD3, 0x75, 0x18, 0x7C, 0x58, 0x55, 0xD4, 0x8C, 0x8E, 0x5F, 0x58, 0x2E, 0xF3, 0xF1, 0xAE, 0xA8, 0xA0, 0x81, 0xC6, 0x0E };
private static ReadOnlySpan<byte> CodeOops4 => new byte[] { 0x44, 0x73, 0x8C, 0x39, 0x5A, 0xF1, 0xDB, 0x5F, 0x62, 0xA1, 0x6E, 0x5F, 0xE6, 0x97, 0x9E, 0x90, 0xD7, 0x5C, 0x97, 0x67, 0xB6, 0xC7, 0x99, 0x61, 0x36, 0xCA, 0x34, 0x7E, 0xB9, 0xAC, 0xC3, 0x76 };
private static ReadOnlySpan<byte> CodeOops5 => new byte[] { 0xB7, 0x25, 0x73, 0xDB, 0xBE, 0xD0, 0x49, 0xFB, 0xFF, 0x9C, 0x32, 0x21, 0xB0, 0x8A, 0x2C, 0x0C, 0x77, 0x46, 0xD5, 0xCF, 0x0E, 0x63, 0x2F, 0x91, 0x85, 0x8B, 0x55, 0x5C, 0x4D, 0xD2, 0xB9, 0xB8 };
private static ReadOnlySpan<byte> CodeOops6 => new byte[] { 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 };
private static ReadOnlySpan<byte> CodeOops7 => new byte[] { 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 };
private static ReadOnlySpan<byte> CodeOops8 => new byte[] { 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B };
private static ReadOnlySpan<byte> CodeArtisan => new byte[] { 0xDE, 0x01, 0x32, 0x1E, 0x7F, 0x22, 0x80, 0x3D, 0x76, 0xDF, 0x74, 0x0E, 0xEC, 0x33, 0xD3, 0xF4, 0x1A, 0x98, 0x9E, 0x9D, 0x22, 0x5C, 0xAC, 0x3B, 0xFE, 0x0B, 0xC2, 0x13, 0xB9, 0x91, 0x24, 0x61 };
private static ReadOnlySpan<byte> CodeCaptain => new byte[] { 0x5E, 0x0B, 0xDD, 0x86, 0x8F, 0x24, 0xDA, 0x49, 0x1A, 0xD2, 0x59, 0xB9, 0x10, 0x38, 0x29, 0x37, 0x99, 0x9D, 0x53, 0xD9, 0x9B, 0x84, 0x91, 0x5B, 0x6C, 0xCE, 0x3E, 0x2A, 0x38, 0x06, 0x47, 0xE6 };
private static ReadOnlySpan<byte> Code63 => new byte[] { 0xA1, 0x65, 0x60, 0x99, 0xB0, 0x9F, 0xBF, 0xD7, 0x20, 0xC8, 0x29, 0xF6, 0x7B, 0x86, 0x27, 0xF5, 0xBE, 0xA9, 0x5B, 0xB0, 0x20, 0x5E, 0x57, 0x7B, 0xFF, 0xBC, 0x1E, 0x8C, 0x04, 0xF9, 0x35, 0xD3 };
private static ReadOnlySpan<byte> CodeShirts => new byte[] { 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB };
private static ReadOnlySpan<byte> CodeWorld => new byte[] { 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 };
// @formatter:on
private static CodeFlag GetMutuallyExclusive(CodeFlag flag)
=> flag switch
{
CodeFlag.Clown => DyeCodes & ~CodeFlag.Clown,
CodeFlag.Emperor => GearCodes & ~CodeFlag.Emperor,
CodeFlag.Individual => 0,
CodeFlag.Dwarf => SizeCodes & ~CodeFlag.Dwarf,
CodeFlag.Giant => SizeCodes & ~CodeFlag.Giant,
CodeFlag.OopsHyur => RaceCodes & ~CodeFlag.OopsHyur,
CodeFlag.OopsElezen => RaceCodes & ~CodeFlag.OopsElezen,
CodeFlag.OopsLalafell => RaceCodes & ~CodeFlag.OopsLalafell,
CodeFlag.OopsMiqote => RaceCodes & ~CodeFlag.OopsMiqote,
CodeFlag.OopsRoegadyn => RaceCodes & ~CodeFlag.OopsRoegadyn,
CodeFlag.OopsAuRa => RaceCodes & ~CodeFlag.OopsAuRa,
CodeFlag.OopsHrothgar => RaceCodes & ~CodeFlag.OopsHrothgar,
CodeFlag.OopsViera => RaceCodes & ~CodeFlag.OopsViera,
CodeFlag.Artisan => 0,
CodeFlag.SixtyThree => 0,
CodeFlag.Shirts => 0,
CodeFlag.World => (DyeCodes | GearCodes) & ~CodeFlag.World,
CodeFlag.Elephants => (DyeCodes | GearCodes) & ~CodeFlag.Elephants,
CodeFlag.Crown => 0,
_ => 0,
};
private static ReadOnlySpan<byte> GetSha(CodeFlag flag)
=> flag switch
{
CodeFlag.Clown => [ 0xC4, 0xEE, 0x1D, 0x6F, 0xC5, 0x5D, 0x47, 0xBE, 0x78, 0x63, 0x66, 0x86, 0x81, 0x15, 0xEB, 0xFA, 0xF6, 0x4A, 0x90, 0xEA, 0xC0, 0xE4, 0xEE, 0x86, 0x69, 0x01, 0x8E, 0xDB, 0xCC, 0x69, 0xD1, 0xBD ],
CodeFlag.Emperor => [ 0xE2, 0x2D, 0x3E, 0x57, 0x16, 0x82, 0x65, 0x98, 0x7E, 0xE6, 0x8F, 0x45, 0x14, 0x7D, 0x65, 0x31, 0xE9, 0xD8, 0xDB, 0xEA, 0xDC, 0xBF, 0xEE, 0x2A, 0xBA, 0xD5, 0x69, 0x96, 0x78, 0x34, 0x3B, 0x57 ],
CodeFlag.Individual => [ 0x95, 0xA4, 0x71, 0xAC, 0xA3, 0xC2, 0x34, 0x94, 0xC1, 0x65, 0x07, 0xF3, 0x7F, 0x93, 0x57, 0xEE, 0xE3, 0x04, 0xC0, 0xE8, 0x1B, 0xA0, 0xE2, 0x08, 0x68, 0x02, 0x8D, 0xAD, 0x76, 0x03, 0x9B, 0xC5 ],
CodeFlag.Dwarf => [ 0x55, 0x97, 0xFE, 0xE9, 0x78, 0x64, 0xE8, 0x2F, 0xCD, 0x25, 0xD1, 0xAE, 0xDF, 0x35, 0xE6, 0xED, 0x03, 0x78, 0x54, 0x1D, 0x56, 0x22, 0x34, 0x75, 0x4B, 0x96, 0x6F, 0xBA, 0xAC, 0xEC, 0x00, 0x46 ],
CodeFlag.Giant => [ 0x6E, 0xBB, 0x91, 0x1D, 0x67, 0xE3, 0x00, 0x07, 0xA1, 0x0F, 0x2A, 0xF0, 0x26, 0x91, 0x38, 0x63, 0xD3, 0x52, 0x82, 0xF7, 0x5D, 0x93, 0xE8, 0x83, 0xB1, 0xF6, 0xB9, 0x69, 0x78, 0x20, 0xC4, 0xCE ],
CodeFlag.OopsHyur => [ 0x4C, 0x51, 0xE2, 0x38, 0xEF, 0xAD, 0x84, 0x0E, 0x4E, 0x11, 0x0F, 0x5E, 0xDE, 0x45, 0x41, 0x9F, 0x6A, 0xF6, 0x5F, 0x5B, 0xA8, 0x91, 0x64, 0x22, 0xEE, 0x62, 0x97, 0x3C, 0x78, 0x18, 0xCD, 0xAF ],
CodeFlag.OopsElezen => [ 0x3D, 0x5B, 0xA9, 0x62, 0xCE, 0xBE, 0x52, 0xF5, 0x94, 0x2A, 0xF9, 0xB7, 0xCF, 0xD9, 0x24, 0x2B, 0x38, 0xC7, 0x4F, 0x28, 0x97, 0x29, 0x1D, 0x01, 0x13, 0x53, 0x44, 0x11, 0x15, 0x6F, 0x9B, 0x56 ],
CodeFlag.OopsLalafell => [ 0x85, 0x8D, 0x5B, 0xC2, 0x66, 0x53, 0x2E, 0xB9, 0xE9, 0x85, 0xE5, 0xF8, 0xD3, 0x75, 0x18, 0x7C, 0x58, 0x55, 0xD4, 0x8C, 0x8E, 0x5F, 0x58, 0x2E, 0xF3, 0xF1, 0xAE, 0xA8, 0xA0, 0x81, 0xC6, 0x0E ],
CodeFlag.OopsMiqote => [ 0x44, 0x73, 0x8C, 0x39, 0x5A, 0xF1, 0xDB, 0x5F, 0x62, 0xA1, 0x6E, 0x5F, 0xE6, 0x97, 0x9E, 0x90, 0xD7, 0x5C, 0x97, 0x67, 0xB6, 0xC7, 0x99, 0x61, 0x36, 0xCA, 0x34, 0x7E, 0xB9, 0xAC, 0xC3, 0x76 ],
CodeFlag.OopsRoegadyn => [ 0xB7, 0x25, 0x73, 0xDB, 0xBE, 0xD0, 0x49, 0xFB, 0xFF, 0x9C, 0x32, 0x21, 0xB0, 0x8A, 0x2C, 0x0C, 0x77, 0x46, 0xD5, 0xCF, 0x0E, 0x63, 0x2F, 0x91, 0x85, 0x8B, 0x55, 0x5C, 0x4D, 0xD2, 0xB9, 0xB8 ],
CodeFlag.OopsAuRa => [ 0x69, 0x93, 0xAF, 0xE4, 0xB8, 0xEC, 0x5F, 0x40, 0xEB, 0x8A, 0x6F, 0xD1, 0x9B, 0xD9, 0x56, 0x0B, 0xEA, 0x64, 0x79, 0x9B, 0x54, 0xA1, 0x41, 0xED, 0xBC, 0x3E, 0x6E, 0x5C, 0xF1, 0x23, 0x60, 0xF8 ],
CodeFlag.OopsHrothgar => [ 0x41, 0xEC, 0x65, 0x05, 0x8D, 0x20, 0x68, 0x5A, 0xB7, 0xEB, 0x92, 0x15, 0x43, 0xCF, 0x15, 0x05, 0x27, 0x51, 0xFE, 0x20, 0xC9, 0xB6, 0x2B, 0x84, 0xD9, 0x6A, 0x49, 0x5A, 0x5B, 0x7F, 0x2E, 0xE7 ],
CodeFlag.OopsViera => [ 0x16, 0xFF, 0x63, 0x85, 0x1C, 0xF5, 0x34, 0x33, 0x67, 0x8C, 0x46, 0x8E, 0x3E, 0xE3, 0xA6, 0x94, 0xF9, 0x74, 0x47, 0xAA, 0xC7, 0x29, 0x59, 0x1F, 0x6C, 0x6E, 0xF2, 0xF5, 0x87, 0x24, 0x9E, 0x2B ],
CodeFlag.Artisan => [ 0xDE, 0x01, 0x32, 0x1E, 0x7F, 0x22, 0x80, 0x3D, 0x76, 0xDF, 0x74, 0x0E, 0xEC, 0x33, 0xD3, 0xF4, 0x1A, 0x98, 0x9E, 0x9D, 0x22, 0x5C, 0xAC, 0x3B, 0xFE, 0x0B, 0xC2, 0x13, 0xB9, 0x91, 0x24, 0x61 ],
CodeFlag.SixtyThree => [ 0xA1, 0x65, 0x60, 0x99, 0xB0, 0x9F, 0xBF, 0xD7, 0x20, 0xC8, 0x29, 0xF6, 0x7B, 0x86, 0x27, 0xF5, 0xBE, 0xA9, 0x5B, 0xB0, 0x20, 0x5E, 0x57, 0x7B, 0xFF, 0xBC, 0x1E, 0x8C, 0x04, 0xF9, 0x35, 0xD3 ],
CodeFlag.Shirts => [ 0xD1, 0x35, 0xD7, 0x18, 0xBE, 0x45, 0x42, 0xBD, 0x88, 0x77, 0x7E, 0xC4, 0x41, 0x06, 0x34, 0x4D, 0x71, 0x3A, 0xC5, 0xCC, 0xA4, 0x1B, 0x7D, 0x3F, 0x3B, 0x86, 0x07, 0xCB, 0x63, 0xD7, 0xF9, 0xDB ],
CodeFlag.World => [ 0xFD, 0xA2, 0xD2, 0xBC, 0xD9, 0x8A, 0x7E, 0x2B, 0x52, 0xCB, 0x57, 0x6E, 0x3A, 0x2E, 0x30, 0xBA, 0x4E, 0xAE, 0x42, 0xEA, 0x5C, 0x57, 0xDF, 0x17, 0x37, 0x3C, 0xCE, 0x17, 0x42, 0x43, 0xAE, 0xD0 ],
CodeFlag.Elephants => [ 0x9F, 0x4C, 0xCF, 0x6D, 0xC4, 0x01, 0x31, 0x46, 0x02, 0x05, 0x31, 0xED, 0xED, 0xB2, 0x66, 0x29, 0x31, 0x09, 0x1E, 0xE7, 0x47, 0xDE, 0x7B, 0x03, 0xB0, 0x3C, 0x06, 0x76, 0x26, 0x91, 0xDF, 0xB2 ],
CodeFlag.Crown => [ 0x43, 0x8E, 0x34, 0x56, 0x24, 0xC9, 0xC6, 0xDE, 0x2A, 0x68, 0x3A, 0x5D, 0xF5, 0x8E, 0xCB, 0xEF, 0x0D, 0x4D, 0x5B, 0xDC, 0x23, 0xF9, 0xF9, 0xBD, 0xD9, 0x60, 0xAD, 0x53, 0xC5, 0xA0, 0x33, 0xC4 ],
_ => [],
};
}

View file

@ -5,17 +5,16 @@ using Dalamud.Game.Command;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Plugin.Services;
using Glamourer.Automation;
using Glamourer.Customization;
using Glamourer.Designs;
using Glamourer.Events;
using Glamourer.Gui;
using Glamourer.Interop;
using Glamourer.State;
using Glamourer.Structs;
using ImGuiNET;
using OtterGui;
using OtterGui.Classes;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
namespace Glamourer.Services;
@ -27,7 +26,7 @@ public class CommandService : IDisposable
private readonly ICommandManager _commands;
private readonly MainWindow _mainWindow;
private readonly IChatGui _chat;
private readonly ActorService _actors;
private readonly ActorManager _actors;
private readonly ObjectManager _objects;
private readonly StateManager _stateManager;
private readonly AutoDesignApplier _autoDesignApplier;
@ -37,7 +36,7 @@ public class CommandService : IDisposable
private readonly DesignFileSystem _designFileSystem;
private readonly Configuration _config;
public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorService actors, ObjectManager objects,
public CommandService(ICommandManager commands, MainWindow mainWindow, IChatGui chat, ActorManager actors, ObjectManager objects,
AutoDesignApplier autoDesignApplier, StateManager stateManager, DesignManager designManager, DesignConverter converter,
DesignFileSystem designFileSystem, AutoDesignManager autoDesignManager, Configuration config)
{
@ -86,7 +85,8 @@ public class CommandService : IDisposable
return;
default:
_chat.Print("Use without argument to toggle the main window.");
_chat.Print(new SeStringBuilder().AddText("Use ").AddPurple("/glamour").AddText(" instead of ").AddRed("/glamourer").AddText(" for application commands.").BuiltString);
_chat.Print(new SeStringBuilder().AddText("Use ").AddPurple("/glamour").AddText(" instead of ").AddRed("/glamourer")
.AddText(" for application commands.").BuiltString);
_chat.Print(new SeStringBuilder().AddCommand("qdb", "Toggles the quick design bar on or off.").BuiltString);
_chat.Print(new SeStringBuilder().AddCommand("lock", "Toggles the lock of the main window on or off.").BuiltString);
return;
@ -114,6 +114,7 @@ public class CommandService : IDisposable
"automation" => SetAutomation(argument),
"copy" => CopyState(argument),
"save" => SaveState(argument),
"delete" => Delete(argument),
_ => PrintHelp(argumentList[0]),
};
}
@ -402,7 +403,7 @@ public class CommandService : IDisposable
{
foreach (var actor in actors.Objects)
{
if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors.AwaitedService), actor, out var state))
if (_stateManager.GetOrCreate(actor.GetIdentifier(_actors), actor, out var state))
_stateManager.ApplyDesign(design, state, StateChanged.Source.Manual);
}
}
@ -411,6 +412,33 @@ public class CommandService : IDisposable
return true;
}
private bool Delete(string argument)
{
if (argument.Length == 0)
{
_chat.Print(new SeStringBuilder().AddText("Use with /glamour delete ").AddYellow("[Design Name, Path or Identifier]").BuiltString);
_chat.Print(new SeStringBuilder()
.AddText(
" 》 The design name is case-insensitive. If multiple designs of that name up to case exist, the first one is chosen.")
.BuiltString);
_chat.Print(new SeStringBuilder()
.AddText(
" 》 If using the design identifier, you need to specify at least 4 characters for it, and the first one starting with the provided characters is chosen.")
.BuiltString);
_chat.Print(new SeStringBuilder()
.AddText(" 》 The design path is the folder path in the selector, with '/' as separators. It is also case-insensitive.")
.BuiltString);
return false;
}
if (!GetDesign(argument, out var designBase, false) || designBase is not Design d)
return false;
_designManager.Delete(d);
return true;
}
private bool CopyState(string argument)
{
if (argument.Length == 0)
@ -521,14 +549,13 @@ public class CommandService : IDisposable
design = leaf.Value;
}
if (design == null)
{
_chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.")
.BuiltString);
return false;
}
if (design != null)
return true;
_chat.Print(new SeStringBuilder().AddText("The token ").AddYellow(argument, true).AddText(" did not resolve to an existing design.")
.BuiltString);
return false;
return true;
}
private unsafe bool IdentifierHandling(string argument, out ActorIdentifier[] identifiers, bool allowAnyWorld, bool allowIndex)
@ -537,7 +564,7 @@ public class CommandService : IDisposable
{
if (_objects.GetName(argument.ToLowerInvariant(), out var obj))
{
var identifier = _actors.AwaitedService.FromObject(obj.AsObject, out _, true, true, true);
var identifier = _actors.FromObject(obj.AsObject, out _, true, true, true);
if (!identifier.IsValid)
{
_chat.Print(new SeStringBuilder().AddText("The placeholder ").AddGreen(argument)
@ -547,15 +574,15 @@ public class CommandService : IDisposable
}
if (allowIndex && identifier.Type is IdentifierType.Npc)
identifier = _actors.AwaitedService.CreateNpc(identifier.Kind, identifier.DataId, obj.Index);
identifiers = new[]
{
identifier = _actors.CreateNpc(identifier.Kind, identifier.DataId, obj.Index);
identifiers =
[
identifier,
};
];
}
else
{
identifiers = _actors.AwaitedService.FromUserString(argument, allowIndex);
identifiers = _actors.FromUserString(argument, allowIndex);
if (!allowAnyWorld
&& identifiers[0].Type is IdentifierType.Player or IdentifierType.Owned
&& identifiers[0].HomeWorld == ushort.MaxValue)
@ -568,7 +595,7 @@ public class CommandService : IDisposable
return true;
}
catch (ActorManager.IdentifierParseError e)
catch (ActorIdentifierFactory.IdentifierParseError e)
{
_chat.Print(new SeStringBuilder().AddText("The argument ").AddRed(argument, true)
.AddText($" could not be converted to an identifier. {e.Message}")

View file

@ -1,27 +1,36 @@
using System.Linq;
using System.Runtime.CompilerServices;
using Dalamud.Plugin.Services;
using Glamourer.Customization;
using Penumbra.GameData.Data;
using System.Threading.Tasks;
using Glamourer.GameData;
using OtterGui.Services;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Structs;
namespace Glamourer.Services;
public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationManager>
public sealed class CustomizeService(
HumanModelList humanModels,
NpcCustomizeSet npcCustomizeSet,
CustomizeManager manager)
: IAsyncService
{
public readonly HumanModelList HumanModels;
public readonly HumanModelList HumanModels = humanModels;
public readonly CustomizeManager Manager = manager;
public readonly NpcCustomizeSet NpcCustomizeSet = npcCustomizeSet;
public CustomizationService(ITextureProvider textures, IDataManager gameData, HumanModelList humanModels, IPluginLog log)
: base(nameof(CustomizationService), () => CustomizationManager.Create(textures, gameData, log))
=> HumanModels = humanModels;
public Task Awaiter { get; }
= Task.WhenAll(humanModels.Awaiter, manager.Awaiter, npcCustomizeSet.Awaiter);
public (Customize NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(Customize oldValues, Customize newValues,
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
public (CustomizeArray NewValue, CustomizeFlag Applied, CustomizeFlag Changed) Combine(CustomizeArray oldValues, CustomizeArray newValues,
CustomizeFlag applyWhich, bool allowUnknown)
{
CustomizeFlag applied = 0;
CustomizeFlag changed = 0;
Customize ret = default;
ret.Load(oldValues);
var ret = oldValues;
if (applyWhich.HasFlag(CustomizeFlag.Clan))
{
changed |= ChangeClan(ref ret, newValues.Clan);
@ -35,8 +44,18 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
applied |= CustomizeFlag.Gender;
}
if (applyWhich.HasFlag(CustomizeFlag.BodyType))
{
if (oldValues[CustomizeIndex.BodyType] != newValues[CustomizeIndex.BodyType])
{
ret.Set(CustomizeIndex.BodyType, newValues[CustomizeIndex.BodyType]);
changed |= CustomizeFlag.BodyType;
}
var set = AwaitedService.GetList(ret.Clan, ret.Gender);
applied |= CustomizeFlag.BodyType;
}
var set = Manager.GetSet(ret.Clan, ret.Gender);
applyWhich = applyWhich.FixApplication(set);
foreach (var index in CustomizationExtensions.AllBasic)
{
@ -64,69 +83,34 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
gender = Gender.Female;
if (gender == Gender.MaleNpc)
gender = Gender.Male;
return (gender, race) switch
{
(Gender.Male, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderM),
(Gender.Male, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderM),
(Gender.Male, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodM),
(Gender.Male, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightM),
(Gender.Male, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkM),
(Gender.Male, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkM),
(Gender.Male, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunM),
(Gender.Male, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonM),
(Gender.Male, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfM),
(Gender.Male, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardM),
(Gender.Male, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenM),
(Gender.Male, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaM),
(Gender.Male, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Male, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Male, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaM),
(Gender.Male, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaM),
(Gender.Female, SubRace.Midlander) => AwaitedService.GetName(CustomName.MidlanderF),
(Gender.Female, SubRace.Highlander) => AwaitedService.GetName(CustomName.HighlanderF),
(Gender.Female, SubRace.Wildwood) => AwaitedService.GetName(CustomName.WildwoodF),
(Gender.Female, SubRace.Duskwight) => AwaitedService.GetName(CustomName.DuskwightF),
(Gender.Female, SubRace.Plainsfolk) => AwaitedService.GetName(CustomName.PlainsfolkF),
(Gender.Female, SubRace.Dunesfolk) => AwaitedService.GetName(CustomName.DunesfolkF),
(Gender.Female, SubRace.SeekerOfTheSun) => AwaitedService.GetName(CustomName.SeekerOfTheSunF),
(Gender.Female, SubRace.KeeperOfTheMoon) => AwaitedService.GetName(CustomName.KeeperOfTheMoonF),
(Gender.Female, SubRace.Seawolf) => AwaitedService.GetName(CustomName.SeawolfF),
(Gender.Female, SubRace.Hellsguard) => AwaitedService.GetName(CustomName.HellsguardF),
(Gender.Female, SubRace.Raen) => AwaitedService.GetName(CustomName.RaenF),
(Gender.Female, SubRace.Xaela) => AwaitedService.GetName(CustomName.XaelaF),
(Gender.Female, SubRace.Helion) => AwaitedService.GetName(CustomName.HelionM),
(Gender.Female, SubRace.Lost) => AwaitedService.GetName(CustomName.LostM),
(Gender.Female, SubRace.Rava) => AwaitedService.GetName(CustomName.RavaF),
(Gender.Female, SubRace.Veena) => AwaitedService.GetName(CustomName.VeenaF),
_ => "Unknown",
};
return Manager.GetSet(race, gender).Name;
}
/// <summary> Returns whether a clan is valid. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsClanValid(SubRace clan)
=> AwaitedService.Clans.Contains(clan);
=> CustomizeManager.Clans.Contains(clan);
/// <summary> Returns whether a gender is valid for the given race. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsGenderValid(Race race, Gender gender)
=> race is Race.Hrothgar ? gender == Gender.Male : AwaitedService.Genders.Contains(gender);
=> race is Race.Hrothgar ? gender == Gender.Male : CustomizeManager.Genders.Contains(gender);
/// <inheritdoc cref="IsCustomizationValid(CustomizationSet,CustomizeValue,CustomizeIndex,CustomizeValue, out CustomizeData?)"/>
/// <inheritdoc cref="IsCustomizationValid(CustomizeSet,CustomizeValue,CustomizeIndex,CustomizeValue, out CustomizeData?)"/>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
=> IsCustomizationValid(set, face, type, value, out _);
/// <summary> Returns whether a customization value is valid for a given clan/gender set and face. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public static bool IsCustomizationValid(CustomizationSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value,
public static bool IsCustomizationValid(CustomizeSet set, CustomizeValue face, CustomizeIndex type, CustomizeValue value,
out CustomizeData? data)
=> set.Validate(type, value, out data, face);
/// <summary> Returns whether a customization value is valid for a given clan, gender and face. </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
public bool IsCustomizationValid(SubRace race, Gender gender, CustomizeValue face, CustomizeIndex type, CustomizeValue value)
=> IsCustomizationValid(AwaitedService.GetList(race, gender), face, type, value);
=> IsCustomizationValid(Manager.GetSet(race, gender), face, type, value);
/// <summary>
/// Check that the given race and clan are valid.
@ -145,10 +129,10 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
return string.Empty;
}
if (AwaitedService.Races.Contains(race))
if (CustomizeManager.Races.Contains(race))
{
actualRace = race;
actualClan = AwaitedService.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown);
actualClan = CustomizeManager.Clans.FirstOrDefault(c => c.ToRace() == race, SubRace.Unknown);
// This should not happen.
if (actualClan == SubRace.Unknown)
{
@ -174,7 +158,7 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
/// </summary>
public string ValidateGender(Race race, Gender gender, out Gender actualGender)
{
if (!AwaitedService.Genders.Contains(gender))
if (!CustomizeManager.Genders.Contains(gender))
{
actualGender = Gender.Male;
return $"The gender {gender.ToName()} is unknown, reset to {Gender.Male.ToName()}.";
@ -215,7 +199,7 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
/// The returned actualValue is either the correct value or the one with index 0.
/// The return value is an empty string or a warning message.
/// </summary>
public static string ValidateCustomizeValue(CustomizationSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value,
public static string ValidateCustomizeValue(CustomizeSet set, CustomizeValue face, CustomizeIndex index, CustomizeValue value,
out CustomizeValue actualValue, bool allowUnknown)
{
if (allowUnknown || IsCustomizationValid(set, face, index, value))
@ -232,7 +216,7 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
}
/// <summary> Change a clan while keeping all other customizations valid. </summary>
public CustomizeFlag ChangeClan(ref Customize customize, SubRace newClan)
public CustomizeFlag ChangeClan(ref CustomizeArray customize, SubRace newClan)
{
if (customize.Clan == newClan)
return 0;
@ -251,12 +235,12 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
flags |= CustomizeFlag.Gender;
}
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
var set = Manager.GetSet(customize.Clan, customize.Gender);
return FixValues(set, ref customize) | flags;
}
/// <summary> Change a gender while keeping all other customizations valid. </summary>
public CustomizeFlag ChangeGender(ref Customize customize, Gender newGender)
public CustomizeFlag ChangeGender(ref CustomizeArray customize, Gender newGender)
{
if (customize.Gender == newGender)
return 0;
@ -269,11 +253,11 @@ public sealed class CustomizationService : AsyncServiceWrapper<ICustomizationMan
return 0;
customize.Gender = newGender;
var set = AwaitedService.GetList(customize.Clan, customize.Gender);
var set = Manager.GetSet(customize.Clan, customize.Gender);
return FixValues(set, ref customize) | CustomizeFlag.Gender;
}
private static CustomizeFlag FixValues(CustomizationSet set, ref Customize customize)
private static CustomizeFlag FixValues(CustomizeSet set, ref CustomizeArray customize)
{
CustomizeFlag flags = 0;
foreach (var idx in CustomizationExtensions.AllBasic)

Some files were not shown because too many files have changed in this diff Show more