diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc index 2bf3b56df..074203002 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/CHHelpers.uc @@ -278,9 +278,24 @@ struct OverrideHasHeightAdvantageStruct var protectedwrite array OverrideHasHeightAdvantageCallbacks; // End Issue #851 +// Start Issue #1138 +struct PrioritizeRightClickMeleeStruct +{ + var delegate PrioritizeRightClickMeleeFn; + var int Priority; + + structdefaultproperties + { + Priority = 50 + } +}; +var protectedwrite array PrioritizeRightClickMeleeCallbacks; +// End Issue #1138 + delegate EHLDelegateReturn ShouldDisplayMultiSlotItemInStrategyDelegate(XComGameState_Unit UnitState, XComGameState_Item ItemState, out int bDisplayItem, XComUnitPawn UnitPawn, optional XComGameState CheckGameState); // Issue #885 delegate EHLDelegateReturn ShouldDisplayMultiSlotItemInTacticalDelegate(XComGameState_Unit UnitState, XComGameState_Item ItemState, out int bDisplayItem, XGUnit UnitVisualizer, optional XComGameState CheckGameState); // Issue #885 delegate EHLDelegateReturn OverrideHasHeightAdvantageDelegate(XComGameState_Unit Attacker, XComGameState_Unit TargetUnit, out int bHasHeightAdvantage); // Issue #851 +delegate EHLDelegateReturn PrioritizeRightClickMeleeDelegate(array MeleeAbilities, out XComGameState_Ability PrioritizedMeleeAbility); // Issue #1138 // Start Issue #123 simulated static function RebuildPerkContentCache() { @@ -940,6 +955,95 @@ simulated function TriggerOverrideHasHeightAdvantage(XComGameState_Unit Attacker } // End Issue #851 +// Start Issue #1138 +/// HL-Docs: ref:PrioritizeRightClickMelee +/// # Delegate Priority +/// You can optionally specify callback Priority. +///```unrealscript +///CHHelpersObj.AddPrioritizeRightClickMeleeCallback(PrioritizeRightClickMelee, 45); +///``` +/// Delegates with higher Priority value are executed first. +/// Delegates with the same Priority are executed in the order they were added to CHHelpers, +/// which would normally be the same as [DLCRunOrder](../misc/DLCRunOrder.md). +/// This function will return `true` if the delegate was successfully registered. +simulated function bool AddPrioritizeRightClickMeleeCallback(delegate PrioritizeRightClickMeleeFn, optional int Priority = 50) +{ + local PrioritizeRightClickMeleeStruct NewPrioritizeRightClickMeleeCallback; + local int i, PriorityIndex; + local bool bPriorityIndexFound; + + if (PrioritizeRightClickMeleeFn == none) + { + return false; + } + // Cycle through the array of callbacks backwards + for (i = PrioritizeRightClickMeleeCallbacks.Length - 1; i >= 0; i--) + { + // Do not allow registering the same delegate more than once. + if (PrioritizeRightClickMeleeCallbacks[i].PrioritizeRightClickMeleeFn == PrioritizeRightClickMeleeFn) + { + return false; + } + + // Record the array index of the callback whose priority is higher or equal to the priority of the new callback, + // so that the new callback can be inserted right after it. + if (PrioritizeRightClickMeleeCallbacks[i].Priority >= Priority && !bPriorityIndexFound) + { + PriorityIndex = i + 1; // +1 so that InsertItem puts the new callback *after* this one. + + // Keep cycling through the array so that the previous check for duplicate delegates can run for every currently registered delegate. + bPriorityIndexFound = true; + } + } + + NewPrioritizeRightClickMeleeCallback.Priority = Priority; + NewPrioritizeRightClickMeleeCallback.PrioritizeRightClickMeleeFn = PrioritizeRightClickMeleeFn; + PrioritizeRightClickMeleeCallbacks.InsertItem(PriorityIndex, NewPrioritizeRightClickMeleeCallback); + + return true; +} + +/// HL-Docs: ref:PrioritizeRightClickMelee +/// # Removing Delegates +/// If necessary, it's possible to remove a delegate. +///```unrealscript +///CHHelpersObj.RemovePrioritizeRightClickMeleeCallback(PrioritizeRightClickMelee); +///``` +/// The function will return `true` if the Callback was successfully deleted, return false otherwise. +simulated function bool RemovePrioritizeRightClickMeleeCallback(delegate PrioritizeRightClickMeleeFn) +{ + local int i; + + for (i = PrioritizeRightClickMeleeCallbacks.Length - 1; i >= 0; i--) + { + if (PrioritizeRightClickMeleeCallbacks[i].PrioritizeRightClickMeleeFn == PrioritizeRightClickMeleeFn) + { + PrioritizeRightClickMeleeCallbacks.Remove(i, 1); + return true; + } + } + return false; +} + +// Called by X2AbilityTrigger_EndOfMove::GetAvailableEndOfMoveAbilityForUnit() +// This is an internal CHL API. It is not intended for use by mods and is not covered by Backwards Compatibility policy. +simulated function TriggerPrioritizeRightClickMelee(array MeleeAbilities, out XComGameState_Ability PrioritizedMeleeAbility) +{ + local delegate PrioritizeRightClickMeleeFn; + local int i; + + for (i = 0; i < PrioritizeRightClickMeleeCallbacks.Length; i++) + { + PrioritizeRightClickMeleeFn = PrioritizeRightClickMeleeCallbacks[i].PrioritizeRightClickMeleeFn; + + if (PrioritizeRightClickMeleeFn(MeleeAbilities, PrioritizedMeleeAbility) == EHLDR_InterruptDelegates) + { + break; + } + } +} +// End Issue #1138 + // Start Issue #855 static function name GetPlaceEvacZoneAbilityName() { diff --git a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityTrigger_EndOfMove.uc b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityTrigger_EndOfMove.uc index d1f2f8b23..5cd7740e2 100644 --- a/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityTrigger_EndOfMove.uc +++ b/X2WOTCCommunityHighlander/Src/XComGame/Classes/X2AbilityTrigger_EndOfMove.uc @@ -9,6 +9,9 @@ class X2AbilityTrigger_EndOfMove extends X2AbilityTrigger; +// Issue #1138 Property for use by mods to support ability specific weighting in prioritization +var int RightClickMeleePriority; + static function bool AbilityHasEndOfMoveTrigger(X2AbilityTemplate Template) { local X2AbilityTrigger Trigger; @@ -27,12 +30,56 @@ static function bool AbilityHasEndOfMoveTrigger(X2AbilityTemplate Template) return false; } +/// HL-Docs: feature:PrioritizeRightClickMelee; issue:1138; tags:tactical +/// This feature allows mods to define which melee ability should be prioritized for use when the +/// user triggers a right-click melee action on an enemy. +/// +/// Normally this override would have been implemented as an event, but this check happens in the +/// tactical UI on user-click and movement tile hover-over which could lead to issues and slowdowns, +/// so the delegates system is used instead. +/// +/// ## How to use +/// +/// Implement the following code in your mod's `X2DownloadableContentInfo` class: +/// ```unrealscript +/// static event OnPostTemplatesCreated() +/// { +/// local CHHelpers CHHelpersObj; +/// +/// CHHelpersObj = class'CHHelpers'.static.GetCDO(); +/// if (CHHelpersObj != none) +/// { +/// CHHelpersObj.AddPrioritizeRightClickMeleeCallback(PrioritizeRightClickMelee); +/// } +/// } +/// +/// // To avoid crashes associated with garbage collection failure when transitioning between Tactical and Strategy, +/// // this function must be bound to the ClassDefaultObject of your class. Having this function in a class that +/// // `extends X2DownloadableContentInfo` is the easiest way to ensure that. +/// static private function EHLDelegateReturn PrioritizeRightClickMelee(array MeleeAbilities, out XComGameState_Ability PrioritizedMeleeAbility) +/// { +/// // Optionally modify PrioritizedMeleeAbility here. +/// // `PrioritizedMeleeAbility` is the currently prioritized melee skill to use, +/// // and should be replaced with a different skill from MeleeAbilities if +/// // using a different prioritization schema. Set `PrioritizedMeleeAbility` to NONE +/// // to revert to base-game behaviour. +/// +/// // Return EHLDR_NoInterrupt or EHLDR_InterruptDelegates depending on +/// // if you want to allow other delegates to run after yours +/// // and potentially modify PrioritizedMeleeAbility further. +/// return EHLDR_NoInterrupt; +///} +/// ``` static function XComGameState_Ability GetAvailableEndOfMoveAbilityForUnit(XComGameState_Unit UnitState) { local XComGameStateHistory History; local GameRulesCache_Unit UnitCache; local XComGameState_Ability AbilityState; local int ActionIndex; + // Variables for Issue #1138 + local CHHelpers CHHelpersObj; + local XComGameState_Ability FirstMeleeAbility, PrioritizedMeleeAbility; + local array MeleeAbilities; if(`TACTICALRULES.GetGameRulesCache_Unit(UnitState.GetReference(), UnitCache)) { @@ -42,10 +89,45 @@ static function XComGameState_Ability GetAvailableEndOfMoveAbilityForUnit(XComGa AbilityState = XComGameState_Ability(History.GetGameStateForObjectID(UnitCache.AvailableActions[ActionIndex].AbilityObjectRef.ObjectID)); if(AbilityState != none && AbilityHasEndOfMoveTrigger(AbilityState.GetMyTemplate())) { - return AbilityState; + // Start Issue #1138 + // No longer greedily return the first melee ability, instead gather all eligible + // candidates and submit to a set of delegates for prioritization + if(FirstMeleeAbility == none) + { + FirstMeleeAbility = AbilityState; + } + MeleeAbilities.AddItem(AbilityState); + + //return AbilityState; + // End Issue #1138 } } } - return None; -} \ No newline at end of file + // Start Issue #1138 + if(MeleeAbilities.Length > 0) + { + PrioritizedMeleeAbility = FirstMeleeAbility; + CHHelpersObj = class'CHHelpers'.static.GetCDO(); + CHHelpersObj.TriggerPrioritizeRightClickMelee(MeleeAbilities, PrioritizedMeleeAbility); + + if(PrioritizedMeleeAbility == none) + { + // If no ability was prioritized revert to default game behavior + return FirstMeleeAbility; + } + // Return the prioritized melee ability + return PrioritizedMeleeAbility; + } + else + { + return None; + } + // End Issue #1138 +} + +// Issue #1138 Default Properties +defaultproperties +{ + RightClickMeleePriority=0 +}