From 954c8721803130eb17e2c9537146ecd7b652d600 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Thu, 13 Aug 2026 20:51:26 +0200 Subject: [PATCH 01/11] Fixed incorrect facewear deserialization offset leading to inaccurate equipment redraw --- RemoteController/Controller.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RemoteController/Controller.cs b/RemoteController/Controller.cs index b74ab4e6..073a694f 100644 --- a/RemoteController/Controller.cs +++ b/RemoteController/Controller.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace RemoteController; @@ -1059,7 +1059,7 @@ private unsafe static byte[] HandleRedrawActor(ReadOnlySpan args) if (flags.HasFlag(RedrawFlags.Facewear)) { request.FacewearId = MemoryMarshal.Read(args[offset..]); - offset += sizeof(uint); + offset += sizeof(ushort); } if (flags.HasFlag(RedrawFlags.Appearance)) From 4cc63f2170fb63c45abf6eb362233a88aad0e10f Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Thu, 13 Aug 2026 21:18:53 +0200 Subject: [PATCH 02/11] Fixed issue with weapons not redrawing properly in some cases --- Anamnesis/Actor/Refresh/AnamnesisActorRefresher.cs | 2 +- RemoteController/Controller.cs | 2 +- RemoteController/Drivers/Modules/RedrawModule.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Anamnesis/Actor/Refresh/AnamnesisActorRefresher.cs b/Anamnesis/Actor/Refresh/AnamnesisActorRefresher.cs index ac0f5506..c16b1a5f 100644 --- a/Anamnesis/Actor/Refresh/AnamnesisActorRefresher.cs +++ b/Anamnesis/Actor/Refresh/AnamnesisActorRefresher.cs @@ -154,7 +154,7 @@ private static (byte[] Buffer, int Length) PackRedrawPayload(ActorMemory actor, writer.WriteByte((byte)flags); - if (flags.HasFlag(RedrawFlags.Weapons)) + if (flags.HasFlag(RedrawFlags.Weapons) || flags.HasFlag(RedrawFlags.Appearance)) { writer.Write(actor.DrawData.MainHand?.WeaponModelId ?? default); writer.Write(actor.DrawData.OffHand?.WeaponModelId ?? default); diff --git a/RemoteController/Controller.cs b/RemoteController/Controller.cs index 073a694f..3f4f4027 100644 --- a/RemoteController/Controller.cs +++ b/RemoteController/Controller.cs @@ -1048,7 +1048,7 @@ private unsafe static byte[] HandleRedrawActor(ReadOnlySpan args) RedrawFlags flags = (RedrawFlags)args[offset++]; request.Flags = flags; - if (flags.HasFlag(RedrawFlags.Weapons)) + if (flags.HasFlag(RedrawFlags.Weapons) || flags.HasFlag(RedrawFlags.Appearance)) { request.MainHandId = MemoryMarshal.Read(args[offset..]); offset += Unsafe.SizeOf(); diff --git a/RemoteController/Drivers/Modules/RedrawModule.cs b/RemoteController/Drivers/Modules/RedrawModule.cs index 1d4347d1..f60bb9e8 100644 --- a/RemoteController/Drivers/Modules/RedrawModule.cs +++ b/RemoteController/Drivers/Modules/RedrawModule.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace RemoteController.Drivers; @@ -205,10 +205,10 @@ private unsafe byte[] ExecutePartialRedraw(nint gameObjPtr, RedrawRequest req) if (success) { - if (req.Flags.HasFlag(RedrawFlags.Weapons) && GposeDriver.InstanceOrNull?.IsInGpose == true) + if ((req.Flags.HasFlag(RedrawFlags.Weapons) || req.Flags.HasFlag(RedrawFlags.Appearance)) && GposeDriver.InstanceOrNull?.IsInGpose == true) { - this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.MainHand, req.MainHandId, 1, 0, 1, 0, 0); - this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.OffHand, req.OffHandId, 1, 0, 1, 0, 0); + this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.MainHand, req.MainHandId, 1, 0, 0, 0, 0); + this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.OffHand, req.OffHandId, 1, 0, 0, 0, 0); } if (req.Flags.HasFlag(RedrawFlags.Facewear)) From fdfbc12668fc89c138314c3c40e8071c2bd3f74a Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Thu, 13 Aug 2026 22:35:10 +0200 Subject: [PATCH 03/11] Improvements to weapon visibility toggle handling --- Anamnesis/Memory/DrawDataMemory.cs | 7 ++- Anamnesis/Memory/WeaponMemory.cs | 50 ++++++++--------- .../Drivers/Modules/RedrawModule.cs | 56 ++++++++++++++++++- .../Interop/Types/DrawDataContainer.cs | 36 +++++++++++- RemoteController/Interop/Types/DrawObject.cs | 20 ++++++- 5 files changed, 133 insertions(+), 36 deletions(-) diff --git a/Anamnesis/Memory/DrawDataMemory.cs b/Anamnesis/Memory/DrawDataMemory.cs index 77cadbda..e20541b7 100644 --- a/Anamnesis/Memory/DrawDataMemory.cs +++ b/Anamnesis/Memory/DrawDataMemory.cs @@ -5,8 +5,9 @@ namespace Anamnesis.Memory; using Anamnesis.Core.Extensions; using PropertyChanged; -using RemoteController.IPC; using System; +using RemoteController.Interop.Types; +using RemoteController.IPC; public class DrawDataMemory : MemoryBase { @@ -24,8 +25,8 @@ public enum CharacterFlagDefs : byte HeadgearEarsHidden = 1 << 5, } - [Bind(0x010)] public WeaponMemory? MainHand { get; set; } - [Bind(0x080)] public WeaponMemory? OffHand { get; set; } + [Bind(DrawDataContainer.MAIN_HAND_OFFSET)] public WeaponMemory? MainHand { get; set; } + [Bind(DrawDataContainer.OFF_HAND_OFFSET)] public WeaponMemory? OffHand { get; set; } [Bind(0x01D0)] public ActorEquipmentMemory? Equipment { get; set; } [Bind(0x0220)] public ActorCustomizeMemory? Customize { get; set; } diff --git a/Anamnesis/Memory/WeaponMemory.cs b/Anamnesis/Memory/WeaponMemory.cs index b9478d49..69226b6a 100644 --- a/Anamnesis/Memory/WeaponMemory.cs +++ b/Anamnesis/Memory/WeaponMemory.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace Anamnesis.Memory; @@ -19,14 +19,8 @@ public class WeaponMemory : MemoryBase, IEquipmentItemMemory private readonly Lock weaponLock = new(); private WeaponModelId weaponModelId; - [Flags] - public enum WeaponFlagDefs : byte - { - WeaponHidden = 1 << 1, - } - [AlsoNotifyFor(nameof(Set), nameof(Base), nameof(Variant), nameof(Dye), nameof(Dye2))] - [Bind(0x000, BindFlags.ActorRefresh | BindFlags.WeaponRefresh)] + [Bind(WeaponData.MODEL_ID_OFFSET, BindFlags.ActorRefresh | BindFlags.WeaponRefresh)] public WeaponModelId WeaponModelId { get @@ -140,39 +134,39 @@ public byte Dye2 } } - [Bind(0x018, BindFlags.Pointer)] public WeaponModelMemory? Model { get; set; } - [Bind(0x040)] public bool IsSheathed { get; set; } - [Bind(0x060)] public WeaponFlagDefs WeaponFlags { get; set; } + [Bind(WeaponData.DRAW_OBJECT_OFFSET, BindFlags.Pointer)] public WeaponModelMemory? Model { get; set; } + [Bind(WeaponData.STATE_OFFSET)] public WeaponStateFlags State { get; set; } - [DependsOn(nameof(WeaponFlags), nameof(IsSheathed))] + [DependsOn(nameof(State))] public bool WeaponHidden { - get => (this.IsSheathed && this.WeaponFlags.HasFlagUnsafe(WeaponFlagDefs.WeaponHidden)) || (!this.IsSheathed && this.Model?.Transform?.Scale == Vector3.Zero); + get => this.State.HasFlagUnsafe(WeaponStateFlags.Hidden); set { if (value) { - this.WeaponFlags |= WeaponFlagDefs.WeaponHidden; + this.State |= WeaponStateFlags.Hidden; } else { - this.WeaponFlags &= ~WeaponFlagDefs.WeaponHidden; - } - - if (this.Model?.Transform == null) - return; - - // If the weapon is unsheathed (in hands) the visibility flag won't work, - // so fall back to setting the weapons scale to 0. - if (!this.IsSheathed) - { - this.Model.Transform.Scale = value ? Vector3.Zero : Vector3.One; + this.State &= ~WeaponStateFlags.Hidden; } - // Special handling for a weapon with 0 scale that has been sheathed attempting to un-hide - else if (!value && this.Model.Transform.Scale == Vector3.Zero) + if (this.Model != null) { - this.Model.Transform.Scale = Vector3.One; + if (value) + { + this.Model.Flags |= (byte)DrawObjectFlags.Hidden; + } + else + { + // When unhiding, if weapon scale is set to 0 by the user, reset it to 1 so that the weapon is visible again. + this.Model.Flags = (byte)(this.Model.Flags & ~(byte)DrawObjectFlags.Hidden); + if (this.Model.Transform != null && this.Model.Transform.Scale == Vector3.Zero) + { + this.Model.Transform.Scale = Vector3.One; + } + } } } } diff --git a/RemoteController/Drivers/Modules/RedrawModule.cs b/RemoteController/Drivers/Modules/RedrawModule.cs index f60bb9e8..5b8130dc 100644 --- a/RemoteController/Drivers/Modules/RedrawModule.cs +++ b/RemoteController/Drivers/Modules/RedrawModule.cs @@ -207,8 +207,32 @@ private unsafe byte[] ExecutePartialRedraw(nint gameObjPtr, RedrawRequest req) { if ((req.Flags.HasFlag(RedrawFlags.Weapons) || req.Flags.HasFlag(RedrawFlags.Appearance)) && GposeDriver.InstanceOrNull?.IsInGpose == true) { + bool isMainHandHidden = drawData->MainHand.IsHidden; + bool isOffHandHidden = drawData->OffHand.IsHidden; + this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.MainHand, req.MainHandId, 1, 0, 0, 0, 0); this.loadWeapon.OriginalFunction(drawDataPtr, WeaponSlot.OffHand, req.OffHandId, 1, 0, 0, 0, 0); + + // Re-apply the weapon visibility flags after loading weapons + drawData->MainHand.IsHidden = isMainHandHidden; + if (drawData->MainHand.WeaponPtr != nint.Zero) + { + DrawObject* mainHandDrawObj = (DrawObject*)drawData->MainHand.WeaponPtr; + if (isMainHandHidden) + mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + else + mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + } + + drawData->OffHand.IsHidden = isOffHandHidden; + if (drawData->OffHand.WeaponPtr != nint.Zero) + { + DrawObject* offHandDrawObj = (DrawObject*)drawData->OffHand.WeaponPtr; + if (isOffHandHidden) + offHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + else + offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + } } if (req.Flags.HasFlag(RedrawFlags.Facewear)) @@ -232,10 +256,40 @@ private unsafe byte[] ExecutePartialRedraw(nint gameObjPtr, RedrawRequest req) return success ? [1] : [0]; } - private byte[] ExecuteFullRedraw(nint gameObjPtr) + private unsafe byte[] ExecuteFullRedraw(nint gameObjPtr) { + nint drawDataPtr = gameObjPtr + Actor.DRAW_DATA_OFFSET; + DrawDataContainerStruct* drawData = (DrawDataContainerStruct*)drawDataPtr; + if (drawData == null) + return [0]; + + bool isMainHandHidden = drawData->MainHand.IsHidden; + bool isOffHandHidden = drawData->OffHand.IsHidden; + this.charDisableDraw.OriginalFunction(gameObjPtr); this.charEnableDraw.OriginalFunction(gameObjPtr); + + // Re-apply the hide/show flags after the redraw + drawData->MainHand.IsHidden = isMainHandHidden; + if (drawData->MainHand.WeaponPtr != nint.Zero) + { + DrawObject* mainHandDrawObj = (DrawObject*)drawData->MainHand.WeaponPtr; + if (isMainHandHidden) + mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + else + mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + } + + drawData->OffHand.IsHidden = isOffHandHidden; + if (drawData->OffHand.WeaponPtr != nint.Zero) + { + DrawObject* offHandDrawObj = (DrawObject*)drawData->OffHand.WeaponPtr; + if (isOffHandHidden) + offHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + else + offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + } + return [1]; } diff --git a/RemoteController/Interop/Types/DrawDataContainer.cs b/RemoteController/Interop/Types/DrawDataContainer.cs index 0f63197b..d2372c39 100644 --- a/RemoteController/Interop/Types/DrawDataContainer.cs +++ b/RemoteController/Interop/Types/DrawDataContainer.cs @@ -1,15 +1,49 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace RemoteController.Interop.Types; using System.Runtime.InteropServices; +[Flags] +public enum WeaponStateFlags : ushort +{ + None = 0, + Hidden = 1 << 1, // 0x02: Weapon is hidden in DrawDataContainer + Loaded = 1 << 4, // 0x10: Weapon model object loaded +} + +[StructLayout(LayoutKind.Explicit, Size = 0x70)] +public struct WeaponData +{ + public const int MODEL_ID_OFFSET = 0x000; + public const int WEAPON_PTR_OFFSET = 0x008; + public const int DRAW_OBJECT_OFFSET = 0x018; + public const int STATE_OFFSET = 0x060; + + [FieldOffset(MODEL_ID_OFFSET)] public WeaponModelId ModelId; + [FieldOffset(WEAPON_PTR_OFFSET)] public nint WeaponPtr; + [FieldOffset(DRAW_OBJECT_OFFSET)] public nint DrawObjectPtr; + [FieldOffset(STATE_OFFSET)] public WeaponStateFlags State; + + public bool IsHidden + { + readonly get => this.State.HasFlag(WeaponStateFlags.Hidden); + set => this.State = value + ? this.State | WeaponStateFlags.Hidden + : this.State & ~WeaponStateFlags.Hidden; + } +} + [StructLayout(LayoutKind.Explicit, Size = 0x268)] public struct DrawDataContainer { + public const int MAIN_HAND_OFFSET = 0x010; + public const int OFF_HAND_OFFSET = 0x080; public const int FACEWEAR_DIRTY_FLAG = 0x248; + [FieldOffset(MAIN_HAND_OFFSET)] public WeaponData MainHand; + [FieldOffset(OFF_HAND_OFFSET)] public WeaponData OffHand; [FieldOffset(0x23E)] public byte DrawFlags; [FieldOffset(0x240)] public ushort FacewearId; [FieldOffset(FACEWEAR_DIRTY_FLAG)] public byte FacewearDirtyFlag; diff --git a/RemoteController/Interop/Types/DrawObject.cs b/RemoteController/Interop/Types/DrawObject.cs index 3b160646..65940e81 100644 --- a/RemoteController/Interop/Types/DrawObject.cs +++ b/RemoteController/Interop/Types/DrawObject.cs @@ -1,10 +1,24 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace RemoteController.Interop.Types; +using System; using System.Runtime.InteropServices; +[Flags] +public enum DrawObjectFlags : byte +{ + None = 0, + Hidden = 1 << 0, // 0x01: Suppresses DirectX 3D model rendering in CharacterBase::UpdateRender + InWorld = 1 << 1, // 0x02: Object attached to scene world node + Loaded = 1 << 3, // 0x08: Model resources initialized + Culled = 1 << 6, // 0x40: Frustum / occlusion culled + + // Combined mask for checking/setting initialized and visible draw objects (0x09: Loaded | Hidden) + ActiveRenderState = Loaded | Hidden, +} + [StructLayout(LayoutKind.Explicit, Size = 0x090)] public struct DrawObject { @@ -14,7 +28,7 @@ public struct DrawObject public bool IsVisible { - readonly get => (this.Flags & 0x09) == 0x09; - set => this.Flags = (byte)(value ? this.Flags | 0x09 : this.Flags & ~0x09); + readonly get => (this.Flags & (byte)DrawObjectFlags.ActiveRenderState) == (byte)DrawObjectFlags.ActiveRenderState; + set => this.Flags = (byte)(value ? this.Flags | (byte)DrawObjectFlags.ActiveRenderState : this.Flags & ~(byte)DrawObjectFlags.ActiveRenderState); } } From 34d9b5accb62c014984f35a5aa214b41fd46c399 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Fri, 14 Aug 2026 01:05:22 +0200 Subject: [PATCH 04/11] Added toggle to synchronize weapon dyes (enabled by default) --- Anamnesis/Actor/Views/EquipmentSelector.xaml | 23 ++++-- .../Actor/Views/EquipmentSelector.xaml.cs | 11 ++- Anamnesis/Actor/Views/ItemView.xaml.cs | 73 +++++++++++++------ Anamnesis/Languages/en.json | 4 +- 4 files changed, 80 insertions(+), 31 deletions(-) diff --git a/Anamnesis/Actor/Views/EquipmentSelector.xaml b/Anamnesis/Actor/Views/EquipmentSelector.xaml index f15f5083..c0b4098e 100644 --- a/Anamnesis/Actor/Views/EquipmentSelector.xaml +++ b/Anamnesis/Actor/Views/EquipmentSelector.xaml @@ -1,15 +1,15 @@ - @@ -163,21 +163,28 @@ Margin="6, 0, 0, 0" Visibility="{Binding IsWeaponSlot, Converter={StaticResource B2V}}"> - + - + + + + + + + + - + diff --git a/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs b/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs index ad3c758c..421cf127 100644 --- a/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs +++ b/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace Anamnesis.Actor.Views; @@ -36,6 +36,7 @@ public partial class EquipmentSelector : EquipmentSelectorDrawer private static bool s_showFilters = false; private static bool s_forceMainModel = false; private static bool s_forceOffModel = false; + private static bool s_linkDyes = true; private static SortModes s_sortMode = SortModes.Row; private readonly Memory.ActorMemory? actor; @@ -63,6 +64,8 @@ public enum SortModes Level, } + public static bool LinkWeaponDyes => s_linkDyes; + // Suppress CA1822: The properties are used in WPF bindings. Do not set them to static. #pragma warning disable CA1822 public bool ShowFilters @@ -76,6 +79,12 @@ public bool AutoOffhand get => s_autoOffhand; set => s_autoOffhand = value; } + + public bool LinkDyes + { + get => s_linkDyes; + set => s_linkDyes = value; + } #pragma warning restore CA1822 public bool IsGpose { get; set; } diff --git a/Anamnesis/Actor/Views/ItemView.xaml.cs b/Anamnesis/Actor/Views/ItemView.xaml.cs index 13564f7c..3a111492 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml.cs +++ b/Anamnesis/Actor/Views/ItemView.xaml.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace Anamnesis.Actor.Views; @@ -156,6 +156,17 @@ public bool IsValidItemForCopy } } + private WeaponMemory? PartnerWeapon + { + get + { + if (!this.IsWeapon || !EquipmentSelector.LinkWeaponDyes || this.Actor?.DrawData == null) + return null; + + return (this.Slot == ItemSlots.MainHand) ? this.Actor.DrawData.OffHand : (this.Slot == ItemSlots.OffHand) ? this.Actor.DrawData.MainHand : null; + } + } + private static void OnItemModelChanged(ItemView sender, IEquipmentItemMemory? value) { if (sender.ItemModel != null) @@ -278,6 +289,33 @@ private void OnSlotMouseUp(object sender, MouseButtonEventArgs e) } } + private void SetDye(byte dyeId, bool isDye2) + { + if (this.ItemModel == null) + return; + + if (isDye2) + { + this.ItemModel.Dye2 = dyeId; + } + else + { + this.ItemModel.Dye = dyeId; + } + + if (this.PartnerWeapon is WeaponMemory partner) + { + if (isDye2) + { + partner.Dye2 = dyeId; + } + else + { + partner.Dye = dyeId; + } + } + } + private void OnDyeMouseUp(object sender, MouseButtonEventArgs e) { if (this.Actor?.CanRefresh != true || this.ItemModel == null) @@ -285,7 +323,7 @@ private void OnDyeMouseUp(object sender, MouseButtonEventArgs e) if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released) { - this.ItemModel.Dye = 0; + this.SetDye(0, isDye2: false); } } @@ -296,13 +334,14 @@ private void OnDye2MouseUp(object sender, MouseButtonEventArgs e) if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released) { - this.ItemModel.Dye2 = 0; + this.SetDye(0, isDye2: true); } } private void OnSwapDyeChannels(object sender, RoutedEventArgs e) { this.ItemModel?.SwapDyeChannels(); + this.PartnerWeapon?.SwapDyeChannels(); } private void SetItem(IItem? item, bool autoOffhand = false, bool forceMain = false, bool forceOff = false) @@ -339,6 +378,12 @@ private void SetItem(IItem? item, bool autoOffhand = false, bool forceMain = fal if (ivm.HasSubModel) { SetModel(this.Actor?.DrawData.OffHand, ivm.SubModelSet, ivm.SubModelBase, ivm.SubModelVariant); + + if (EquipmentSelector.LinkWeaponDyes && this.ItemModel != null && this.Actor?.DrawData.OffHand is WeaponMemory offWeapon) + { + offWeapon.Dye = this.ItemModel.Dye; + offWeapon.Dye2 = this.ItemModel.Dye2; + } } else { @@ -364,16 +409,9 @@ private void OnDyeClick(object sender, RoutedEventArgs e) SelectorDrawer.Show(this.Dye, (v) => { - if (v == null) - return; - - if (this.ItemModel is ItemMemory item) - { - item.Dye = v.Id; - } - else if (this.ItemModel is WeaponMemory weapon) + if (v != null) { - weapon.Dye = v.Id; + this.SetDye(v.Id, isDye2: false); } }); } @@ -385,16 +423,9 @@ private void OnDyeClick2(object sender, RoutedEventArgs e) SelectorDrawer.Show(this.Dye2, (v) => { - if (v == null) - return; - - if (this.ItemModel is ItemMemory item) - { - item.Dye2 = v.Id; - } - else if (this.ItemModel is WeaponMemory weapon) + if (v != null) { - weapon.Dye2 = v.Id; + this.SetDye(v.Id, isDye2: true); } }); } diff --git a/Anamnesis/Languages/en.json b/Anamnesis/Languages/en.json index ef86fc82..23e8e5d0 100644 --- a/Anamnesis/Languages/en.json +++ b/Anamnesis/Languages/en.json @@ -81,8 +81,10 @@ "EquipmentSelector_AutoOffhandTip": "Automatically equip paired weapons in the off-hand slot, and unequip the off-hand when equipping two-handed weapons", "EquipmentSelector_ForceMainModel": "Force main hand model", "EquipmentSelector_ForceMainModelTooltip": "Use the main hand model instead of the off-hand model.", - "EquipmentSelector_ForceOffModel": "Force off hand model", + "EquipmentSelector_ForceOffModel": "Force off-hand model", "EquipmentSelector_ForceOffModelTooltip": "Use the off-hand model instead of the main hand model.", + "EquipmentSelector_LinkDyes": "Link main & off-hand dyes", + "EquipmentSelector_LinkDyesTip": "Synchronize dye changes between main hand and off-hand", "EquipmentSelector_FilterOther": "Other", "EquipmentSelector_Categories": "Categories", From 9f50c3f94ca160530daf119c7844dce223baa8e9 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 02:11:58 +0200 Subject: [PATCH 05/11] Fixed off-hand weapons not importing correctly with NPC appearances --- Anamnesis/GameData/Excel/EventNpc.cs | 4 ++-- Anamnesis/GameData/Interfaces/INpcBase.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Anamnesis/GameData/Excel/EventNpc.cs b/Anamnesis/GameData/Excel/EventNpc.cs index 553f100f..ceb0badf 100644 --- a/Anamnesis/GameData/Excel/EventNpc.cs +++ b/Anamnesis/GameData/Excel/EventNpc.cs @@ -287,7 +287,7 @@ static EventNpc IExcelRow.Create(ExcelPage page, uint offset, uint row /// The model base value of the item. /// The equipment model value of the item. /// - /// The weapon item from game data, or + /// The weapon item from game data, or /// if none is found. /// private static IItem GetWeaponItem(ItemSlots slot, ulong baseVal, ulong? equipVal) @@ -298,7 +298,7 @@ private static IItem GetWeaponItem(ItemSlots slot, ulong baseVal, ulong? equipVa if (equipVal != null && equipVal != 0 && equipVal != uint.MaxValue && equipVal != long.MaxValue) return LuminaExtensions.GetWeaponItem(slot, (ulong)equipVal); - return ItemUtility.EmperorsNewFists; + return ItemUtility.NoneItem; } /// diff --git a/Anamnesis/GameData/Interfaces/INpcBase.cs b/Anamnesis/GameData/Interfaces/INpcBase.cs index d14dea31..aec8eea5 100644 --- a/Anamnesis/GameData/Interfaces/INpcBase.cs +++ b/Anamnesis/GameData/Interfaces/INpcBase.cs @@ -398,7 +398,7 @@ public static CharacterFile ToFile(this INpcBase npc) { save.ModelSet = item.SubModelSet; save.ModelBase = item.SubModelBase; - save.ModelVariant = item.SubModelBase; + save.ModelVariant = item.SubModelVariant; } return save; From 0a747d7c553bb5abc5e7c68613315badc5d14fb7 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 16:08:01 +0200 Subject: [PATCH 06/11] Fixed localized tooltips not displaying correctly for item view component --- Anamnesis/Actor/Views/ItemView.xaml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/Anamnesis/Actor/Views/ItemView.xaml b/Anamnesis/Actor/Views/ItemView.xaml index c0901823..a62a595b 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml +++ b/Anamnesis/Actor/Views/ItemView.xaml @@ -151,16 +151,25 @@ + Visibility="{Binding IsWeapon, Converter={StaticResource B2V}, Mode=OneWay}"> + + + + - + + + + + - + + + + + From 4daafab4375bbc04677a2038e221b02fca42f822 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 16:58:58 +0200 Subject: [PATCH 07/11] Added main hand and off-hand dye channel link toggle to dye component context menu --- .../Actor/Views/EquipmentSelector.xaml.cs | 6 +++- Anamnesis/Actor/Views/ItemView.xaml | 30 ++++++++++++++++++- Anamnesis/Actor/Views/ItemView.xaml.cs | 15 ++++++++++ Anamnesis/Languages/en.json | 4 ++- 4 files changed, 52 insertions(+), 3 deletions(-) diff --git a/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs b/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs index 421cf127..0dfed554 100644 --- a/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs +++ b/Anamnesis/Actor/Views/EquipmentSelector.xaml.cs @@ -64,7 +64,11 @@ public enum SortModes Level, } - public static bool LinkWeaponDyes => s_linkDyes; + public static bool LinkWeaponDyes + { + get => s_linkDyes; + set => s_linkDyes = value; + } // Suppress CA1822: The properties are used in WPF bindings. Do not set them to static. #pragma warning disable CA1822 diff --git a/Anamnesis/Actor/Views/ItemView.xaml b/Anamnesis/Actor/Views/ItemView.xaml index a62a595b..8be82963 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml +++ b/Anamnesis/Actor/Views/ItemView.xaml @@ -1,4 +1,4 @@ - + + + + + + + + + + + + + + @@ -293,6 +307,20 @@ + + + + + + + + + + + + + + diff --git a/Anamnesis/Actor/Views/ItemView.xaml.cs b/Anamnesis/Actor/Views/ItemView.xaml.cs index 3a111492..5c7eef89 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml.cs +++ b/Anamnesis/Actor/Views/ItemView.xaml.cs @@ -94,6 +94,16 @@ public WeaponSubModelMemory? ExtendedViewModel [DependsOn(nameof(IsWeapon))] public bool ShowWeaponWarning => this.IsWeapon && GposeService.InstanceOrNull?.IsGpose != true; + public bool LinkWeaponDyes + { + get => EquipmentSelector.LinkWeaponDyes; + set + { + EquipmentSelector.LinkWeaponDyes = value; + this.OnPropertyChanged(nameof(this.LinkWeaponDyes)); + } + } + public bool IsValidWeapon { get @@ -344,6 +354,11 @@ private void OnSwapDyeChannels(object sender, RoutedEventArgs e) this.PartnerWeapon?.SwapDyeChannels(); } + private void OnToggleLinkDyes(object sender, RoutedEventArgs e) + { + this.LinkWeaponDyes = !this.LinkWeaponDyes; + } + private void SetItem(IItem? item, bool autoOffhand = false, bool forceMain = false, bool forceOff = false) { if (this.IsWeapon && GposeService.InstanceOrNull?.IsGpose != true) diff --git a/Anamnesis/Languages/en.json b/Anamnesis/Languages/en.json index 23e8e5d0..67f37760 100644 --- a/Anamnesis/Languages/en.json +++ b/Anamnesis/Languages/en.json @@ -83,8 +83,10 @@ "EquipmentSelector_ForceMainModelTooltip": "Use the main hand model instead of the off-hand model.", "EquipmentSelector_ForceOffModel": "Force off-hand model", "EquipmentSelector_ForceOffModelTooltip": "Use the off-hand model instead of the main hand model.", - "EquipmentSelector_LinkDyes": "Link main & off-hand dyes", + "EquipmentSelector_LinkDyes": "Link main & off-hand dye channels", "EquipmentSelector_LinkDyesTip": "Synchronize dye changes between main hand and off-hand", + "EquipmentSelector_UnlinkDyes": "Unlink main & off-hand dye channels", + "EquipmentSelector_UnlinkDyesTip": "Stop synchronizing dye changes between main hand and off-hand", "EquipmentSelector_FilterOther": "Other", "EquipmentSelector_Categories": "Categories", From 0011ad062b3a3bc3e5f7aacb6986c5a115a5280b Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 18:04:29 +0200 Subject: [PATCH 08/11] Tidied up code --- Anamnesis/Actor/Items/DummyItem.cs | 32 ++++++-- Anamnesis/Actor/Utilities/ItemUtility.cs | 79 +++++++------------ Anamnesis/GameData/Excel/Equipment.cs | 23 +----- .../Converters/IItemConverter.cs | 3 +- 4 files changed, 59 insertions(+), 78 deletions(-) diff --git a/Anamnesis/Actor/Items/DummyItem.cs b/Anamnesis/Actor/Items/DummyItem.cs index 078354ee..75e1c4b7 100644 --- a/Anamnesis/Actor/Items/DummyItem.cs +++ b/Anamnesis/Actor/Items/DummyItem.cs @@ -6,12 +6,32 @@ namespace Anamnesis.Actor.Items; using Anamnesis.GameData.Sheets; using Anamnesis.Services; using Anamnesis.TexTools; +using Lumina; using System.Runtime.CompilerServices; -public class DummyItem(ushort modelSet, ushort modelBase, ushort modelVariant) : IItem +public class DummyItem : IItem { + public DummyItem(ushort modelSet, ushort modelBase, ushort modelVariant) + { + this.ModelSet = modelSet; + this.ModelBase = modelBase; + this.ModelVariant = modelVariant; + this.Model = ExcelPageExtensions.ConvertToModel(modelSet, modelBase, modelVariant); + this.IsWeapon = modelSet != 0; + } + + public DummyItem(ulong model, bool isWeapon = false) + { + this.Model = model; + this.IsWeapon = isWeapon; + LuminaExtensions.GetModel(model, isWeapon, out ushort modelSet, out ushort modelBase, out ushort modelVariant); + this.ModelSet = modelSet; + this.ModelBase = modelBase; + this.ModelVariant = modelVariant; + } + public uint RowId => 0; - public bool IsWeapon => true; + public bool IsWeapon { get; private set; } public bool HasSubModel => false; public string Name => LocalizationService.GetString("Item_Unknown"); public string Description => string.Empty; @@ -20,10 +40,10 @@ public class DummyItem(ushort modelSet, ushort modelBase, ushort modelVariant) : public Mod? Mod => null; public byte EquipLevel => 0; - public ulong Model => ExcelPageExtensions.ConvertToModel(this.ModelSet, this.ModelBase, this.ModelVariant); - public ushort ModelBase { get; private set; } = modelBase; - public ushort ModelVariant { get; private set; } = modelVariant; - public ushort ModelSet { get; private set; } = modelSet; + public ulong Model { get; private set; } + public ushort ModelBase { get; private set; } + public ushort ModelVariant { get; private set; } + public ushort ModelSet { get; private set; } public ulong SubModel => ExcelPageExtensions.ConvertToModel(this.SubModelSet, this.SubModelBase, this.SubModelVariant); public ushort SubModelBase { get; } diff --git a/Anamnesis/Actor/Utilities/ItemUtility.cs b/Anamnesis/Actor/Utilities/ItemUtility.cs index 6ad18a5c..a82faa23 100644 --- a/Anamnesis/Actor/Utilities/ItemUtility.cs +++ b/Anamnesis/Actor/Utilities/ItemUtility.cs @@ -4,10 +4,12 @@ namespace Anamnesis.Actor.Utilities; using Anamnesis.Actor.Items; +using Anamnesis.Core.Extensions; using Anamnesis.GameData; using Anamnesis.GameData.Excel; using Anamnesis.GameData.Sheets; using Anamnesis.Services; +using System; using System.Linq; public static class ItemUtility @@ -33,42 +35,30 @@ public static class ItemUtility /// /// Searches the gamedata service item list for an item with the given model attributes. /// - public static IItem GetItem(ItemSlots slot, ushort modelSet, ushort modelBase, ushort modelVariant, bool isChocobo) + public static IItem GetItem(ItemSlots slot, ushort modelSet, ushort modelBase, ushort modelVariant, bool isChocobo = false) { - if ((modelBase == 0 || modelBase == 1) && modelVariant == 0) + bool isWeaponSlot = (slot & ItemSlots.Weapons) != 0; + if ((isWeaponSlot && modelSet == 0) || modelBase == 0) return NoneItem; - if (modelBase == NpcBodyItem.ModelBase) - return NpcBodyItem; - - return isChocobo - ? ChocoboItemSearch(slot, modelSet, modelBase, modelVariant) - : ItemSearch(slot, modelSet, modelBase, modelVariant); - } - - public static IItem GetDummyItem(ushort modelSet, ushort modelBase, ushort modelVariant) - { - var model = ExcelPageExtensions.ConvertToModel(modelSet, modelBase, modelVariant); - - if (NoneItem.Model == model) - return NoneItem; + ulong model = ExcelPageExtensions.ConvertToModel(modelSet, modelBase, modelVariant); - if (NpcBodyItem.Model == model) + if (model == NpcBodyItem.Model) return NpcBodyItem; - if (InvisibileBodyItem.Model == model) + if (model == InvisibileBodyItem.Model) return InvisibileBodyItem; - if (InvisibileHeadItem.Model == model) + if (model == InvisibileHeadItem.Model) return InvisibileHeadItem; - return new DummyItem(modelSet, modelBase, modelVariant); + return isChocobo + ? ChocoboItemSearch(slot, model) + : ItemSearch(slot, model, isWeaponSlot); } - private static IItem ChocoboItemSearch(ItemSlots slot, ushort modelSet, ushort modelBase, ushort modelVariant) + private static IItem ChocoboItemSearch(ItemSlots slot, ulong model) { - var model = ExcelPageExtensions.ConvertToModel(modelSet, modelBase, modelVariant); - if (slot == ItemSlots.Legs) { if (YellowChocoboSkin.Model == model) @@ -81,52 +71,43 @@ private static IItem ChocoboItemSearch(ItemSlots slot, ushort modelSet, ushort m { foreach (BuddyEquip equip in GameDataService.BuddyEquips) { - if (equip.Head != null && equip.Head.Slot == slot && equip.Head.Model == model) - return equip.Head; - - if (equip.Body != null && equip.Body.Slot == slot && equip.Body.Model == model) - return equip.Body; - - if (equip.Feet != null && equip.Feet.Slot == slot && equip.Feet.Model == model) - return equip.Feet; + BuddyEquip.BuddyItem? item = slot switch + { + ItemSlots.Head => equip.Head, + ItemSlots.Body => equip.Body, + ItemSlots.Legs => equip.Feet, + _ => null, + }; + + if (item != null && item.Model == model) + return item; } } - return new DummyItem(modelSet, modelBase, modelVariant); + return new DummyItem(model, isWeapon: false); } - private static IItem ItemSearch(ItemSlots slot, ushort modelSet, ushort modelBase, ushort modelVariant) + private static IItem ItemSearch(ItemSlots slot, ulong model, bool isWeaponSlot) { - var model = ExcelPageExtensions.ConvertToModel(modelSet, modelBase, modelVariant); - foreach (uint rowId in GameDataService.ItemsByModel[model]) { var tItem = GameDataService.Items.GetRow(rowId); - if (slot == ItemSlots.MainHand || slot == ItemSlots.OffHand) - { - if (!tItem.IsWeapon) - continue; - } - else - { - if (!tItem.FitsInSlot(slot)) - continue; - } + if (isWeaponSlot ? !tItem.IsWeapon : !tItem.FitsInSlot(slot)) + continue; // Big old hack, but we prefer the emperors bracelets to the promise bracelets (even though they are the same model) - if (slot == ItemSlots.Wrists && tItem.Name.StartsWith("Promise of")) + if (slot == ItemSlots.Wrists && tItem.Name.StartsWith("Promise of", StringComparison.Ordinal)) continue; return tItem; } - if (slot == ItemSlots.MainHand || slot == ItemSlots.OffHand) + if (isWeaponSlot) { foreach (uint rowId in GameDataService.ItemsBySubModel[model]) { var tItem = GameDataService.Items.GetRow(rowId); - if (tItem.IsWeapon) return tItem; } @@ -144,6 +125,6 @@ private static IItem ItemSearch(ItemSlots slot, ushort modelSet, ushort modelBas return tItem; } - return new DummyItem(modelSet, modelBase, modelVariant); + return new DummyItem(model, isWeapon: isWeaponSlot); } } diff --git a/Anamnesis/GameData/Excel/Equipment.cs b/Anamnesis/GameData/Excel/Equipment.cs index 1056cbad..0d5ee9b7 100644 --- a/Anamnesis/GameData/Excel/Equipment.cs +++ b/Anamnesis/GameData/Excel/Equipment.cs @@ -98,26 +98,5 @@ public bool IsFavorite /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool FitsInSlot(ItemSlots slot) - { - return slot switch - { - ItemSlots.MainHand => this.Slot.HasFlagUnsafe(ItemSlots.MainHand), - ItemSlots.OffHand => this.Slot.HasFlagUnsafe(ItemSlots.OffHand), - ItemSlots.Head => this.Slot.HasFlagUnsafe(ItemSlots.Head), - ItemSlots.Body => this.Slot.HasFlagUnsafe(ItemSlots.Body), - ItemSlots.Hands => this.Slot.HasFlagUnsafe(ItemSlots.Hands), - ItemSlots.Waist => this.Slot.HasFlagUnsafe(ItemSlots.Waist), - ItemSlots.Legs => this.Slot.HasFlagUnsafe(ItemSlots.Legs), - ItemSlots.Feet => this.Slot.HasFlagUnsafe(ItemSlots.Feet), - ItemSlots.Ears => this.Slot.HasFlagUnsafe(ItemSlots.Ears), - ItemSlots.Neck => this.Slot.HasFlagUnsafe(ItemSlots.Neck), - ItemSlots.Wrists => this.Slot.HasFlagUnsafe(ItemSlots.Wrists), - ItemSlots.RightRing => this.Slot.HasFlagUnsafe(ItemSlots.RightRing), - ItemSlots.LeftRing => this.Slot.HasFlagUnsafe(ItemSlots.LeftRing), - ItemSlots.Glasses => this.Slot.HasFlagUnsafe(ItemSlots.Glasses), - ItemSlots.SoulCrystal => this.Slot.HasFlagUnsafe(ItemSlots.SoulCrystal), - _ => false, - }; - } + public bool FitsInSlot(ItemSlots slot) => (this.Slot & slot) != 0; } diff --git a/Anamnesis/Serialization/Converters/IItemConverter.cs b/Anamnesis/Serialization/Converters/IItemConverter.cs index ad475dfb..e0365037 100644 --- a/Anamnesis/Serialization/Converters/IItemConverter.cs +++ b/Anamnesis/Serialization/Converters/IItemConverter.cs @@ -40,7 +40,8 @@ public static IItem FromString(string str) if (str.Contains(',')) { (ushort modelSet, ushort modelBase, ushort modelVariant) = SplitString(str); - return ItemUtility.GetDummyItem(modelSet, modelBase, modelVariant); + ItemSlots slot = modelSet != 0 ? ItemSlots.Weapons : ItemSlots.All; + return ItemUtility.GetItem(slot, modelSet, modelBase, modelVariant); } else { From 1d3b53ca478f2ce230853831b5e42715bbb6302b Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 18:38:26 +0200 Subject: [PATCH 09/11] Fixed issue where items with same (sub)model do not get selected correctly --- Anamnesis/Actor/Views/ItemView.xaml.cs | 56 +++++++++++++++++++++--- Anamnesis/Memory/IEquipmentItemMemory.cs | 2 + Anamnesis/Memory/ItemMemory.cs | 7 ++- Anamnesis/Memory/WeaponMemory.cs | 2 + 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/Anamnesis/Actor/Views/ItemView.xaml.cs b/Anamnesis/Actor/Views/ItemView.xaml.cs index 5c7eef89..9fc22e74 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml.cs +++ b/Anamnesis/Actor/Views/ItemView.xaml.cs @@ -216,6 +216,21 @@ private static void SetModel(IEquipmentItemMemory? itemModel, ushort modelSet, u } } + private static bool MatchesWeapon(IItem item, WeaponMemory weaponVm, ItemSlots slot) + { + // For Off-Hand paired weapons, match against the SubModel + if (slot == ItemSlots.OffHand && item.HasSubModel) + { + return item.SubModelSet == weaponVm.Set + && item.SubModelBase == weaponVm.Base + && item.SubModelVariant == weaponVm.Variant; + } + + return item.ModelSet == weaponVm.Set + && item.ModelBase == weaponVm.Base + && item.ModelVariant == weaponVm.Variant; + } + private void OnOpenInConsoleGamesWikiClicked(object sender, RoutedEventArgs e) { this.OpenItemInFanSiteUrl("https://ffxiv.consolegameswiki.com/wiki/" + this.Item?.Name.Replace(" ", "_")); @@ -366,6 +381,9 @@ private void SetItem(IItem? item, bool autoOffhand = false, bool forceMain = fal this.lockViewModel = true; + if (this.ItemModel != null) + this.ItemModel.EquippedItem = item; + if (item != null) { bool useSubModel = this.Slot == ItemSlots.OffHand && item.HasSubModel; @@ -394,15 +412,21 @@ private void SetItem(IItem? item, bool autoOffhand = false, bool forceMain = fal { SetModel(this.Actor?.DrawData.OffHand, ivm.SubModelSet, ivm.SubModelBase, ivm.SubModelVariant); - if (EquipmentSelector.LinkWeaponDyes && this.ItemModel != null && this.Actor?.DrawData.OffHand is WeaponMemory offWeapon) + if (this.Actor?.DrawData.OffHand is WeaponMemory offWeapon) { - offWeapon.Dye = this.ItemModel.Dye; - offWeapon.Dye2 = this.ItemModel.Dye2; + offWeapon.EquippedItem = ivm; + if (EquipmentSelector.LinkWeaponDyes && this.ItemModel != null) + { + offWeapon.Dye = this.ItemModel.Dye; + offWeapon.Dye2 = this.ItemModel.Dye2; + } } } else { SetModel(this.Actor?.DrawData.OffHand, 0, 0, 0); + if (this.Actor?.DrawData.OffHand is WeaponMemory offWeapon) + offWeapon.EquippedItem = ItemUtility.NoneItem; } } @@ -476,7 +500,15 @@ private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs if (valueVm is ItemMemory itemVm) { - IItem? item = ItemUtility.GetItem(slots, 0, itemVm.Base, itemVm.Variant, this.Actor.IsChocobo); + // Do not re-fetch if the selected item is the same as the current item + IItem? item = itemVm.EquippedItem ?? this.Item; + if (item == null || item.ModelBase != itemVm.Base || item.ModelVariant != itemVm.Variant) + { + item = ItemUtility.GetItem(slots, 0, itemVm.Base, itemVm.Variant, this.Actor.IsChocobo); + } + + itemVm.EquippedItem = item; + IDye? dye; IDye? dye2; @@ -506,7 +538,21 @@ private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs } else if (valueVm is WeaponMemory weaponVm) { - IItem? item = ItemUtility.GetItem(slots, weaponVm.Set, weaponVm.Base, weaponVm.Variant, this.Actor.IsChocobo); + // Do not re-fetch if the selected item is the same as the current item + IItem? item = weaponVm.EquippedItem ?? this.Item; + + // Check if MainHand has a paired sub-model matching this OffHand + if (item == null && slots == ItemSlots.OffHand && this.Actor.DrawData.MainHand?.EquippedItem is IItem mainItem && MatchesWeapon(mainItem, weaponVm, slots)) + { + item = mainItem; + } + + if (item == null || !MatchesWeapon(item, weaponVm, slots)) + { + item = ItemUtility.GetItem(slots, weaponVm.Set, weaponVm.Base, weaponVm.Variant, this.Actor.IsChocobo); + } + + weaponVm.EquippedItem = item; if (weaponVm.Set == 0) weaponVm.Dye = 0; diff --git a/Anamnesis/Memory/IEquipmentItemMemory.cs b/Anamnesis/Memory/IEquipmentItemMemory.cs index 2ef877f8..aecb3ec9 100644 --- a/Anamnesis/Memory/IEquipmentItemMemory.cs +++ b/Anamnesis/Memory/IEquipmentItemMemory.cs @@ -3,6 +3,7 @@ namespace Anamnesis.Memory; +using Anamnesis.GameData; using System.ComponentModel; public interface IEquipmentItemMemory : INotifyPropertyChanged @@ -11,6 +12,7 @@ public interface IEquipmentItemMemory : INotifyPropertyChanged byte Dye { get; set; } byte Dye2 { get; set; } ushort Set { get; set; } + IItem? EquippedItem { get; set; } public void SwapDyeChannels(); public void Clear(bool isPlayer); diff --git a/Anamnesis/Memory/ItemMemory.cs b/Anamnesis/Memory/ItemMemory.cs index a0bde65c..6450c9a7 100644 --- a/Anamnesis/Memory/ItemMemory.cs +++ b/Anamnesis/Memory/ItemMemory.cs @@ -36,6 +36,9 @@ public ItemModelId ItemId } } + // Items dont have a 'Set' but the UI wants to bind to something, so... + public ushort Set { get; set; } = 0; + [AlsoNotifyFor(nameof(ItemId))] public ushort Base { @@ -112,8 +115,8 @@ public byte Dye2 } } - // Items dont have a 'Set' but the UI wants to bind to something, so... - public ushort Set { get; set; } = 0; + public IItem? EquippedItem { get; set; } + public bool WeaponHidden { get; set; } = false; public void Clear(bool isPlayer) diff --git a/Anamnesis/Memory/WeaponMemory.cs b/Anamnesis/Memory/WeaponMemory.cs index 69226b6a..a4c6c6d8 100644 --- a/Anamnesis/Memory/WeaponMemory.cs +++ b/Anamnesis/Memory/WeaponMemory.cs @@ -137,6 +137,8 @@ public byte Dye2 [Bind(WeaponData.DRAW_OBJECT_OFFSET, BindFlags.Pointer)] public WeaponModelMemory? Model { get; set; } [Bind(WeaponData.STATE_OFFSET)] public WeaponStateFlags State { get; set; } + public IItem? EquippedItem { get; set; } + [DependsOn(nameof(State))] public bool WeaponHidden { From ec52ee55a7a65fe5960c042a312c167627126876 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 20:16:22 +0200 Subject: [PATCH 10/11] Fixed actors not being rendered on full redraw in edge cases; Fixed draw object flag naming --- Anamnesis/Files/CharacterFile.cs | 16 ++----- Anamnesis/Memory/DrawObjectMemory.cs | 6 +-- Anamnesis/Memory/GameObjectMemory.cs | 11 +---- Anamnesis/Memory/WeaponMemory.cs | 4 +- .../Drivers/Modules/RedrawModule.cs | 22 +++++---- RemoteController/Interop/Types/DrawObject.cs | 6 +-- RemoteController/Interop/Types/GameObject.cs | 45 ++++++++++++++++++- 7 files changed, 70 insertions(+), 40 deletions(-) diff --git a/Anamnesis/Files/CharacterFile.cs b/Anamnesis/Files/CharacterFile.cs index bae12c34..f020d1da 100644 --- a/Anamnesis/Files/CharacterFile.cs +++ b/Anamnesis/Files/CharacterFile.cs @@ -910,19 +910,9 @@ public void Write(WeaponMemory? vm, bool isMainHand) } else { - if (isMainHand) - { - vm.Set = ItemUtility.EmperorsNewFists.ModelSet; - vm.Base = ItemUtility.EmperorsNewFists.ModelBase; - vm.Variant = ItemUtility.EmperorsNewFists.ModelVariant; - } - else - { - vm.Set = 0; - vm.Base = 0; - vm.Variant = 0; - } - + vm.Set = 0; + vm.Base = 0; + vm.Variant = 0; vm.Dye = ItemUtility.NoneDye.Id; vm.Dye2 = ItemUtility.NoneDye.Id; } diff --git a/Anamnesis/Memory/DrawObjectMemory.cs b/Anamnesis/Memory/DrawObjectMemory.cs index 19f9977d..99df66a4 100644 --- a/Anamnesis/Memory/DrawObjectMemory.cs +++ b/Anamnesis/Memory/DrawObjectMemory.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace Anamnesis.Memory; @@ -22,7 +22,7 @@ public class DrawObjectMemory : SceneObjectMemory [DependsOn(nameof(Flags))] public bool IsVisible { - get => (this.Flags & 0x09) == 0x09; - set => this.Flags = (byte)(value ? this.Flags | 0x09 : this.Flags & ~0x09); + get => (this.Flags & (byte)DrawObjectFlags.ActiveRenderState) == (byte)DrawObjectFlags.ActiveRenderState; + set => this.Flags = (byte)(value ? this.Flags | (byte)DrawObjectFlags.ActiveRenderState : this.Flags & ~(byte)DrawObjectFlags.ActiveRenderState); } } diff --git a/Anamnesis/Memory/GameObjectMemory.cs b/Anamnesis/Memory/GameObjectMemory.cs index 6c6090ad..142088ad 100644 --- a/Anamnesis/Memory/GameObjectMemory.cs +++ b/Anamnesis/Memory/GameObjectMemory.cs @@ -1,4 +1,4 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace Anamnesis.Memory; @@ -12,13 +12,6 @@ namespace Anamnesis.Memory; public class GameObjectMemory : MemoryBase { - public enum RenderModes : uint - { - Draw = 0, - Unload = 2, - Load = 4, - } - [Bind(0x030)] public Utf8String NameBytes { get; set; } [Bind(0x078)] public uint ObjectId { get; set; } [Bind(0x084)] public uint DataId { get; set; } @@ -30,7 +23,7 @@ public enum RenderModes : uint [Bind(0x096)] public byte DistanceFromPlayerY { get; set; } [Bind(0x00C4)] public float Scale { get; set; } [Bind(GameObject.DRAW_OBJECT_OFFSET, BindFlags.Pointer)] public DrawObjectMemory? ModelObject { get; set; } - [Bind(0x0118)] public RenderModes RenderMode { get; set; } + [Bind(GameObject.RENDER_FLAGS_OFFSET)] public RenderModes RenderMode { get; set; } public string Id => $"n{this.NameHash}_d{this.DataId}_o{this.Address}"; public string IdNoAddress => $"n{this.NameHash}_d{this.DataId}"; ////_k{this.ObjectKind}"; diff --git a/Anamnesis/Memory/WeaponMemory.cs b/Anamnesis/Memory/WeaponMemory.cs index a4c6c6d8..02c49c56 100644 --- a/Anamnesis/Memory/WeaponMemory.cs +++ b/Anamnesis/Memory/WeaponMemory.cs @@ -158,12 +158,12 @@ public bool WeaponHidden { if (value) { - this.Model.Flags |= (byte)DrawObjectFlags.Hidden; + this.Model.Flags = (byte)(this.Model.Flags & ~(byte)DrawObjectFlags.Visible); } else { // When unhiding, if weapon scale is set to 0 by the user, reset it to 1 so that the weapon is visible again. - this.Model.Flags = (byte)(this.Model.Flags & ~(byte)DrawObjectFlags.Hidden); + this.Model.Flags |= (byte)DrawObjectFlags.Visible; if (this.Model.Transform != null && this.Model.Transform.Scale == Vector3.Zero) { this.Model.Transform.Scale = Vector3.One; diff --git a/RemoteController/Drivers/Modules/RedrawModule.cs b/RemoteController/Drivers/Modules/RedrawModule.cs index 5b8130dc..da917054 100644 --- a/RemoteController/Drivers/Modules/RedrawModule.cs +++ b/RemoteController/Drivers/Modules/RedrawModule.cs @@ -219,9 +219,9 @@ private unsafe byte[] ExecutePartialRedraw(nint gameObjPtr, RedrawRequest req) { DrawObject* mainHandDrawObj = (DrawObject*)drawData->MainHand.WeaponPtr; if (isMainHandHidden) - mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Visible; else - mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Visible; } drawData->OffHand.IsHidden = isOffHandHidden; @@ -229,9 +229,9 @@ private unsafe byte[] ExecutePartialRedraw(nint gameObjPtr, RedrawRequest req) { DrawObject* offHandDrawObj = (DrawObject*)drawData->OffHand.WeaponPtr; if (isOffHandHidden) - offHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Visible; else - offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + offHandDrawObj->Flags |= (byte)DrawObjectFlags.Visible; } } @@ -267,6 +267,12 @@ private unsafe byte[] ExecuteFullRedraw(nint gameObjPtr) bool isOffHandHidden = drawData->OffHand.IsHidden; this.charDisableDraw.OriginalFunction(gameObjPtr); + + // Clear the game object's render mode flag to allow it to be drawn. + // Otherwise, the subsequent EnableDraw call will re-hide the object. + RenderModes* renderMode = (RenderModes*)(gameObjPtr + GameObjectStruct.RENDER_FLAGS_OFFSET); + *renderMode &= ~RenderModes.DisableDraw; + this.charEnableDraw.OriginalFunction(gameObjPtr); // Re-apply the hide/show flags after the redraw @@ -275,9 +281,9 @@ private unsafe byte[] ExecuteFullRedraw(nint gameObjPtr) { DrawObject* mainHandDrawObj = (DrawObject*)drawData->MainHand.WeaponPtr; if (isMainHandHidden) - mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Visible; else - mainHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + mainHandDrawObj->Flags |= (byte)DrawObjectFlags.Visible; } drawData->OffHand.IsHidden = isOffHandHidden; @@ -285,9 +291,9 @@ private unsafe byte[] ExecuteFullRedraw(nint gameObjPtr) { DrawObject* offHandDrawObj = (DrawObject*)drawData->OffHand.WeaponPtr; if (isOffHandHidden) - offHandDrawObj->Flags |= (byte)DrawObjectFlags.Hidden; + offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Visible; else - offHandDrawObj->Flags &= (byte)~DrawObjectFlags.Hidden; + offHandDrawObj->Flags |= (byte)DrawObjectFlags.Visible; } return [1]; diff --git a/RemoteController/Interop/Types/DrawObject.cs b/RemoteController/Interop/Types/DrawObject.cs index 65940e81..d3449c37 100644 --- a/RemoteController/Interop/Types/DrawObject.cs +++ b/RemoteController/Interop/Types/DrawObject.cs @@ -10,13 +10,13 @@ namespace RemoteController.Interop.Types; public enum DrawObjectFlags : byte { None = 0, - Hidden = 1 << 0, // 0x01: Suppresses DirectX 3D model rendering in CharacterBase::UpdateRender + Visible = 1 << 0, // 0x01: Controls DirectX 3D model rendering in CharacterBase::UpdateRender InWorld = 1 << 1, // 0x02: Object attached to scene world node Loaded = 1 << 3, // 0x08: Model resources initialized Culled = 1 << 6, // 0x40: Frustum / occlusion culled - // Combined mask for checking/setting initialized and visible draw objects (0x09: Loaded | Hidden) - ActiveRenderState = Loaded | Hidden, + // Combined mask for checking/setting initialized and visible draw objects (0x09: Loaded | Visible) + ActiveRenderState = Loaded | Visible, } [StructLayout(LayoutKind.Explicit, Size = 0x090)] diff --git a/RemoteController/Interop/Types/GameObject.cs b/RemoteController/Interop/Types/GameObject.cs index b97bbb97..fb86ba6f 100644 --- a/RemoteController/Interop/Types/GameObject.cs +++ b/RemoteController/Interop/Types/GameObject.cs @@ -1,15 +1,56 @@ -// © Anamnesis. +// © Anamnesis. // Licensed under the MIT license. namespace RemoteController.Interop.Types; +using System; using System.Runtime.InteropServices; +/// +/// A visibility arbitration bitmask for . +/// +/// +/// The game uses this field to determine whether an actor should be rendered or hidden in the scene. +/// Different subsystems (cutscenes, layout streaming, mount/dismount managers, event handlers, etc.) +/// can set individual bits in this field to indicate that the actor should be hidden for the flagged reason. +/// +/// An actor is rendered by the render engine if and only if evaluates to (0), +/// indicating no subsystem is actively requesting the actor to be hidden. +/// +/// +[Flags] +public enum RenderModes : ulong +{ + /// + /// No subsystem is requesting the object to be hidden. The object is drawn normally. + /// + Draw = 0, + + /// + /// Object model resources are unloaded / pending unload. + /// + Unload = 1UL << 1, + + /// + /// Object model resources are actively loading. + /// + Load = 1UL << 2, + + /// + /// Set by GameObject::DisableDraw when tearing down draw state. + /// + DisableDraw = 1UL << 11, +} + [StructLayout(LayoutKind.Explicit, Size = 0x01A0)] public struct GameObject { public const int DRAW_OBJECT_OFFSET = 0x0100; + public const int RENDER_FLAGS_OFFSET = 0x0118; - [FieldOffset(0x100)] + [FieldOffset(DRAW_OBJECT_OFFSET)] public unsafe DrawObject* ModelObject; + + [FieldOffset(RENDER_FLAGS_OFFSET)] + public RenderModes RenderMode; } From 634b4da6d1717d4d3a5d8ff72abade5f0ae09019 Mon Sep 17 00:00:00 2001 From: ergoxiv Date: Sat, 15 Aug 2026 20:33:16 +0200 Subject: [PATCH 11/11] Fixed up comment formatting inconsistencies found during self-review --- Anamnesis/Actor/Views/ItemView.xaml.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Anamnesis/Actor/Views/ItemView.xaml.cs b/Anamnesis/Actor/Views/ItemView.xaml.cs index 9fc22e74..db1cac51 100644 --- a/Anamnesis/Actor/Views/ItemView.xaml.cs +++ b/Anamnesis/Actor/Views/ItemView.xaml.cs @@ -218,7 +218,7 @@ private static void SetModel(IEquipmentItemMemory? itemModel, ushort modelSet, u private static bool MatchesWeapon(IItem item, WeaponMemory weaponVm, ItemSlots slot) { - // For Off-Hand paired weapons, match against the SubModel + // For off-hand paired weapons, match against the submodel if (slot == ItemSlots.OffHand && item.HasSubModel) { return item.SubModelSet == weaponVm.Set @@ -541,7 +541,7 @@ private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs // Do not re-fetch if the selected item is the same as the current item IItem? item = weaponVm.EquippedItem ?? this.Item; - // Check if MainHand has a paired sub-model matching this OffHand + // Check if the main hand already has a paired sub-model matching the off-hand if (item == null && slots == ItemSlots.OffHand && this.Actor.DrawData.MainHand?.EquippedItem is IItem mainItem && MatchesWeapon(mainItem, weaponVm, slots)) { item = mainItem;