Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions DEPENDENCY.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@ Each entry below is `Sharlayan.Resources.Mappers.*Mapper.Build()` computing a by
|---|---|---|
| `ActorItemMapper` | `FFXIV/Client/Game/Object/GameObject.cs` + `FFXIV/Client/Game/Character/Character.cs` + `FFXIV/Client/Game/Character/CharacterData.cs` + `FFXIV/Client/Game/Character/BattleChara.cs` + `FFXIV/Client/Game/Character/CastInfo.cs` + `FFXIV/Common/Math/Vector3.cs` | GameObject `_name`, Character `EntityId` / `OwnerId` / `ObjectKind` / `Rotation` / `HitboxRadius` / `FateId` / `BaseId` / `YalmDistanceFromPlayerX` / `Position` / `Health` / `MaxHealth` / `Mana` / `GatheringPoints` / `MaxGatheringPoints` / `CraftingPoints` / `MaxCraftingPoints` / `TitleId` / `ClassJob` / `Level` / `Icon` / `GMRank` / `TargetId` / `NameId` / `CombatTaggerId` / `TargetableStatus` / `EventId` / `RenderFlags` / `Flags`; BattleChara `CastInfo` / `StatusManager`; CastInfo `IsCasting` / `Interruptible` / `ActionId` / `TargetId` / `CurrentCastTime` / `TotalCastTime` |
| `PartyMemberMapper` | `FFXIV/Client/Game/Group/PartyMember.cs` + `FFXIV/Common/Math/Vector3.cs` | `CurrentHP` / `MaxHP` / `CurrentMP` / `EntityId` / `_name` / `ClassJob` / `Level` / `Position` / `StatusManager` |
| `PlayerInfoMapper` | `FFXIV/Client/Game/UI/PlayerState.cs` | `CurrentClassJobId`, `BaseStrength` / `BaseDexterity` / `BaseVitality` / `BaseIntelligence` / `BaseMind` / `BasePiety`, `_classJobLevels` + `_classJobExperience` (string-name reflection) |
| `PlayerInfoMapper` | `FFXIV/Client/Game/UI/PlayerState.cs` | `CurrentClassJobId`, `BaseStrength` / `BaseDexterity` / `BaseVitality` / `BaseIntelligence` / `BaseMind` / `BasePiety`, `_classJobLevels` + `_classJobExperience` (string-name reflection); `RPR` (ExpIdx 28) and `SGE` (ExpIdx 29) wired via `_classJobLevels` / `_classJobExperience`, same pattern as `VPR`/`PCT` |
| `StatusItemMapper` | `FFXIV/Client/Game/StatusManager.cs` (nested `Status` struct) | `StatusId` / `Param` / `RemainingTime` / `SourceObject` |
| `InventoryItemMapper` | `FFXIV/Client/Game/InventoryItem.cs` | `Slot` / `ItemId` / `Quantity` / `SpiritbondOrCollectability` / `Condition` / `Flags` / `_materia` / `_materiaGrades` / `_stains` / `GlamourId` |
| `InventoryContainerMapper` | `FFXIV/Client/Game/InventoryContainer.cs` | `Type` / `Size` |
| `InventoryContainerMapper` | `FFXIV/Client/Game/InventoryContainer.cs` | `Type` / `Size` / `SourceSize` = `Marshal.SizeOf<InventoryContainer>()` (expected 32 / 0x20) |
| `HotBarItemMapper` | `FFXIV/Client/UI/Misc/RaptureHotbarModule.HotbarSlot.cs` + `FFXIV/Client/System/String/Utf8String.cs` | `HotbarSlot.CommandId` / `PopUpHelp` / `_popUpKeybindHint`; `Utf8String._inlineBuffer` |
| `RecastItemMapper` | `FFXIV/Client/UI/Arrays/Common/ActionBarSlotNumberArray.cs` | `ActionType` / `ActionId` / `IconId` / `Executable` / `GlobalCoolDownPercentage` / `CurrentCharges` / `Glows` / `ManaCost` / `InRange` |
| `TargetInfoMapper` | `FFXIV/Client/Game/Control/TargetSystem.cs` + `FFXIV/Client/Game/Character/Character.cs` | `Target` / `TargetObjectId` / `MouseOverTarget` / `FocusTarget` / `PreviousTarget`; `sizeof(Character)` |
Expand Down
111 changes: 84 additions & 27 deletions Sharlayan.Harness/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ void Log(string line) {
return 1;
}
Process game = candidates[0];
for (int i = 1; i < candidates.Length; i++) {
candidates[i].Dispose();
}
Log($" ✓ {ProcessName} PID {game.Id}");
try {
Log($" ✓ MainModule : {game.MainModule?.FileName}");
Expand Down Expand Up @@ -329,12 +332,19 @@ void Probe(string label, uint? id) {
// Live refresh — the one-shot report above is the snapshot for the file;
// this loop lets the user watch eye-check values change in-game. Skipped
// when --once is passed (CI / single-shot usage).
if (runOnce || directHandler == null) {
try {
if (runOnce || directHandler == null) {
return 0;
}

await RunLiveRefreshLoop(directHandler, liveIntervalMs);
return 0;
}

await RunLiveRefreshLoop(directHandler, liveIntervalMs);
return 0;
finally {
if (directHandler != null) {
SharlayanMemoryManager.Instance.RemoveHandler(directHandler.Configuration.ProcessModel.ProcessID);
}
}
}

private static async Task RunLiveRefreshLoop(MemoryHandler handler, int intervalMs) {
Expand Down Expand Up @@ -518,38 +528,84 @@ void DumpTarget(string label, Sharlayan.Core.ActorItem? t) {
log($" ✗ EyeCheck: {ex.GetType().Name}: {ex.Message}");
}

// [3c.1] STATUS LOCALIZATION — prints the local player's currently-active status
// entries showing both StatusName (localized per Configuration.GameLanguage) and the
// new StatusNameEnglish (always English regardless of language). ActorItem.StatusItems
// can hold stale slots (expired Duration=0 entries, or slots whose StatusID is out of
// the XIVDatabase range and resolves to "???"); we skip both so the display only
// shows what's actually applied right now.
// [3c.1] STATUS EFFECTS — prints currently-active status entries for the local
// player, the current target, and party members. Shows both StatusName (localized
// per Configuration.GameLanguage) and StatusNameEnglish, plus the category fields
// sourced from Lumina's Status sheet: StatusCategory (raw byte), IsBeneficial /
// IsDetrimental (derived), and CanDispel (Esuna-able). The target dump is the
// easiest place to eye-check IsDetrimental — apply a DoT to a striking dummy and
// it should show [D]; food/self-buffs on the player should show [B].
// ActorItem.StatusItems can hold stale slots (expired Duration=0 entries, or slots
// whose StatusID is out of the XIVDatabase range and resolves to "???"); we skip
// both so the display only shows what's actually applied right now.
try {
var dpStatus = handler.Reader.GetCurrentPlayer()?.Entity;
if (dpStatus != null) {
log(string.Empty);
log($" [3c.1] LOCAL PLAYER STATUSES (GameLanguage={handler.Configuration.GameLanguage})");
// Shared per-entry dump. Returns lines printed so callers can show an
// "(empty)" placeholder. Keeps local-player / target / party output identical.
int DumpStatuses(string label, IEnumerable<Sharlayan.Core.StatusItem>? statuses, int cap = 8) {
log($" {label}:");
int shown = 0;
if (dpStatus.StatusItems != null) {
foreach (var s in dpStatus.StatusItems) {
if (statuses != null) {
foreach (var s in statuses) {
if (!s.IsValid()) continue;
// Hard filters: expired slots (Duration <= 0) and IDs the XIVDatabase
// didn't resolve (StatusNameEnglish == "???") are residual / garbage.
if (s.Duration <= 0f) continue;
if (string.IsNullOrEmpty(s.StatusNameEnglish) || s.StatusNameEnglish == Sharlayan.Constants.UNKNOWN_LOCALIZED_NAME) continue;
if (shown++ >= 6) break;
string en = s.StatusNameEnglish;
string loc = s.StatusName ?? "(null)";
log($" [{s.StatusID,4}] StatusName=\"{loc}\" StatusNameEnglish=\"{en}\" Stacks={s.Stacks} Duration={s.Duration:F1}s");
// Exception: permanent statuses (tank stances, chocobo buffs) read
// Duration <= 0 but carry a resolved name — keep those.
bool resolved = !string.IsNullOrEmpty(s.StatusNameEnglish) && s.StatusNameEnglish != Sharlayan.Constants.UNKNOWN_LOCALIZED_NAME;
if (!resolved) continue;
if (shown++ >= cap) break;
// B = beneficial (StatusCategory 1), D = detrimental (2), - = neither (0).
string cat = s.IsBeneficial ? "B" : s.IsDetrimental ? "D" : "-";
log($" [{s.StatusID,4}] [{cat}] \"{s.StatusName}\" (en \"{s.StatusNameEnglish}\") StatusCategory={s.StatusCategory} IsBeneficial={s.IsBeneficial} IsDetrimental={s.IsDetrimental} CanDispel={s.CanDispel} Stacks={s.Stacks} Duration={s.Duration:F1}s");
}
}
if (shown == 0) {
log(" (no active statuses on local player)");
log(" (none active)");
}
return shown;
}

log(string.Empty);
log($" [3c.1] STATUS EFFECTS (GameLanguage={handler.Configuration.GameLanguage}) [B]=beneficial [D]=detrimental [-]=neither");

var dpStatus = handler.Reader.GetCurrentPlayer()?.Entity;
DumpStatuses($"LocalPlayer \"{dpStatus?.Name}\"", dpStatus?.StatusItems);

// Current target — debuffs the player applies (DoTs, Vulnerability Up, ...)
// land here, so this is where IsDetrimental / CanDispel actually light up.
try {
var targetStatus = handler.Reader.GetTargetInfo()?.TargetInfo?.CurrentTarget;
if (targetStatus != null) {
DumpStatuses($"CurrentTarget \"{targetStatus.Name}\"", targetStatus.StatusItems);
}
else {
log(" CurrentTarget: (no target)");
}
}
catch (Exception ex) {
log($" ✗ Target statuses: {ex.GetType().Name}: {ex.Message}");
}

// Party members (excluding the local player, who is also in the party list).
try {
if (handler.Reader.CanGetPartyMembers()) {
var party = handler.Reader.GetPartyMembers()?.PartyMembers;
if (party != null) {
int dumped = 0;
foreach (var (id, member) in party) {
if (member == null || (dpStatus != null && id == dpStatus.ID)) continue;
if (dumped++ >= 3) break; // cap — light-party-sized sample is enough for an eye-check
DumpStatuses($"Party \"{member.Name}\"", member.StatusItems, cap: 4);
}
}
}
}
catch (Exception ex) {
log($" ✗ Party statuses: {ex.GetType().Name}: {ex.Message}");
}
}
catch (Exception ex) {
log($" ✗ StatusLocalization: {ex.GetType().Name}: {ex.Message}");
log($" ✗ StatusEffects: {ex.GetType().Name}: {ex.Message}");
}

// [3c.3] ENMITY TABLES — surfaces both enmity-related signatures in one section so
Expand Down Expand Up @@ -765,10 +821,11 @@ public Action<string> Begin() {
}

public void End() {
for (int i = _thisFrameLineCount; i < _previousLineCount; i++) {
WriteLine(string.Empty);
int contentLines = this._thisFrameLineCount;
for (int i = contentLines; i < this._previousLineCount; i++) {
this.WriteLine(string.Empty);
}
_previousLineCount = Math.Max(_previousLineCount, _thisFrameLineCount);
this._previousLineCount = contentLines;
Console.CursorVisible = true;
}

Expand Down
54 changes: 54 additions & 0 deletions Sharlayan.Tests/Core/StatusItemTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="StatusItemTests.cs" company="SyndicatedLife">
// Copyright© 2007 - 2026 Ryan Wilson <syndicated.life@gmail.com> (https://syndicated.life/)
// Licensed under the MIT license. See LICENSE.md in the solution root for full license information.
// </copyright>
// <summary>
// Pins the StatusCategory → IsBeneficial / IsDetrimental mapping on Core.StatusItem.
// The 1 = beneficial / 2 = detrimental values come from the game's Status Excel sheet
// (the same discriminator the in-game UI uses for the green/red icon border); these
// tests stop a refactor from silently swapping or shifting them.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Sharlayan.Tests.Core {
using Sharlayan.Core;

using Xunit;

public class StatusItemTests {
[Fact]
public void StatusCategory1_IsBeneficial_NotDetrimental() {
StatusItem item = new StatusItem { StatusCategory = 1 };
Assert.True(item.IsBeneficial);
Assert.False(item.IsDetrimental);
}

[Fact]
public void StatusCategory2_IsDetrimental_NotBeneficial() {
StatusItem item = new StatusItem { StatusCategory = 2 };
Assert.True(item.IsDetrimental);
Assert.False(item.IsBeneficial);
}

[Fact]
public void StatusCategory0_IsNeither() {
// 0 = system statuses, or a status id that didn't resolve against
// XIVDatabase (StatusEffectLookup's DefaultStatusInfo leaves it 0).
StatusItem item = new StatusItem();
Assert.False(item.IsBeneficial);
Assert.False(item.IsDetrimental);
}

[Theory]
[InlineData(3)]
[InlineData(byte.MaxValue)]
public void StatusCategory_UnknownValues_AreNeither(byte category) {
// Future-proofing: if the game ever adds category 3+, neither flag
// should light up until the mapping is deliberately extended.
StatusItem item = new StatusItem { StatusCategory = category };
Assert.False(item.IsBeneficial);
Assert.False(item.IsDetrimental);
}
}
}
7 changes: 4 additions & 3 deletions Sharlayan.Tests/Resources/Mappers/RecastItemMapperTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace Sharlayan.Tests.Resources.Mappers {
using System.Runtime.InteropServices;

using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
using FFXIVClientStructs.FFXIV.Client.UI.Arrays.Common;

using Sharlayan.Models.Structures;
Expand Down Expand Up @@ -91,10 +92,10 @@ public void Build_ItemSize_MatchesActionBarSlotNumberArraySize() {

[Fact]
public void Build_ContainerSize_MatchesActionBarBarNumberArraySize() {
// Each ActionBarBarNumberArray is 272 × 4 bytes (12 slots × 68 + other bar state).
// RECAST_KEY + type × ContainerSize must land at bars[type]._slots base.
// ContainerSize must equal the FCS struct size so RECAST_KEY + type × ContainerSize
// lands at bars[type]._slots base. Derived from FCS directly so layout drift fails.
RecastItem item = RecastItemMapper.Build();
Assert.Equal(272 * sizeof(int), item.ContainerSize);
Assert.Equal(Marshal.SizeOf<ActionBarNumberArray.ActionBarBarNumberArray>(), item.ContainerSize);
}

[Fact]
Expand Down
16 changes: 16 additions & 0 deletions Sharlayan.Tests/Resources/Providers/FCSDependencyIntegrityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ namespace Sharlayan.Tests.Resources.Providers {
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
using FFXIVClientStructs.FFXIV.Client.System.Framework;

using NativeInventoryContainer = FFXIVClientStructs.FFXIV.Client.Game.InventoryContainer;
using FFXIVClientStructs.FFXIV.Client.System.String;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
Expand Down Expand Up @@ -283,6 +285,20 @@ public void AllMappers_Build_DoesNotThrow() {
_ = ChatLogPointersMapper.Build();
}

// ------------------------------------------------------------------------------
// InventoryContainer stride — the mapper's SourceSize must equal the FCS struct
// size so Reader.Inventory uses the correct per-container byte stride.
// ------------------------------------------------------------------------------

[Fact]
public void InventoryContainerMapper_SourceSize_MatchesFCSStructSize() {
// InventoryContainer is [StructLayout(Size = 0x20)] — 32 bytes.
// If FCS ever resizes the struct the mapper must be updated to match.
int expected = Marshal.SizeOf<NativeInventoryContainer>();
Assert.Equal(32, expected); // document the current known-good value
Assert.Equal(expected, InventoryContainerMapper.Build().SourceSize);
}

// ------------------------------------------------------------------------------
// Helpers
// ------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions Sharlayan/Core/ActorItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ public ActorItem Clone() {
Stacks = statusItem.Stacks,
StatusID = statusItem.StatusID,
StatusName = statusItem.StatusName,
StatusNameEnglish = statusItem.StatusNameEnglish,
TargetName = statusItem.TargetName,
});
}
Expand Down
4 changes: 2 additions & 2 deletions Sharlayan/Core/ActorItemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ public float GetDistanceTo(ActorItem compare) {
float distanceX = (float) Math.Abs(compare.X - this.X);
float distanceY = (float) Math.Abs(compare.Y - this.Y);
float distanceZ = (float) Math.Abs(compare.Z - this.Z);
return (float) Math.Sqrt(Math.Pow(distanceX, 2) + Math.Pow(distanceY, 2) + Math.Pow(distanceZ, 2));
return (float) Math.Sqrt(distanceX * distanceX + distanceY * distanceY + distanceZ * distanceZ);
}

public float GetHorizontalDistanceTo(ActorItem compare) {
float distanceX = (float) Math.Abs(compare.X - this.X);
float distanceY = (float) Math.Abs(compare.Y - this.Y);
return (float) Math.Sqrt(Math.Pow(distanceX, 2) + Math.Pow(distanceY, 2));
return (float) Math.Sqrt(distanceX * distanceX + distanceY * distanceY);
}

private double safeDivide(double a, double b) {
Expand Down
11 changes: 8 additions & 3 deletions Sharlayan/Core/Coordinate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,20 @@ public float AngleTo(Coordinate b) {
}

public float Distance2D(Coordinate coordinate) {
return (float) Math.Sqrt(Math.Pow(this.X - coordinate.X, 2) + Math.Pow(this.Y - coordinate.Y, 2));
double dx = this.X - coordinate.X;
double dy = this.Y - coordinate.Y;
return (float) Math.Sqrt(dx * dx + dy * dy);
}

public float DistanceTo(Coordinate coordinate) {
return (float) Math.Sqrt(Math.Pow(this.X - coordinate.X, 2) + Math.Pow(this.Y - coordinate.Y, 2) + Math.Pow(this.Z - coordinate.Z, 2));
double dx = this.X - coordinate.X;
double dy = this.Y - coordinate.Y;
double dz = this.Z - coordinate.Z;
return (float) Math.Sqrt(dx * dx + dy * dy + dz * dz);
}

public Coordinate Normalize() {
float length = (float) Math.Sqrt(Math.Pow(this.X, 2) + Math.Pow(this.Y, 2) + Math.Pow(this.Z, 2));
float length = (float) Math.Sqrt(this.X * this.X + this.Y * this.Y + this.Z * this.Z);
return new Coordinate {
X = this.X / length,
Y = this.Y / length,
Expand Down
2 changes: 1 addition & 1 deletion Sharlayan/Core/Enums/Inventory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ public enum Container : uint {
//Unknown27008 = 27008,
}

public enum MateriaType : byte {
public enum MateriaType : ushort {
None = 0,

Cracked,
Expand Down
8 changes: 8 additions & 0 deletions Sharlayan/Core/Interfaces/IPlayerInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,10 +186,18 @@ public interface IPlayerInfo {

int ROG_CurrentEXP { get; set; }

byte RPR { get; set; }

int RPR_CurrentEXP { get; set; }

byte SAM { get; set; }

int SAM_CurrentEXP { get; set; }

byte SGE { get; set; }

int SGE_CurrentEXP { get; set; }

short SkillSpeed { get; set; }

short SlashingResistance { get; set; }
Expand Down
Loading
Loading