Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ internal class P2_Missing_1238_4567_KT_Strat : SplatoonScript
{
#region Metadata

public override Metadata? Metadata => new(6, "mirage");
public override Metadata? Metadata => new(7, "mirage");
public override HashSet<uint>? ValidTerritories => [TerritoryDmad];

#endregion
Expand Down Expand Up @@ -193,6 +193,21 @@ private static readonly (string Label, Vector3 Position)[] TowerSlots =
private InterludeNavPhase _interludeNavPhase;
private bool _interludeBossCastSeen;

// Priority index cache (rebuilt only when the priority list order changes).
private Dictionary<string, int>? _priorityIndexCache;

// Ordered party cache (rebuilt only when party membership or priority order changes).
private List<IPlayerCharacter>? _orderedPartyCache;
private bool _orderedPartyCacheValid;
private int _orderedPartyMembershipHash;
private int _orderedPartyPriorityHash;

// Live role result cache (recomputed only when role inputs change).
private bool _liveRoleResultValid;
private int _liveRoleInputHash;
private int _liveRolePatternIdCache;
private string? _liveRoleLabelCache;

#endregion

#region Types
Expand Down Expand Up @@ -479,6 +494,8 @@ public override void OnMapEffect(uint position, ushort data1, ushort data2)

public override void OnSettingsDraw()
{
_priorityIndexCache = null;

if(ImGui.BeginTabBar("##P212384567KTSettings"))
{
if(ImGui.BeginTabItem("Main###tabMain"))
Expand Down Expand Up @@ -1373,6 +1390,7 @@ private void ResetState()
_step4AutoMarkDueAt = 0;
_interludeNavPhase = InterludeNavPhase.None;
_interludeBossCastSeen = false;
InvalidateRoleCaches();

if(Controller.TryGetElementByName(ElActiveTower0, out var tower0))
tower0.Enabled = false;
Expand All @@ -1384,6 +1402,20 @@ private void ResetState()
DisableMyRoleMarker();
}

// Clear priority, ordered party, and live role caches.
private void InvalidateRoleCaches()
{
_priorityIndexCache = null;
_orderedPartyCache = null;
_orderedPartyCacheValid = false;
_orderedPartyMembershipHash = 0;
_orderedPartyPriorityHash = 0;
_liveRoleResultValid = false;
_liveRoleInputHash = 0;
_liveRolePatternIdCache = -1;
_liveRoleLabelCache = null;
}

private void DisableAllMarkers()
{
if(Controller.TryGetElementByName(ElActiveTower0, out var tower0))
Expand Down Expand Up @@ -1742,37 +1774,77 @@ private void DrawPartyDebuffStatus()
ImGui.TextUnformatted($"Party debuffs: waiting ({assigned}/{PartyPlayerCount})");
}

// Party members ordered by priority list (cached until membership/priority changes).
private List<IPlayerCharacter> GetOrderedPartyPlayers()
{
C.EnsureDefaults();
var priority = C.PriorityData.GetPlayers(_ => true);
if(priority == null || priority.Count != PartyPlayerCount)
{
_orderedPartyCacheValid = false;
return [];
}

var priorityHash = 17;
foreach(var player in priority)
priorityHash = priorityHash * 31 + StringComparer.Ordinal.GetHashCode(player.Name);
if(priorityHash != _orderedPartyPriorityHash)
_priorityIndexCache = null;

var members = Controller.GetPartyMembers().ToList();
var membershipHash = ComputeMembershipHash(members);

if(_orderedPartyCacheValid && _orderedPartyCache != null
&& membershipHash == _orderedPartyMembershipHash && priorityHash == _orderedPartyPriorityHash)
return _orderedPartyCache;

var names = priority.Select(x => x.Name).ToHashSet(StringComparer.Ordinal);
var members = Controller.GetPartyMembers()
.Where(p => names.Contains(p.Name.ToString()))
.ToList();
if(members.Count != PartyPlayerCount)
var filtered = members.Where(p => names.Contains(p.Name.ToString())).ToList();
if(filtered.Count != PartyPlayerCount)
{
_orderedPartyCacheValid = false;
return [];
}

return OrderByPriority(members).ToList();
_orderedPartyCache = OrderByPriority(filtered).ToList();
_orderedPartyMembershipHash = membershipHash;
_orderedPartyPriorityHash = priorityHash;
_orderedPartyCacheValid = true;
return _orderedPartyCache;
}

// Order-independent hash of party member identities for cache invalidation.
private static int ComputeMembershipHash(List<IPlayerCharacter> members)
{
var hash = members.Count;
foreach(var member in members)
hash ^= (int)member.EntityId;
return hash;
}

// Priority list index for a player from the cached index map.
private int GetPriorityIndex(IPlayerCharacter player)
=> GetPriorityIndexMap().TryGetValue(player.Name.ToString(), out var index) ? index : int.MaxValue;

// Cached name->priority index map, built once until priority order changes.
private Dictionary<string, int> GetPriorityIndexMap()
{
var priorityList = C.PriorityData.GetPlayers(_ => true);
if(priorityList == null)
return int.MaxValue;
if(_priorityIndexCache != null)
return _priorityIndexCache;

var name = player.Name.ToString();
for(var i = 0; i < priorityList.Count; i++)
var map = new Dictionary<string, int>(StringComparer.Ordinal);
var priorityList = C.PriorityData.GetPlayers(_ => true);
if(priorityList != null)
{
if(priorityList[i].Name == name)
return i;
for(var i = 0; i < priorityList.Count; i++)
{
if(!map.ContainsKey(priorityList[i].Name))
map[priorityList[i].Name] = i;
}
}

return int.MaxValue;
_priorityIndexCache = map;
return map;
}

private IEnumerable<PlayerInfo> OrderInfosByPriority(IEnumerable<PlayerInfo> infos)
Expand Down Expand Up @@ -2496,6 +2568,52 @@ private bool TryUpdateLiveRoles(out int patternId, out string roleLabel)
_initialGroupResolved = true;
}

// Marker-based role resolution reads live in-game head markers, so its result is never cached.
var useMarkerResolve = (C.SpreadUseMarker && C.SpreadMarkerType != MarkerResolveKind.None)
|| (C.ConeUseMarker && C.ConeMarkerType != MarkerResolveKind.None);
var inputHash = ComputeLiveRoleInputHash();
if(!useMarkerResolve && _liveRoleResultValid && inputHash == _liveRoleInputHash)
{
patternId = _liveRolePatternIdCache;
roleLabel = _liveRoleLabelCache ?? "";
return _liveRoleLabelCache != null;
}

var resolved = ResolveLiveRolesUncached(out patternId, out roleLabel);
_liveRoleInputHash = inputHash;
_liveRoleResultValid = true;
_liveRolePatternIdCache = resolved ? patternId : -1;
_liveRoleLabelCache = resolved ? roleLabel : null;
return resolved;
}

// Compute a hash of every input that affects live role assignment.
private int ComputeLiveRoleInputHash()
{
var hash = 17;
hash = hash * 31 + _step;
hash = hash * 31 + (_initialGroupResolved ? 1 : 0);
foreach(var info in _infos)
{
hash = hash * 31 + (int)info.Player.EntityId;
hash = hash * 31 + (int)info.Half;
hash = hash * 31 + (int)info.Debuff;
}

hash = hash * 31 + C.Step1PairMode;
hash = hash * 31 + (C.SpreadUseMarker ? 1 : 0);
hash = hash * 31 + (C.ConeUseMarker ? 1 : 0);
hash = hash * 31 + (int)C.SpreadMarkerType;
hash = hash * 31 + (int)C.ConeMarkerType;
return hash;
}

// Resolve live role labels and apply step/pattern rules without caching.
private bool ResolveLiveRolesUncached(out int patternId, out string roleLabel)
{
patternId = -1;
roleLabel = "";

if(!TryDetectPartyPattern(out patternId))
return false;

Expand Down
Loading