Compare commits

...

19 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
24 changed files with 163 additions and 50 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

@ -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

@ -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

@ -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

@ -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

@ -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

@ -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,7 +68,7 @@ public static unsafe class MaterialService
return null;
var material = (MaterialResourceHandle*) model.AsCharacterBase->MaterialsSpan[index].Value;
if (material == null || material->DataSet == null)
if (material == null || material->DataSet == null || material->DataSetSize < sizeof(ColorTable.Table) || !material->HasColorTable)
return null;
return (ColorTable.Table*)material->DataSet;

View file

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

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

@ -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",

@ -1 +1 @@
Subproject commit 4a9b71a93e76aa5eed818542288329e34ec0dd89
Subproject commit 1459e2b8f5e1687f659836709e23571235d4206c

@ -1 +1 @@
Subproject commit af41b1787acef9df7dc83619fe81e63a36443ee5
Subproject commit c23ee05c1e9fa103eaa52e6aa7e855ef568ee669

@ -1 +1 @@
Subproject commit 15e7c8eb41867e6bbd3fe6a8885404df087bc7e7
Subproject commit d889f9ef918514a46049725052d378b441915b00

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

View file

@ -17,8 +17,8 @@
"Character"
],
"InternalName": "Glamourer",
"AssemblyVersion": "1.5.1.0",
"TestingAssemblyVersion": "1.5.1.0",
"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.1.0/Glamourer.zip",
"DownloadLinkUpdate": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.0/Glamourer.zip",
"DownloadLinkTesting": "https://github.com/Ottermandias/Glamourer/releases/download/1.5.1.0/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"
}
]