Compare commits

..

36 commits

Author SHA1 Message Date
Actions User
5b6517aae8 [CI] Updating repo.json for 1.5.1.5 2025-11-28 22:09:08 +00:00
Ottermandias
aadcf771e7 1.5.1.5 2025-11-28 23:06:53 +01:00
Ottermandias
bf4673a1d9 Save Remove and Inherit for mod associations. 2025-11-07 23:30:18 +01:00
Ottermandias
76b214c643 Fix try-on interaction with Penumbra for facewear. 2025-11-07 23:30:18 +01:00
Ottermandias
434a5a809e Make old backup files overwrite instead of throwing. 2025-11-07 23:30:18 +01:00
Actions User
88fe25f69e [CI] Updating repo.json for testing_1.5.1.4 2025-10-23 15:40:25 +00:00
Ottermandias
bef1e39ac3 Update Libraries. 2025-10-23 17:37:27 +02:00
Ottermandias
c604d5dbe5 Add DeletePlayerState. 2025-10-23 17:25:59 +02:00
Ottermandias
a0d912a395 Fix issue with reverting state of unavailable actors. 2025-10-23 17:25:59 +02:00
Actions User
a56852f918 [CI] Updating repo.json for 1.5.1.3 2025-10-07 11:02:02 +00:00
Ottermandias
4228fc1b89 fu 2025-10-07 12:59:39 +02:00
Ottermandias
e644b8da28 Fix span issue. 2025-10-07 12:53:55 +02:00
Ottermandias
ace3a8f755 Again. 2025-10-07 12:43:40 +02:00
Ottermandias
76ed347cbf Update signatures. 2025-10-07 12:28:18 +02:00
Ottermandias
c3469a1687 Fix facewear advanced dyes, fix backup service not running in task, update libraries. 2025-09-28 23:55:44 +02:00
Ottermandias
0a9693daea
Update CodeService.cs 2025-09-15 20:29:13 +02:00
Actions User
414bd8bee7 [CI] Updating repo.json for 1.5.1.2 2025-08-28 16:52:43 +00:00
Ottermandias
8e1745d67a Once more with feeling 2025-08-28 18:47:57 +02:00
Actions User
889f01a724 [CI] Updating repo.json for 1.5.1.1 2025-08-26 09:58:08 +00:00
Ottermandias
6e62905fa7 Fix staging incompatibility with CS. 2025-08-26 11:55:00 +02:00
Actions User
654787fa0d [CI] Updating repo.json for 1.5.1.0 2025-08-25 08:45:28 +00:00
Ottermandias
835ba23935 1.5.1.0 2025-08-25 10:43:14 +02:00
Ottermandias
389a8781d6 Update library. 2025-08-25 10:39:38 +02:00
Actions User
3eabe591df [CI] Updating repo.json for testing_1.5.0.9 2025-08-24 13:59:02 +00:00
Ottermandias
487d3b9399 Update PCP Service. 2025-08-24 15:49:29 +02:00
Actions User
4d4e4669dd [CI] Updating repo.json for testing_1.5.0.8 2025-08-22 18:34:49 +00:00
Ottermandias
fb065549e9 Add PCP Service. 2025-08-22 20:32:32 +02:00
Ottermandias
2c34154915 Update API. 2025-08-22 20:32:32 +02:00
Actions User
3704051b0f [CI] Updating repo.json for 1.5.0.7 2025-08-17 08:45:55 +00:00
Ottermandias
b2b8f2b6eb Make glamourers visor toggle trigger static visors. (?!?) 2025-08-17 10:43:26 +02:00
Ottermandias
22e6c0655b Add ear state when toggling meta application via button. 2025-08-16 11:59:08 +02:00
Ottermandias
bb2ba0cf11 Add glasses to advanced dye slot combo. 2025-08-16 11:59:08 +02:00
Ottermandias
e854386b23 Update OtterGui 2025-08-16 11:59:08 +02:00
Actions User
49d24df2e7 [CI] Updating repo.json for 1.5.0.6 2025-08-12 12:53:44 +00:00
Ottermandias
c9b291c2f3 Add new parameter to LoadWeapon hook. 2025-08-12 14:47:09 +02:00
Actions User
65f789880d [CI] Updating repo.json for 1.5.0.5 2025-08-12 10:32:03 +00:00
36 changed files with 502 additions and 127 deletions

@ -1 +1 @@
Subproject commit 54c1944dc7db704733b4788520e494761bb0b58e
Subproject commit 59a7ab5fa9941eb754757b62e4cb189e455e9514

View file

@ -1,13 +1,13 @@
using Glamourer.Api.Enums;
using Glamourer.Designs;
using Glamourer.State;
using OtterGui;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Enums;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using Penumbra.String;
namespace Glamourer.Api;
@ -15,14 +15,23 @@ namespace Glamourer.Api;
public class ApiHelpers(ActorObjectManager objects, StateManager stateManager, ActorManager actors) : IApiService
{
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]
internal IEnumerable<ActorState> FindExistingStates(string actorName)
internal IEnumerable<ActorState> FindExistingStates(string actorName, ushort worldId = ushort.MaxValue)
{
if (actorName.Length == 0 || !ByteString.FromString(actorName, out var byteString))
yield break;
foreach (var state in stateManager.Values.Where(state
=> state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString))
yield return state;
if (worldId == WorldId.AnyWorld.Id)
{
foreach (var state in stateManager.Values.Where(state
=> state.Identifier.Type is IdentifierType.Player && state.Identifier.PlayerName == byteString))
yield return state;
}
else
{
var identifier = actors.CreatePlayer(byteString, worldId);
if (stateManager.TryGetValue(identifier, out var state))
yield return state;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining | MethodImplOptions.AggressiveOptimization)]

View file

@ -6,7 +6,7 @@ namespace Glamourer.Api;
public class GlamourerApi(DesignsApi designs, StateApi state, ItemsApi items) : IGlamourerApi, IApiService
{
public const int CurrentApiVersionMajor = 1;
public const int CurrentApiVersionMinor = 6;
public const int CurrentApiVersionMinor = 7;
public (int Major, int Minor) ApiVersion
=> (CurrentApiVersionMajor, CurrentApiVersionMinor);

View file

@ -54,6 +54,7 @@ public sealed class IpcProviders : IDisposable, IApiService
IpcSubscribers.RevertStateName.Provider(pi, api.State),
IpcSubscribers.UnlockState.Provider(pi, api.State),
IpcSubscribers.UnlockStateName.Provider(pi, api.State),
IpcSubscribers.DeletePlayerState.Provider(pi, api.State),
IpcSubscribers.UnlockAll.Provider(pi, api.State),
IpcSubscribers.RevertToAutomation.Provider(pi, api.State),
IpcSubscribers.RevertToAutomationName.Provider(pi, api.State),

View file

@ -8,6 +8,7 @@ using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.GameData.Interop;
using Penumbra.GameData.Structs;
using StateChanged = Glamourer.Events.StateChanged;
namespace Glamourer.Api;
@ -17,7 +18,6 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
private readonly ApiHelpers _helpers;
private readonly StateManager _stateManager;
private readonly DesignConverter _converter;
private readonly Configuration _config;
private readonly AutoDesignApplier _autoDesigns;
private readonly ActorObjectManager _objects;
private readonly StateChanged _stateChanged;
@ -27,7 +27,6 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
public StateApi(ApiHelpers helpers,
StateManager stateManager,
DesignConverter converter,
Configuration config,
AutoDesignApplier autoDesigns,
ActorObjectManager objects,
StateChanged stateChanged,
@ -37,7 +36,6 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
_helpers = helpers;
_stateManager = stateManager;
_converter = converter;
_config = config;
_autoDesigns = autoDesigns;
_objects = objects;
_stateChanged = stateChanged;
@ -202,6 +200,27 @@ public sealed class StateApi : IGlamourerApiState, IApiService, IDisposable
return ApiHelpers.Return(GlamourerApiEc.Success, args);
}
public GlamourerApiEc DeletePlayerState(string playerName, ushort worldId, uint key)
{
var args = ApiHelpers.Args("Name", playerName, "World", worldId, "Key", key);
var states = _helpers.FindExistingStates(playerName).ToList();
if (states.Count is 0)
return ApiHelpers.Return(GlamourerApiEc.NothingDone, args);
var anyLocked = false;
foreach (var state in states)
{
if (state.CanUnlock(key))
_stateManager.DeleteState(state.Identifier);
else
anyLocked = true;
}
return ApiHelpers.Return(anyLocked
? GlamourerApiEc.InvalidKey
: GlamourerApiEc.Success, args);
}
public int UnlockAll(uint key)
=> _stateManager.Values.Count(state => state.Unlock(key));

View file

@ -40,34 +40,37 @@ public class Configuration : IPluginConfiguration, ISavable
[JsonIgnore]
public readonly EphemeralConfig Ephemeral;
public bool UseRestrictedGearProtection { get; set; } = false;
public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public bool EnableAutoDesigns { get; set; } = true;
public bool HideApplyCheckmarks { get; set; } = false;
public bool SmallEquip { get; set; } = false;
public bool UnlockedItemMode { get; set; } = false;
public byte DisableFestivals { get; set; } = 1;
public bool EnableGameContextMenu { get; set; } = true;
public bool HideWindowInCutscene { get; set; } = false;
public bool ShowAutomationSetEditing { get; set; } = true;
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
public bool ShowUnlockedItemWarnings { get; set; } = true;
public bool RevertManualChangesOnZoneChange { get; set; } = false;
public bool ShowQuickBarInTabs { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool ShowWindowWhenUiHidden { get; set; } = false;
public bool KeepAdvancedDyesAttached { get; set; } = true;
public bool ShowPalettePlusImport { get; set; } = true;
public bool UseFloatForColors { get; set; } = true;
public bool UseRgbForColors { get; set; } = true;
public bool ShowColorConfig { get; set; } = true;
public bool ChangeEntireItem { get; set; } = false;
public bool AlwaysApplyAssociatedMods { get; set; } = false;
public bool UseTemporarySettings { get; set; } = true;
public bool AllowDoubleClickToApply { get; set; } = false;
public bool RespectManualOnAutomationUpdate { get; set; } = false;
public bool PreventRandomRepeats { get; set; } = false;
public bool AttachToPcp { get; set; } = true;
public bool UseRestrictedGearProtection { get; set; } = false;
public bool OpenFoldersByDefault { get; set; } = false;
public bool AutoRedrawEquipOnChanges { get; set; } = false;
public bool EnableAutoDesigns { get; set; } = true;
public bool HideApplyCheckmarks { get; set; } = false;
public bool SmallEquip { get; set; } = false;
public bool UnlockedItemMode { get; set; } = false;
public byte DisableFestivals { get; set; } = 1;
public bool EnableGameContextMenu { get; set; } = true;
public bool HideWindowInCutscene { get; set; } = false;
public bool ShowAutomationSetEditing { get; set; } = true;
public bool ShowAllAutomatedApplicationRules { get; set; } = true;
public bool ShowUnlockedItemWarnings { get; set; } = true;
public bool RevertManualChangesOnZoneChange { get; set; } = false;
public bool ShowQuickBarInTabs { get; set; } = true;
public bool OpenWindowAtStart { get; set; } = false;
public bool ShowWindowWhenUiHidden { get; set; } = false;
public bool KeepAdvancedDyesAttached { get; set; } = true;
public bool ShowPalettePlusImport { get; set; } = true;
public bool UseFloatForColors { get; set; } = true;
public bool UseRgbForColors { get; set; } = true;
public bool ShowColorConfig { get; set; } = true;
public bool ChangeEntireItem { get; set; } = false;
public bool AlwaysApplyAssociatedMods { get; set; } = true;
public bool UseTemporarySettings { get; set; } = true;
public bool AllowDoubleClickToApply { get; set; } = false;
public bool RespectManualOnAutomationUpdate { get; set; } = false;
public bool PreventRandomRepeats { get; set; } = false;
public string PcpFolder { get; set; } = "PCP";
public string PcpColor { get; set; } = "";
public DesignPanelFlag HideDesignPanel { get; set; } = 0;
public DesignPanelFlag AutoExpandDesignPanel { get; set; } = 0;

View file

@ -100,7 +100,7 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
public new JObject JsonSerialize()
{
var ret = new JObject()
var ret = new JObject
{
["FileVersion"] = FileVersion,
["Identifier"] = Identifier,
@ -131,12 +131,17 @@ public sealed class Design : DesignBase, ISavable, IDesignStandIn
var ret = new JArray();
foreach (var (mod, settings) in AssociatedMods)
{
var obj = new JObject()
var obj = new JObject
{
["Name"] = mod.Name,
["Directory"] = mod.DirectoryName,
["Enabled"] = settings.Enabled,
};
if (settings.Remove)
obj["Remove"] = true;
else if (settings.ForceInherit)
obj["Inherit"] = true;
else
obj["Enabled"] = settings.Enabled;
if (settings.Enabled)
{
obj["Priority"] = settings.Priority;

View file

@ -6,12 +6,13 @@ using Glamourer.GameData;
using Glamourer.Interop.Material;
using Glamourer.Interop.Penumbra;
using Glamourer.Services;
using OtterGui.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using OtterGui.Extensions;
using Penumbra.GameData.DataContainers;
using Penumbra.GameData.Enums;
namespace Glamourer.Designs;
public sealed class DesignManager : DesignEditor
@ -228,7 +229,7 @@ public sealed class DesignManager : DesignEditor
design.Tags = design.Tags.Append(tag).OrderBy(t => t).ToArray();
design.LastEdit = DateTimeOffset.UtcNow;
var idx = design.Tags.IndexOf(tag);
var idx = design.Tags.AsEnumerable().IndexOf(tag);
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Added tag {tag} at {idx} to design {design.Identifier}.");
DesignChanged.Invoke(DesignChanged.Type.AddedTag, design, new TagAddedTransaction(tag, idx));
@ -261,7 +262,7 @@ public sealed class DesignManager : DesignEditor
SaveService.QueueSave(design);
Glamourer.Log.Debug($"Renamed tag {oldTag} at {tagIdx} to {newTag} in design {design.Identifier} and reordered tags.");
DesignChanged.Invoke(DesignChanged.Type.ChangedTag, design,
new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.IndexOf(newTag)));
new TagChangedTransaction(oldTag, newTag, tagIdx, design.Tags.AsEnumerable().IndexOf(newTag)));
}
/// <summary> Add an associated mod to a design. </summary>
@ -556,7 +557,7 @@ public sealed class DesignManager : DesignEditor
try
{
File.Move(SaveService.FileNames.MigrationDesignFile,
Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"));
Path.ChangeExtension(SaveService.FileNames.MigrationDesignFile, ".json.bak"), true);
Glamourer.Log.Information($"Moved migrated design file {SaveService.FileNames.MigrationDesignFile} to backup file.");
}
catch (Exception ex)

View file

@ -71,6 +71,7 @@ public class Glamourer : IDalamudPlugin
sb.Append($"> **`Festival Easter-Eggs: `** {config.DisableFestivals}\n");
sb.Append($"> **`Apply Entire Weapon: `** {config.ChangeEntireItem}\n");
sb.Append($"> **`Apply Associated Mods:`** {config.AlwaysApplyAssociatedMods}\n");
sb.Append($"> **`Attach to PCP: `** {config.AttachToPcp}\n");
sb.Append($"> **`Hidden Panels: `** {config.HideDesignPanel}\n");
sb.Append($"> **`Show QDB: `** {config.Ephemeral.ShowDesignQuickBar}\n");
sb.Append($"> **`QDB Hotkey: `** {config.ToggleQuickDesignBar}\n");

View file

@ -1,4 +1,4 @@
<Project Sdk="Dalamud.NET.Sdk/13.0.0">
<Project Sdk="Dalamud.NET.Sdk/13.1.0">
<PropertyGroup>
<RootNamespace>Glamourer</RootNamespace>
<AssemblyName>Glamourer</AssemblyName>

View file

@ -44,6 +44,7 @@ public class GlamourerChangelog
Add1_3_8_0(Changelog);
Add1_4_0_0(Changelog);
Add1_5_0_0(Changelog);
Add1_5_1_0(Changelog);
}
private (int, ChangeLogDisplayType) ConfigData()
@ -64,6 +65,16 @@ public class GlamourerChangelog
}
}
private static void Add1_5_1_0(Changelog log)
=> log.NextVersion("Version 1.5.1.0")
.RegisterHighlight("Added support for Penumbras PCP functionality to add the current state of the character as a design.")
.RegisterEntry("On import, a design for the PCP is created and, if possible, applied to the character.", 1)
.RegisterEntry("No automation is assigned.", 1)
.RegisterEntry("Finer control about this can be found in the settings.", 1)
.RegisterEntry("Fixed an issue with static visors not toggling through Glamourer (1.5.0.7).")
.RegisterEntry("The advanced dye slot combo now contains glasses (1.5.0.7).")
.RegisterEntry("Several fixes for patch-related issues (1.5.0.1 - 1.5.0.6");
private static void Add1_5_0_0(Changelog log)
=> log.NextVersion("Version 1.5.0.0")
.RegisterImportant("Updated for game version 7.30 and Dalamud API13, which uses a new GUI backend. Some things may not work as expected. Please let me know any issues you encounter.")

View file

@ -18,7 +18,6 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
public const float GlossWidth = 100;
public const float SpecularStrengthWidth = 125;
private EquipSlot _newSlot = EquipSlot.Head;
private int _newMaterialIdx;
private int _newRowIdx;
private MaterialValueIndex _newKey = MaterialValueIndex.FromSlot(EquipSlot.Head);
@ -178,14 +177,42 @@ public class MaterialDrawer(DesignManager _designManager, Configuration _config)
public sealed class MaterialSlotCombo;
private void DrawSlotCombo()
{
var width = ImUtf8.CalcTextSize(EquipSlot.OffHand.ToName()).X + ImGui.GetFrameHeightWithSpacing();
ImGui.SetNextItemWidth(width);
using var combo = ImUtf8.Combo("##slot"u8, _newKey.SlotName());
if (combo)
{
var currentSlot = _newKey.ToEquipSlot();
foreach (var tmpSlot in EquipSlotExtensions.FullSlots)
{
if (ImUtf8.Selectable(tmpSlot.ToName(), tmpSlot == currentSlot) && currentSlot != tmpSlot)
_newKey = MaterialValueIndex.FromSlot(tmpSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
}
var currentBonus = _newKey.ToBonusSlot();
foreach (var bonusSlot in BonusExtensions.AllFlags)
{
if (ImUtf8.Selectable(bonusSlot.ToName(), bonusSlot == currentBonus) && bonusSlot != currentBonus)
_newKey = MaterialValueIndex.FromSlot(bonusSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
}
}
ImUtf8.HoverTooltip("Choose a slot for an advanced dye row."u8);
}
public void DrawNew(Design design)
{
if (EquipSlotCombo.Draw("##slot", "Choose a slot for an advanced dye row.", ref _newSlot))
_newKey = MaterialValueIndex.FromSlot(_newSlot) with
{
MaterialIndex = (byte)_newMaterialIdx,
RowIndex = (byte)_newRowIdx,
};
DrawSlotCombo();
ImUtf8.SameLineInner();
DrawMaterialIdxDrag();
ImUtf8.SameLineInner();

View file

@ -22,11 +22,12 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
private readonly CustomizeService _customize;
private readonly GPoseService _gpose;
private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2];
private readonly EquipItem[] _lastItems = new EquipItem[EquipFlagExtensions.NumEquipFlags / 2 + BonusExtensions.AllFlags.Count];
public IEnumerable<KeyValuePair<EquipSlot, EquipItem>> LastItems
=> EquipSlotExtensions.EqdpSlots.Append(EquipSlot.MainHand).Append(EquipSlot.OffHand).Zip(_lastItems)
.Select(p => new KeyValuePair<EquipSlot, EquipItem>(p.First, p.Second));
public IEnumerable<KeyValuePair<object, EquipItem>> LastItems
=> EquipSlotExtensions.EqdpSlots.Cast<object>().Append(EquipSlot.MainHand).Append(EquipSlot.OffHand)
.Concat(BonusExtensions.AllFlags.Cast<object>()).Zip(_lastItems)
.Select(p => new KeyValuePair<object, EquipItem>(p.First, p.Second));
public ChangedItemType LastType { get; private set; } = ChangedItemType.None;
public uint LastId { get; private set; }
@ -72,6 +73,21 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
if (!Player())
return;
var bonusSlot = item.Type.ToBonus();
if (bonusSlot is not BonusItemFlag.Unknown)
{
// + 2 due to weapons.
var glasses = _lastItems[bonusSlot.ToSlot() + 2];
using (_ = !openTooltip ? null : ImRaii.Tooltip())
{
ImGui.TextUnformatted($"{prefix}Right-Click to apply to current actor.");
if (glasses.Valid)
ImGui.TextUnformatted($"{prefix}Control + Right-Click to re-apply {glasses.Name} to current actor.");
}
return;
}
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
@ -109,6 +125,27 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
public void ApplyItem(ActorState state, EquipItem item)
{
var bonusSlot = item.Type.ToBonus();
if (bonusSlot is not BonusItemFlag.Unknown)
{
// + 2 due to weapons.
var glasses = _lastItems[bonusSlot.ToSlot() + 2];
if (ImGui.GetIO().KeyCtrl && glasses.Valid)
{
Glamourer.Log.Debug($"Re-Applying {glasses.Name} to {bonusSlot.ToName()}.");
SetLastItem(bonusSlot, default, state);
_stateManager.ChangeBonusItem(state, bonusSlot, glasses, ApplySettings.Manual);
}
else
{
Glamourer.Log.Debug($"Applying {item.Name} to {bonusSlot.ToName()}.");
SetLastItem(bonusSlot, item, state);
_stateManager.ChangeBonusItem(state, bonusSlot, item, ApplySettings.Manual);
}
return;
}
var slot = item.Type.ToSlot();
var last = _lastItems[slot.ToIndex()];
switch (slot)
@ -265,7 +302,22 @@ public sealed class PenumbraChangedItemTooltip : IDisposable
{
var oldItem = state.ModelData.Item(slot);
if (oldItem.Id != item.Id)
_lastItems[slot.ToIndex()] = oldItem;
last = oldItem;
}
}
private void SetLastItem(BonusItemFlag slot, EquipItem item, ActorState state)
{
ref var last = ref _lastItems[slot.ToSlot() + 2];
if (!item.Valid)
{
last = default;
}
else
{
var oldItem = state.ModelData.BonusItem(slot);
if (oldItem.Id != item.Id)
last = oldItem;
}
}

View file

@ -2,11 +2,11 @@
using Dalamud.Utility;
using Dalamud.Bindings.ImGui;
using OtterGui;
using OtterGui.Custom;
using OtterGui.Extensions;
using OtterGui.Log;
using OtterGui.Widgets;
using Penumbra.GameData.DataContainers;
using OtterGui.Custom;
namespace Glamourer.Gui.Tabs.AutomationTab;

View file

@ -89,7 +89,13 @@ public unsafe class PenumbraPanel(PenumbraService _penumbra, PenumbraChangedItem
ImGui.Separator();
foreach (var (slot, item) in _penumbraTooltip.LastItems)
{
ImGuiUtil.DrawTableColumn($"{slot.ToName()} Revert-Item");
switch (slot)
{
case EquipSlot e: ImGuiUtil.DrawTableColumn($"{e.ToName()} Revert-Item"); break;
case BonusItemFlag f: ImGuiUtil.DrawTableColumn($"{f.ToName()} Revert-Item"); break;
default: ImGuiUtil.DrawTableColumn("Unk Revert-Item"); break;
}
ImGuiUtil.DrawTableColumn(item.Valid ? item.Name : "None");
ImGui.TableNextColumn();
}

View file

@ -154,6 +154,7 @@ public class DesignPanel
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.FromDesign(MetaIndex.WeaponState, _manager, _selector.Selected!));
EquipmentDrawer.DrawMetaToggle(ToggleDrawData.CrestFromDesign(CrestFlag.OffHand, _manager, _selector.Selected!));
}
ImGui.SameLine();
using (var _ = ImRaii.Group())
{
@ -403,6 +404,7 @@ public class DesignPanel
_manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.HatState, equip.Value);
_manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.VisorState, equip.Value);
_manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.WeaponState, equip.Value);
_manager.ChangeApplyMeta(_selector.Selected!, MetaIndex.EarState, equip.Value);
}
if (customize.HasValue)

View file

@ -71,6 +71,8 @@ public class ModAssociationsTab(PenumbraService penumbra, DesignFileSystemSelect
private void DrawApplyAllButton()
{
var (id, name) = penumbra.CurrentCollection;
if (config.Ephemeral.IncognitoMode)
name = id.ShortGuid();
if (ImGuiUtil.DrawDisabledButton($"Try Applying All Associated Mods to {name}##applyAll",
new Vector2(ImGui.GetContentRegionAvail().X, 0), string.Empty, id == Guid.Empty))
ApplyAll();

View file

@ -490,7 +490,7 @@ public class MultiDesignPanel(
foreach (var leaf in selector.SelectedPaths.OfType<DesignFileSystem.Leaf>())
{
var index = leaf.Value.Tags.IndexOf(_tag);
var index = leaf.Value.Tags.AsEnumerable().IndexOf(_tag);
if (index >= 0)
_removeDesigns.Add((leaf.Value, index));
else

View file

@ -1,4 +1,5 @@
using Dalamud.Game.ClientState.Keys;
using Dalamud.Bindings.ImGui;
using Dalamud.Game.ClientState.Keys;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility;
@ -8,7 +9,8 @@ using Glamourer.Designs;
using Glamourer.Gui.Tabs.DesignTab;
using Glamourer.Interop;
using Glamourer.Interop.PalettePlus;
using Dalamud.Bindings.ImGui;
using Glamourer.Services;
using OtterGui;
using OtterGui.Raii;
using OtterGui.Text;
using OtterGui.Widgets;
@ -27,7 +29,8 @@ public class SettingsTab(
CollectionOverrideDrawer overrides,
CodeDrawer codeDrawer,
Glamourer glamourer,
AutoDesignApplier autoDesignApplier)
AutoDesignApplier autoDesignApplier,
PcpService pcpService)
: ITab
{
private readonly VirtualKey[] _validKeys = keys.GetValidVirtualKeys().Prepend(VirtualKey.NO_KEY).ToArray();
@ -89,6 +92,15 @@ public class SettingsTab(
Checkbox("Auto-Reload Gear"u8,
"Automatically reload equipment pieces on your own character when changing any mod options in Penumbra in their associated collection."u8,
config.AutoRedrawEquipOnChanges, v => config.AutoRedrawEquipOnChanges = v);
Checkbox("Attach to PCP-Handling"u8,
"Add the actor's glamourer state when a PCP is created by Penumbra, and create a design and apply it if possible when a PCP is installed by Penumbra."u8,
config.AttachToPcp, pcpService.Set);
var active = config.DeleteDesignModifier.IsActive();
ImGui.SameLine();
if (ImUtf8.ButtonEx("Delete all PCP Designs"u8, "Deletes all designs tagged with 'PCP' from the design list."u8, disabled: !active))
pcpService.CleanPcpDesigns();
if (!active)
ImUtf8.HoverTooltip(ImGuiHoveredFlags.AllowWhenDisabled, $"\nHold {config.DeleteDesignModifier} while clicking.");
Checkbox("Revert Manual Changes on Zone Change"u8,
"Restores the old behaviour of reverting your character to its game or automation base whenever you change the zone."u8,
config.RevertManualChangesOnZoneChange, v => config.RevertManualChangesOnZoneChange = v);
@ -124,6 +136,28 @@ public class SettingsTab(
Checkbox("Reset Temporary Settings"u8,
"Newly created designs will be configured to clear all advanced settings applied by Glamourer to the collection by default."u8,
config.DefaultDesignSettings.ResetTemporarySettings, v => config.DefaultDesignSettings.ResetTemporarySettings = v);
var tmp = config.PcpFolder;
ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X);
if (ImUtf8.InputText("##pcpFolder"u8, ref tmp))
config.PcpFolder = tmp;
if (ImGui.IsItemDeactivatedAfterEdit())
config.Save();
ImGuiUtil.LabeledHelpMarker("Default PCP Organizational Folder",
"The folder any designs created due to penumbra character packs are moved to on creation.\nLeave blank to import into Root.");
tmp = config.PcpColor;
ImGui.SetNextItemWidth(0.4f * ImGui.GetContentRegionAvail().X);
if (ImUtf8.InputText("##pcpColor"u8, ref tmp))
config.PcpColor = tmp;
if (ImGui.IsItemDeactivatedAfterEdit())
config.Save();
ImGuiUtil.LabeledHelpMarker("Default PCP Design Color",
"The name of the color group any designs created due to penumbra character packs are assigned.\nLeave blank for no specific color assignment.");
}
private void DrawInterfaceSettings()

View file

@ -62,7 +62,7 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
var drawData = type switch
{
MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, slotId),
MaterialValueIndex.DrawObjectType.Human => GetTempSlot((Human*)characterBase, (HumanSlot)slotId),
_ => GetTempSlot((Weapon*)characterBase),
};
var mode = PrepareColorSet.GetMode(material);
@ -192,13 +192,24 @@ public sealed unsafe class MaterialManager : IRequiredService, IDisposable
}
/// <summary> We need to get the temporary set, variant and stain that is currently being set if it is available. </summary>
private static CharacterWeapon GetTempSlot(Human* human, byte slotId)
private static CharacterWeapon GetTempSlot(Human* human, HumanSlot slotId)
{
if (human->ChangedEquipData == null)
return ((Model)human).GetArmor(((uint)slotId).ToEquipSlot()).ToWeapon(0);
if (human->ChangedEquipData is null)
return slotId.ToSpecificEnum() switch
{
EquipSlot slot => ((Model)human).GetArmor(slot).ToWeapon(0),
BonusItemFlag bonus => ((Model)human).GetBonus(bonus).ToWeapon(0),
_ => default,
};
var item = (ChangedEquipData*)human->ChangedEquipData + slotId;
return ((CharacterArmor*)item)->ToWeapon(0);
if (!slotId.ToSlotIndex(out var index))
return default;
var item = (ChangedEquipData*)human->ChangedEquipData + index;
if (index < 10)
return ((CharacterArmor*)item)->ToWeapon(0);
return new CharacterWeapon(item->BonusModel, 0, item->BonusVariant, StainIds.None);
}
/// <summary>

View file

@ -1,6 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Graphics.Kernel;
using FFXIVClientStructs.FFXIV.Client.System.Resource.Handle;
using Lumina.Data.Files;
using Penumbra.GameData.Files.MaterialStructs;
using Penumbra.GameData.Interop;
using Texture = FFXIVClientStructs.FFXIV.Client.Graphics.Kernel.Texture;
@ -69,9 +68,9 @@ public static unsafe class MaterialService
return null;
var material = (MaterialResourceHandle*) model.AsCharacterBase->MaterialsSpan[index].Value;
if (material == null || material->ColorTable == null)
if (material == null || material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable)
return null;
return (ColorTable.Table*)material->ColorTable;
return (ColorTable.Table*)material->DataSet;
}
}

View file

@ -50,6 +50,18 @@ public readonly record struct MaterialValueIndex(
return idx > 2 ? Invalid : new MaterialValueIndex(DrawObjectType.Human, (byte)(idx + 16), 0, 0);
}
public string SlotName()
{
var slot = ToEquipSlot();
if (slot is not EquipSlot.Unknown)
return slot.ToName();
if (DrawObject is DrawObjectType.Human && SlotIndex is 16)
return BonusItemFlag.Glasses.ToString();
return EquipSlot.Unknown.ToName();
}
public EquipSlot ToEquipSlot()
=> DrawObject switch
{
@ -59,6 +71,13 @@ public readonly record struct MaterialValueIndex(
_ => EquipSlot.Unknown,
};
public BonusItemFlag ToBonusSlot()
=> DrawObject switch
{
DrawObjectType.Human when SlotIndex > 15 => ((uint)SlotIndex - 16).ToBonusSlot(),
_ => BonusItemFlag.Unknown,
};
public unsafe bool TryGetModel(Actor actor, out Model model)
{
if (!actor.Valid)

View file

@ -69,13 +69,13 @@ public sealed unsafe class PrepareColorSet
public static bool TryGetColorTable(MaterialResourceHandle* material, StainIds stainIds,
out ColorTable.Table table)
{
if (material->ColorTable == null)
if (material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable)
{
table = default;
return false;
}
var newTable = *(ColorTable.Table*)material->ColorTable;
var newTable = *(ColorTable.Table*)material->DataSet;
if (GetDyeTable(material, out var dyeTable))
{
if (stainIds.Stain1.Id != 0)

View file

@ -2,6 +2,7 @@
using Dalamud.Plugin;
using Glamourer.Events;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Classes;
using Penumbra.Api.Enums;
using Penumbra.Api.Helpers;
@ -49,6 +50,8 @@ public class PenumbraService : IDisposable
private readonly EventSubscriber<nint, Guid, nint, nint, nint> _creatingCharacterBase;
private readonly EventSubscriber<nint, Guid, nint> _createdCharacterBase;
private readonly EventSubscriber<ModSettingChange, Guid, string, bool> _modSettingChanged;
private readonly EventSubscriber<JObject, string, Guid> _pcpParsed;
private readonly EventSubscriber<JObject, ushort, string> _pcpCreated;
private global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier? _collectionByIdentifier;
private global::Penumbra.Api.IpcSubscribers.GetCollections? _collections;
@ -101,6 +104,8 @@ public class PenumbraService : IDisposable
_createdCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatedCharacterBase.Subscriber(pi);
_creatingCharacterBase = global::Penumbra.Api.IpcSubscribers.CreatingCharacterBase.Subscriber(pi);
_modSettingChanged = global::Penumbra.Api.IpcSubscribers.ModSettingChanged.Subscriber(pi);
_pcpCreated = global::Penumbra.Api.IpcSubscribers.CreatingPcp.Subscriber(pi);
_pcpParsed = global::Penumbra.Api.IpcSubscribers.ParsingPcp.Subscriber(pi);
Reattach();
}
@ -135,6 +140,18 @@ public class PenumbraService : IDisposable
remove => _modSettingChanged.Event -= value;
}
public event Action<JObject, ushort, string> PcpCreated
{
add => _pcpCreated.Event += value;
remove => _pcpCreated.Event -= value;
}
public event Action<JObject, string, Guid> PcpParsed
{
add => _pcpParsed.Event += value;
remove => _pcpParsed.Event -= value;
}
public Dictionary<Guid, string> GetCollections()
=> Available ? _collections!.Invoke() : [];
@ -514,28 +531,30 @@ public class PenumbraService : IDisposable
_clickSubscriber.Enable();
_creatingCharacterBase.Enable();
_createdCharacterBase.Enable();
_pcpCreated.Enable();
_pcpParsed.Enable();
_modSettingChanged.Enable();
_collectionByIdentifier = new global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier(_pluginInterface);
_collections = new global::Penumbra.Api.IpcSubscribers.GetCollections(_pluginInterface);
_redraw = new global::Penumbra.Api.IpcSubscribers.RedrawObject(_pluginInterface);
_checkCutsceneParent = new global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndexFunc(_pluginInterface).Invoke();
_getGameObject = new global::Penumbra.Api.IpcSubscribers.GetGameObjectFromDrawObjectFunc(_pluginInterface).Invoke();
_objectCollection = new global::Penumbra.Api.IpcSubscribers.GetCollectionForObject(_pluginInterface);
_getMods = new global::Penumbra.Api.IpcSubscribers.GetModList(_pluginInterface);
_currentCollection = new global::Penumbra.Api.IpcSubscribers.GetCollection(_pluginInterface);
_getCurrentSettings = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings(_pluginInterface);
_inheritMod = new global::Penumbra.Api.IpcSubscribers.TryInheritMod(_pluginInterface);
_setMod = new global::Penumbra.Api.IpcSubscribers.TrySetMod(_pluginInterface);
_setModPriority = new global::Penumbra.Api.IpcSubscribers.TrySetModPriority(_pluginInterface);
_setModSetting = new global::Penumbra.Api.IpcSubscribers.TrySetModSetting(_pluginInterface);
_setModSettings = new global::Penumbra.Api.IpcSubscribers.TrySetModSettings(_pluginInterface);
_openModPage = new global::Penumbra.Api.IpcSubscribers.OpenMainWindow(_pluginInterface);
_getChangedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItems(_pluginInterface);
_setTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings(_pluginInterface);
_setTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer(_pluginInterface);
_removeTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings(_pluginInterface);
_collectionByIdentifier = new global::Penumbra.Api.IpcSubscribers.GetCollectionsByIdentifier(_pluginInterface);
_collections = new global::Penumbra.Api.IpcSubscribers.GetCollections(_pluginInterface);
_redraw = new global::Penumbra.Api.IpcSubscribers.RedrawObject(_pluginInterface);
_checkCutsceneParent = new global::Penumbra.Api.IpcSubscribers.GetCutsceneParentIndexFunc(_pluginInterface).Invoke();
_getGameObject = new global::Penumbra.Api.IpcSubscribers.GetGameObjectFromDrawObjectFunc(_pluginInterface).Invoke();
_objectCollection = new global::Penumbra.Api.IpcSubscribers.GetCollectionForObject(_pluginInterface);
_getMods = new global::Penumbra.Api.IpcSubscribers.GetModList(_pluginInterface);
_currentCollection = new global::Penumbra.Api.IpcSubscribers.GetCollection(_pluginInterface);
_getCurrentSettings = new global::Penumbra.Api.IpcSubscribers.GetCurrentModSettings(_pluginInterface);
_inheritMod = new global::Penumbra.Api.IpcSubscribers.TryInheritMod(_pluginInterface);
_setMod = new global::Penumbra.Api.IpcSubscribers.TrySetMod(_pluginInterface);
_setModPriority = new global::Penumbra.Api.IpcSubscribers.TrySetModPriority(_pluginInterface);
_setModSetting = new global::Penumbra.Api.IpcSubscribers.TrySetModSetting(_pluginInterface);
_setModSettings = new global::Penumbra.Api.IpcSubscribers.TrySetModSettings(_pluginInterface);
_openModPage = new global::Penumbra.Api.IpcSubscribers.OpenMainWindow(_pluginInterface);
_getChangedItems = new global::Penumbra.Api.IpcSubscribers.GetChangedItems(_pluginInterface);
_setTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettings(_pluginInterface);
_setTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.SetTemporaryModSettingsPlayer(_pluginInterface);
_removeTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettings(_pluginInterface);
_removeTemporaryModSettingsPlayer = new global::Penumbra.Api.IpcSubscribers.RemoveTemporaryModSettingsPlayer(_pluginInterface);
_removeAllTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings(_pluginInterface);
_removeAllTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettings(_pluginInterface);
_removeAllTemporaryModSettingsPlayer =
new global::Penumbra.Api.IpcSubscribers.RemoveAllTemporaryModSettingsPlayer(_pluginInterface);
_queryTemporaryModSettings = new global::Penumbra.Api.IpcSubscribers.QueryTemporaryModSettings(_pluginInterface);
@ -566,6 +585,8 @@ public class PenumbraService : IDisposable
_creatingCharacterBase.Disable();
_createdCharacterBase.Disable();
_modSettingChanged.Disable();
_pcpCreated.Disable();
_pcpParsed.Disable();
if (Available)
{
_collectionByIdentifier = null;
@ -612,5 +633,7 @@ public class PenumbraService : IDisposable
_initializedEvent.Dispose();
_disposedEvent.Dispose();
_modSettingChanged.Dispose();
_pcpCreated.Dispose();
_pcpParsed.Dispose();
}
}

View file

@ -9,9 +9,9 @@ namespace Glamourer.Interop;
public class VisorService : IDisposable
{
private readonly PenumbraReloaded _penumbra;
private readonly IGameInteropProvider _interop;
public readonly VisorStateChanged Event;
private readonly PenumbraReloaded _penumbra;
private readonly IGameInteropProvider _interop;
public readonly VisorStateChanged Event;
public VisorService(VisorStateChanged visorStateChanged, IGameInteropProvider interop, PenumbraReloaded penumbra)
{
@ -36,7 +36,7 @@ public class VisorService : IDisposable
/// <param name="human"> The draw object. </param>
/// <param name="on"> The desired state (true: toggled). </param>
/// <returns> Whether the state was changed. </returns>
public bool SetVisorState(Model human, bool on)
public unsafe bool SetVisorState(Model human, bool on)
{
if (!human.IsHuman)
return false;
@ -46,6 +46,8 @@ public class VisorService : IDisposable
if (oldState == on)
return false;
// No clue what this flag does, but it's necessary for toggling static visors on or off, e.g. Alternate Cap (6229-1).
human.AsHuman->StateFlags |= (CharacterBase.StateFlag)0x40000000;
SetupVisorDetour(human, human.GetArmor(EquipSlot.Head).Set.Id, on);
return true;
}

View file

@ -13,7 +13,7 @@ public unsafe class WeaponService : IDisposable
private readonly WeaponLoading _event;
private readonly ThreadLocal<bool> _inUpdate = new(() => false);
private readonly delegate* unmanaged[Stdcall]<DrawDataContainer*, uint, ulong, byte, byte, byte, byte, void>
private readonly delegate* unmanaged[Stdcall]<DrawDataContainer*, uint, ulong, byte, byte, byte, byte, int, void>
_original;
public WeaponService(WeaponLoading @event, IGameInteropProvider interop)
@ -22,7 +22,7 @@ public unsafe class WeaponService : IDisposable
_loadWeaponHook =
interop.HookFromAddress<LoadWeaponDelegate>((nint)DrawDataContainer.MemberFunctionPointers.LoadWeapon, LoadWeaponDetour);
_original =
(delegate* unmanaged[Stdcall] < DrawDataContainer*, uint, ulong, byte, byte, byte, byte, void >)
(delegate* unmanaged[Stdcall] < DrawDataContainer*, uint, ulong, byte, byte, byte, byte, int, void >)
DrawDataContainer.MemberFunctionPointers.LoadWeapon;
_loadWeaponHook.Enable();
}
@ -36,13 +36,14 @@ public unsafe class WeaponService : IDisposable
// redrawOnEquality controls whether the game does anything if the new weapon is identical to the old one.
// skipGameObject seems to control whether the new weapons are written to the game object or just influence the draw object. (1 = skip, 0 = change)
// unk4 seemed to be the same as unk1.
// unk5 is new in 7.30 and is checked at the beginning of the function to call some timeline related function.
private delegate void LoadWeaponDelegate(DrawDataContainer* drawData, uint slot, ulong weapon, byte redrawOnEquality, byte unk2,
byte skipGameObject, byte unk4);
byte skipGameObject, byte unk4, byte unk5);
private readonly Hook<LoadWeaponDelegate> _loadWeaponHook;
private void LoadWeaponDetour(DrawDataContainer* drawData, uint slot, ulong weaponValue, byte redrawOnEquality, byte unk2,
byte skipGameObject, byte unk4)
byte skipGameObject, byte unk4, byte unk5)
{
if (!_inUpdate.Value)
{
@ -64,21 +65,21 @@ public unsafe class WeaponService : IDisposable
else if (weaponValue == actor.GetMainhand().Value && weaponValue != 0)
_event.Invoke(actor, EquipSlot.MainHand, ref tmpWeapon);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4);
_loadWeaponHook.Original(drawData, slot, weapon.Value, redrawOnEquality, unk2, skipGameObject, unk4, unk5);
if (tmpWeapon.Value != weapon.Value)
{
if (tmpWeapon.Skeleton.Id == 0)
tmpWeapon.Stains = StainIds.None;
_loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4);
_loadWeaponHook.Original(drawData, slot, tmpWeapon.Value, 1, unk2, 1, unk4, unk5);
}
Glamourer.Log.Excessive(
$"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}");
$"Weapon reloaded for 0x{actor.Address:X} ({actor.Utf8Name}) with attributes {slot} {weapon.Value:X14}, {redrawOnEquality}, {unk2}, {skipGameObject}, {unk4}, {unk5}");
}
else
{
_loadWeaponHook.Original(drawData, slot, weaponValue, redrawOnEquality, unk2, skipGameObject, unk4);
_loadWeaponHook.Original(drawData, slot, weaponValue, redrawOnEquality, unk2, skipGameObject, unk4, unk5);
}
}
@ -89,18 +90,18 @@ public unsafe class WeaponService : IDisposable
{
case EquipSlot.MainHand:
_inUpdate.Value = true;
_original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
_original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0);
_inUpdate.Value = false;
return;
case EquipSlot.OffHand:
_inUpdate.Value = true;
_original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0);
_original(&character.AsCharacter->DrawData, 1, weapon.Value, 1, 0, 1, 0, 0);
_inUpdate.Value = false;
return;
case EquipSlot.BothHand:
_inUpdate.Value = true;
_original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0);
_original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0);
_original(&character.AsCharacter->DrawData, 0, weapon.Value, 1, 0, 1, 0, 0);
_original(&character.AsCharacter->DrawData, 1, CharacterWeapon.Empty.Value, 1, 0, 1, 0, 0);
_inUpdate.Value = false;
return;
}

View file

@ -1,9 +1,10 @@
using OtterGui.Classes;
using OtterGui.Log;
using OtterGui.Services;
namespace Glamourer.Services;
public class BackupService
public class BackupService : IAsyncService
{
private readonly Logger _logger;
private readonly DirectoryInfo _configDirectory;
@ -14,7 +15,7 @@ public class BackupService
_logger = logger;
_fileNames = GlamourerFiles(fileNames);
_configDirectory = new DirectoryInfo(fileNames.ConfigDirectory);
Backup.CreateAutomaticBackup(logger, _configDirectory, _fileNames);
Awaiter = Task.Run(() => Backup.CreateAutomaticBackup(logger, new DirectoryInfo(fileNames.ConfigDirectory), _fileNames));
}
/// <summary> Create a permanent backup with a given name for migrations. </summary>
@ -40,4 +41,9 @@ public class BackupService
return list;
}
public Task Awaiter { get; }
public bool Finished
=> Awaiter.IsCompletedSuccessfully;
}

View file

@ -50,7 +50,8 @@ public class CodeService
| CodeFlag.OopsMiqote
| CodeFlag.OopsRoegadyn
| CodeFlag.OopsAuRa
| CodeFlag.OopsHrothgar;
| CodeFlag.OopsHrothgar
| CodeFlag.OopsViera;
public const CodeFlag FullCodes = CodeFlag.Face | CodeFlag.Manderville | CodeFlag.Smiles;
@ -250,3 +251,4 @@ public class CodeService
_ => (false, 0, string.Empty, string.Empty, string.Empty),
};
}

View file

@ -0,0 +1,119 @@
using Glamourer.Designs;
using Glamourer.Interop.Penumbra;
using Glamourer.State;
using Newtonsoft.Json.Linq;
using OtterGui.Services;
using Penumbra.GameData.Actors;
using Penumbra.GameData.Interop;
namespace Glamourer.Services;
public class PcpService : IRequiredService
{
private readonly Configuration _config;
private readonly PenumbraService _penumbra;
private readonly ActorObjectManager _objects;
private readonly StateManager _state;
private readonly DesignConverter _designConverter;
private readonly DesignManager _designManager;
public PcpService(Configuration config, PenumbraService penumbra, ActorObjectManager objects, StateManager state,
DesignConverter designConverter, DesignManager designManager)
{
_config = config;
_penumbra = penumbra;
_objects = objects;
_state = state;
_designConverter = designConverter;
_designManager = designManager;
_config.AttachToPcp = !_config.AttachToPcp;
Set(!_config.AttachToPcp);
}
public void CleanPcpDesigns()
{
var designs = _designManager.Designs.Where(d => d.Tags.Contains("PCP")).ToList();
Glamourer.Log.Information($"[PCPService] Deleting {designs.Count} designs containing the tag PCP.");
foreach (var design in designs)
_designManager.Delete(design);
}
public void Set(bool value)
{
if (value == _config.AttachToPcp)
return;
_config.AttachToPcp = value;
_config.Save();
if (value)
{
Glamourer.Log.Information("[PCPService] Attached to PCP handling.");
_penumbra.PcpCreated += OnPcpCreation;
_penumbra.PcpParsed += OnPcpParse;
}
else
{
Glamourer.Log.Information("[PCPService] Detached from PCP handling.");
_penumbra.PcpCreated -= OnPcpCreation;
_penumbra.PcpParsed -= OnPcpParse;
}
}
private void OnPcpParse(JObject jObj, string modDirectory, Guid collection)
{
Glamourer.Log.Debug("[PCPService] Parsing PCP file.");
if (jObj["Glamourer"] is not JObject glamourer)
return;
if (glamourer["Version"]!.ToObject<int>() is not 1)
return;
if (_designConverter.FromJObject(glamourer["Design"] as JObject, true, true) is not { } designBase)
return;
var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject);
if (!actorIdentifier.IsValid)
return;
var time = new DateTimeOffset(jObj["Time"]?.ToObject<DateTime>() ?? DateTime.UtcNow);
var design = _designManager.CreateClone(designBase,
$"{_config.PcpFolder}/{actorIdentifier} - {jObj["Note"]?.ToObject<string>() ?? string.Empty}", true);
_designManager.AddTag(design, "PCP");
_designManager.SetWriteProtection(design, true);
_designManager.AddMod(design, new Mod(modDirectory, modDirectory), new ModSettings([], 0, true, false, false));
_designManager.ChangeDescription(design, $"PCP design created for {actorIdentifier} on {time}.");
_designManager.ChangeResetAdvancedDyes(design, true);
_designManager.SetQuickDesign(design, false);
_designManager.ChangeColor(design, _config.PcpColor);
Glamourer.Log.Debug("[PCPService] Created PCP design.");
if (_state.GetOrCreate(actorIdentifier, _objects.TryGetValue(actorIdentifier, out var data) ? data.Objects[0] : Actor.Null,
out var state))
{
_state.ApplyDesign(state!, design, ApplySettings.Manual);
Glamourer.Log.Debug($"[PCPService] Applied PCP design to {actorIdentifier.Incognito(null)}");
}
}
private void OnPcpCreation(JObject jObj, ushort index, string path)
{
Glamourer.Log.Debug("[PCPService] Adding Glamourer data to PCP file.");
var actorIdentifier = _objects.Actors.FromJson(jObj["Actor"] as JObject);
if (!actorIdentifier.IsValid)
return;
if (!_state.GetOrCreate(actorIdentifier, _objects.Objects[(int)index], out var state))
{
Glamourer.Log.Debug($"[PCPService] Could not get or create state for actor {index}.");
return;
}
var design = _designConverter.Convert(state, ApplicationRules.All);
jObj["Glamourer"] = new JObject
{
["Version"] = 1,
["Design"] = design.JsonSerialize(),
};
}
}

View file

@ -385,7 +385,7 @@ public class StateApplier(
var actors = ChangeMetaState(state, MetaIndex.Wetness, true);
if (redraw)
{
if (withLock)
if (withLock && actors.Valid)
state.TempLock();
ForceRedraw(actors);
}

View file

@ -4,9 +4,9 @@
"net9.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[13.0.0, )",
"resolved": "13.0.0",
"contentHash": "Mb3cUDSK/vDPQ8gQIeuCw03EMYrej1B4J44a1AvIJ9C759p9XeqdU9Hg4WgOmlnlPe0G7ILTD32PKSUpkQNa8w=="
"requested": "[13.1.0, )",
"resolved": "13.1.0",
"contentHash": "XdoNhJGyFby5M/sdcRhnc5xTop9PHy+H50PTWpzLhJugjB19EDBiHD/AsiDF66RETM+0qKUdJBZrNuebn7qswQ=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
@ -24,6 +24,19 @@
"Vortice.DXGI": "3.4.2-beta"
}
},
"FlatSharp.Compiler": {
"type": "Transitive",
"resolved": "7.9.0",
"contentHash": "MU6808xvdbWJ3Ev+5PKalqQuzvVbn1DzzQH8txRDHGFUNDvHjd+ejqpvnYc9BSJ8Qp8VjkkpJD8OzRYilbPp3A=="
},
"FlatSharp.Runtime": {
"type": "Transitive",
"resolved": "7.9.0",
"contentHash": "Bm8+WqzEsWNpxqrD5x4x+zQ8dyINlToCreM5FI2oNSfUVc9U9ZB+qztX/jd8rlJb3r0vBSlPwVLpw0xBtPa3Vw==",
"dependencies": {
"System.Memory": "4.5.5"
}
},
"JetBrains.Annotations": {
"type": "Transitive",
"resolved": "2024.3.0",
@ -55,6 +68,11 @@
"SharpGen.Runtime": "2.1.2-beta"
}
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.5",
"contentHash": "XIWiDvKPXaTveaB7HVganDlOCRoj03l+jrwNvcge/t8vhGYKvqV+dMv6G4SAX2NoNmN0wZfVPTAlFwZcZvVOUw=="
},
"Vortice.DirectX": {
"type": "Transitive",
"resolved": "3.4.2-beta",
@ -95,6 +113,8 @@
"penumbra.gamedata": {
"type": "Project",
"dependencies": {
"FlatSharp.Compiler": "[7.9.0, )",
"FlatSharp.Runtime": "[7.9.0, )",
"OtterGui": "[1.0.0, )",
"Penumbra.Api": "[5.10.0, )",
"Penumbra.String": "[1.0.6, )"

@ -1 +1 @@
Subproject commit 5e32bb225e5fbb60ed8426ed887fd7e8a831ebae
Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c

@ -1 +1 @@
Subproject commit c27a06004138f2ec80ccdb494bb6ddf6d39d2165
Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669

@ -1 +1 @@
Subproject commit 2f5e901314444238ab3aa6c5043368622bca815a
Subproject commit d889f9ef918514a46049725052d378b441915b00

@ -1 +1 @@
Subproject commit 878acce46e286867d6ef1f8ecedb390f7bac34fd
Subproject commit c8611a0c546b6b2ec29214ab319fc2c38fe74793

View file

@ -17,8 +17,8 @@
"Character"
],
"InternalName": "Glamourer",
"AssemblyVersion": "1.5.0.4",
"TestingAssemblyVersion": "1.5.0.4",
"AssemblyVersion": "1.5.1.5",
"TestingAssemblyVersion": "1.5.1.5",
"RepoUrl": "https://github.com/Ottermandias/Glamourer",
"ApplicableVersion": "any",
"DalamudApiLevel": 13,
@ -27,9 +27,9 @@
"IsTestingExclusive": "False",
"DownloadCount": 1,
"LastUpdate": 1618608322,
"DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.0.4/Glamourer.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.0.4/Glamourer.zip",
"DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.0.4/Glamourer.zip",
"DownloadLinkInstall": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.5/Glamourer.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.5/Glamourer.zip",
"DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.5/Glamourer.zip",
"IconUrl": "https://raw.githubusercontent.com/Ottermandias/Glamourer/main/images/icon.png"
}
]