Skip to content

Commit

Permalink
Merge pull request ngld#207 from aMehGit-feature/combatantExpansion
Browse files Browse the repository at this point in the history
  • Loading branch information
ngld committed Nov 19, 2021
2 parents 99618cf + 8e8645e commit e947672
Show file tree
Hide file tree
Showing 7 changed files with 194 additions and 4 deletions.
33 changes: 30 additions & 3 deletions OverlayPlugin.Core/EventSources/EnmityEventSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ public class EnmityEventSource : EventSourceBase
private const string EnmityTargetDataEvent = "EnmityTargetData";
// All of the mobs with aggro on the player. Equivalent of the sidebar aggro list in game.
private const string EnmityAggroListEvent = "EnmityAggroList";
// TargetableEnemies
private const string TargetableEnemiesEvent = "TargetableEnemies";
// State of combat, both act and game.
private const string InCombatEvent = "InCombat";

Expand Down Expand Up @@ -58,7 +60,7 @@ public EnmityEventSource(TinyIoCContainer container) : base(container)
}

RegisterEventTypes(new List<string> {
EnmityTargetDataEvent, EnmityAggroListEvent,
EnmityTargetDataEvent, EnmityAggroListEvent, TargetableEnemiesEvent
});
RegisterCachedEventType(InCombatEvent);
}
Expand Down Expand Up @@ -190,7 +192,8 @@ protected override void Update()

bool targetData = HasSubscriber(EnmityTargetDataEvent);
bool aggroList = HasSubscriber(EnmityAggroListEvent);
if (!targetData && !aggroList)
bool targetableEnemies = HasSubscriber(TargetableEnemiesEvent);
if (!targetData && !aggroList && !targetableEnemies)
return;

var combatants = memory.GetCombatantList();
Expand All @@ -204,7 +207,10 @@ protected override void Update()
{
this.DispatchEvent(CreateAggroList(combatants));
}

if (targetableEnemies)
{
this.DispatchEvent(CreateTargetableEnemyList(combatants));
}
#if TRACE
Log(LogLevel.Trace, "UpdateEnmity: {0}ms", stopwatch.ElapsedMilliseconds);
#endif
Expand Down Expand Up @@ -233,6 +239,13 @@ internal class EnmityAggroListObject
public List<AggroEntry> AggroList;
}

[Serializable]
internal class TargetableEnemiesObject
{
public string type = TargetableEnemiesEvent;
public List<TargetableEnemyEntry> TargetableEnemyList;
}

internal JObject CreateTargetData(List<Combatant> combatants)
{
EnmityTargetDataObject enmity = new EnmityTargetDataObject();
Expand Down Expand Up @@ -289,6 +302,20 @@ internal JObject CreateAggroList(List<Combatant> combatants)
}
return JObject.FromObject(enmity);
}

internal JObject CreateTargetableEnemyList(List<Combatant> combatants)
{
TargetableEnemiesObject enemies = new TargetableEnemiesObject();
try
{
enemies.TargetableEnemyList = memory.GetTargetableEnemyList(combatants);
}
catch (Exception ex)
{
this.logger.Log(LogLevel.Error, "CreateAggroList: {0}", ex);
}
return JObject.FromObject(enemies);
}
}

public class CombatStatusChangedArgs : EventArgs
Expand Down
6 changes: 6 additions & 0 deletions OverlayPlugin.Core/MemoryProcessors/EnmityMemory50.cs
Original file line number Diff line number Diff line change
Expand Up @@ -521,6 +521,12 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
return result;
}

// Satisfying abstract method.
public override List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList)
{
return new List<TargetableEnemyEntry>();
}

public unsafe List<EffectEntry> GetEffectEntries(byte* source, ObjectType type, uint mycharID)
{
var result = new List<EffectEntry>();
Expand Down
6 changes: 6 additions & 0 deletions OverlayPlugin.Core/MemoryProcessors/EnmityMemory52.cs
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,12 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
return result;
}

// Satisfying abstract method.
public override List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList)
{
return new List<TargetableEnemyEntry>();
}

public unsafe List<EffectEntry> GetEffectEntries(byte* source, ObjectType type, uint mycharID)
{
var result = new List<EffectEntry>();
Expand Down
6 changes: 6 additions & 0 deletions OverlayPlugin.Core/MemoryProcessors/EnmityMemory53.cs
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,12 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
return result;
}

// Satisfying abstract method.
public override List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList)
{
return new List<TargetableEnemyEntry>();
}

public unsafe List<EffectEntry> GetEffectEntries(byte* source, ObjectType type, uint mycharID)
{
var result = new List<EffectEntry>();
Expand Down
6 changes: 6 additions & 0 deletions OverlayPlugin.Core/MemoryProcessors/EnmityMemory54.cs
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,12 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
return result;
}

// Satisfying abstract method.
public override List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList)
{
return new List<TargetableEnemyEntry>();
}

public unsafe List<EffectEntry> GetEffectEntries(byte* source, ObjectType type, uint mycharID)
{
var result = new List<EffectEntry>();
Expand Down
45 changes: 44 additions & 1 deletion OverlayPlugin.Core/MemoryProcessors/EnmityMemory55.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class EnmityMemory55 : EnmityMemory
private IntPtr inCombatAddress = IntPtr.Zero;

private const string charmapSignature = "48c1ea0381faa7010000????8bc2488d0d";
private const string targetSignature = "83E901740832C04883C4205BC3488D0D";
private const string targetSignature = "83E901740832C04883C4205BC3488D0D";
private const string enmitySignature = "83f9ff7412448b048e8bd3488d0d";
private const string inCombatSignature = "84c07425450fb6c7488d0d";

Expand Down Expand Up @@ -303,6 +303,18 @@ public unsafe struct CombatantMemory
[FieldOffset(0x8C)]
public byte Type;

[FieldOffset(0x1980)]
public byte MonsterType;

[FieldOffset(0x94)]
public byte Status;

[FieldOffset(0x104)]
public int ModelStatus;

[FieldOffset(0x19A0)]
public byte AggressionStatus;

[FieldOffset(0x92)]
public byte EffectiveDistance;

Expand Down Expand Up @@ -372,6 +384,11 @@ public unsafe Combatant GetCombatantFromByteArray(byte[] source, uint mycharID,
ID = mem.ID,
OwnerID = mem.OwnerID == emptyID ? 0 : mem.OwnerID,
Type = (ObjectType)mem.Type,
MonsterType = (MonsterType)mem.MonsterType,
Status = (ObjectStatus)mem.Status,
ModelStatus = (ModelStatus)mem.ModelStatus,
// Normalize all possible aggression statuses into the basic 4 ones.
AggressionStatus = (AggressionStatus)(mem.AggressionStatus - (mem.AggressionStatus / 4) * 4),
EffectiveDistance = mem.EffectiveDistance,
PosX = mem.PosX,
PosY = mem.PosY,
Expand All @@ -382,6 +399,9 @@ public unsafe Combatant GetCombatantFromByteArray(byte[] source, uint mycharID,
MaxHP = mem.MaxHP,
Effects = exceptEffects ? new List<EffectEntry>() : GetEffectEntries(mem.Effects, (ObjectType)mem.Type, mycharID),
};
combatant.IsTargetable =
(combatant.ModelStatus == ModelStatus.Visible)
&& ((combatant.Status == ObjectStatus.NormalActorStatus) || (combatant.Status == ObjectStatus.NormalSubActorStatus));
if (combatant.Type != ObjectType.PC && combatant.Type != ObjectType.Monster)
{
// Other types have garbage memory for hp.
Expand Down Expand Up @@ -552,6 +572,7 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
// This is likely because we're reading the memory for the aggro sidebar.
HateRate = (int)e.Enmity,
isCurrentTarget = (e.ID == currentTargetID),
IsTargetable = c.IsTargetable,
Name = c.Name,
MaxHP = c.MaxHP,
CurrentHP = c.CurrentHP,
Expand Down Expand Up @@ -583,6 +604,28 @@ public override unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantLi
return result;
}

public override List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList)
{
var enemyList = new List<TargetableEnemyEntry>();
for (int i = 0; i != combatantList.Count; ++i)
{
var combatant = combatantList[i];
bool isHostile = (combatant.Type == ObjectType.Monster) && (combatant.MonsterType == MonsterType.Hostile);
if (!isHostile || !combatant.IsTargetable) continue;
var entry = new TargetableEnemyEntry
{
ID = combatant.ID,
Name = combatant.Name,
CurrentHP = combatant.CurrentHP,
MaxHP = combatant.CurrentHP,
IsEngaged = (combatant.AggressionStatus >= AggressionStatus.EngagedPassive),
EffectiveDistance = combatant.EffectiveDistance
};
enemyList.Add(entry);
}
return enemyList;
}

public unsafe List<EffectEntry> GetEffectEntries(byte* source, ObjectType type, uint mycharID)
{
var result = new List<EffectEntry>();
Expand Down
96 changes: 96 additions & 0 deletions OverlayPlugin.Core/MemoryProcessors/EnmityMemoryCommon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,96 @@ public enum ObjectType : byte
Minion = 0x09
}

/// <summary>A byte offset by 0x1980 from the combatant's address that further describes the combatant if their ObjectType is a Monster.</summary>
// Basically, pets, combat NPCs (ones that attack), and monsters all share ObjectType 2 (Monster), this field distinguishes the monsters from those two.
public enum MonsterType : byte
{
Friendly = 0,
Hostile = 4
}

/// <summary>An unsigned byte offset by 0x94 from the combatant's address that describes its status.</summary>
public enum ObjectStatus : byte
{
/// <summary>Indicates that a targetable PC, NPC, monster, minion, or object is at an effective distance of 97 or less from the player.</summary>
// A dead PC keeps status 191.
NormalActorStatus = 191,

/// <summary>Indicates that a targetable pet, chocobo, or sometimes a monster belonging to another one (add) is in the vicinity.</summary>
// It hasn't been tested, but it probably works off the 97 effective distance as well.
NormalSubActorStatus = 190,

/// <summary>Indicates that a previously targetable actor or sub-actor is now untargetable.</summary>
// It's the same as log line 34 toggling the nameplate off.
TemporarilyUntargetable = 189,

/// <summary>Indicates a dead actor or sub-actor, or one that loads/spawns into the game while being untargetable.</summary>
// This includes the invisible "helper" boss actors in instances.
// When an enemy's HP reaches 0, their nameplates grays out and then disappears. The status is only updated to 188 after the nameplate's gone.
LoadsUntargetable = 188

/* More statuses do exist, and a lot of them are not fully tested or understood:
* Status 47 indicates that an actor is updating from status 191 to 175.
* Status 175 indicates that an actor is at an effective distance of between 98 and 109 (inclusive).
* When a combatant just loads in and is 98 or more yalms away from the player, the status is set to 171, which could signify that
* the combatant is pending an update on its status.
* Watching an unskippable cutscene sets the status to 255. (needs confirmation still though)
* Watching a skippable cuscene from the inn sets the status to 111. Fun fact: Manually setting the status to 111 creates a clone with a T-pose.
* And more.
*/
}

/// <summary>An int offset by 0x104 from the combatant's address that describes the 3D model visibility.</summary>
// In a fight like O1N, where the main boss (Alte Roite) momentarily disappears and appears at the edge of the map to do his knockback,
// the ModelStatus gets set to 16384 (Hidden), but the ObjectStatus remains at 191. The fact that the ObjectStatus doesn't change to 189 could mean that
// the boss is still accepting damage in this invisibility period, and that no ghosting will occur. Checking the logs shows that
// there is indeed no presence of log line 34 that toggles the targetability.
// Doing Titan (Extreme) confirms this behavior seen in O1N:
// When Titan's model disappears from the jumps, the field gets set to 16384 (Hidden). When the model reappears, the field gets set back to 0 (Visible).
// And when Titan's Heart spawns, Titan's ModelStatus remains at 0 (while his ObjectStatus remains at 189).
// In TEA, all bosses load from the start with ModelStatus 16384 (except for Living Liquid of course), manually changing the values to 0 makes the bosses
// appear on the map in an immune state, although it's finicky to make that happen consistently.
public enum ModelStatus : int
{
Visible = 0,
/// <summary>Indicates that the combatant's model has unloaded from the game's memory, be it from a death or a wipe.</summary>
Unloaded = 2048,
Hidden = 16384,

// There are other statuses as well:
// Manually setting the status 2306 (or 258) hides the model instantly.
// Manually setting the status to 4 hides the model but still allows you to move.
// And a few others, but monsters consistently use 0, 2048, and 16384.
}

/// <summary>A byte offset by 0x19A0 from the combatant's address that describes the combatant's aggression status.</summary>
// It's not exactly only about the aggression status, it's that and other things embedded into it:
// Sheathing the weapon adds 4 to the status.
// Being mid-cast adds 128 to the status.
public enum AggressionStatus : byte
{
/// <summary>Indicates a combatant that doesn't aggro on sight and is out of combat.</summary>
Passive = 0,
/// <summary>Indicates a combatant that aggros on sight and is out of combat.</summary>
Aggressive = 1,
/// <summary>Indicates a combatant that doesn't aggro on sight and is in combat.</summary>
EngagedPassive = 2,
/// <summary>Indicates a combatant that aggros on sight and is in combat.</summary>
EngagedAggressive = 3
}

[Serializable]
public class Combatant
{
public uint ID;
public uint OwnerID;
public ObjectType Type;
public MonsterType MonsterType;
public ObjectStatus Status;
public ModelStatus ModelStatus;
public AggressionStatus AggressionStatus;
public uint TargetID;
public bool IsTargetable;

public byte Job;
public string Name;
Expand Down Expand Up @@ -71,6 +154,7 @@ public class AggroEntry
public int HateRate;
public int Order;
public bool isCurrentTarget;
public bool IsTargetable;

public int CurrentHP;
public int MaxHP;
Expand All @@ -92,6 +176,17 @@ public class EffectEntry
public bool isOwner;
}

[Serializable]
public class TargetableEnemyEntry
{
public uint ID;
public string Name;
public int CurrentHP;
public int MaxHP;
public bool IsEngaged;
public byte EffectiveDistance;
}

public abstract class EnmityMemory
{
abstract public bool IsValid();
Expand All @@ -102,6 +197,7 @@ public abstract class EnmityMemory
abstract public List<Combatant> GetCombatantList();
abstract public List<EnmityEntry> GetEnmityEntryList(List<Combatant> combatantList);
abstract public unsafe List<AggroEntry> GetAggroList(List<Combatant> combatantList);
abstract public List<TargetableEnemyEntry> GetTargetableEnemyList(List<Combatant> combatantList);
abstract public bool GetInCombat();
}
}

0 comments on commit e947672

Please sign in to comment.