From 97529766f99e8ee0c65c8f92505415ddb2a57cc3 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 12 Mar 2024 20:57:43 -0700 Subject: [PATCH 01/61] Reorganize SimUtils.lua --- lua/SimUtils.lua | 135 +++++++++++++++++++++++++---------------------- 1 file changed, 72 insertions(+), 63 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 66d92f082b..bf76ed8519 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -2,9 +2,8 @@ -- -- General Sim scripts --- ============================================================================== --- Diplomacy --- ============================================================================== +------------------------------------------------------------------------------------------------------------------------ +--#region General Unit Transfer Scripts local CreateWreckage = import("/lua/wreckage.lua").CreateWreckage @@ -12,33 +11,6 @@ local transferUnbuiltCategory = categories.ALLUNITS local transferUnitsCategory = categories.ALLUNITS - categories.INSIGNIFICANTUNIT local buildersCategory = categories.ALLUNITS - categories.CONSTRUCTION - categories.ENGINEER -local sharedUnits = {} - ----@param owner number --- categoriesToKill is an optional input (it defaults to all categories) -function KillSharedUnits(owner, categoriesToKill) - local sharedUnitOwner = sharedUnits[owner] - if sharedUnitOwner and not table.empty(sharedUnitOwner) then - local sharedUnitOwnerSize = table.getn(sharedUnitOwner) - for i = sharedUnitOwnerSize, 1, -1 do - local unit = sharedUnitOwner[i] - if not unit.Dead and unit.oldowner == owner then - if categoriesToKill then - if EntityCategoryContains(categoriesToKill, unit) then - table.remove(sharedUnits[owner], i) - unit:Kill() - end - else - unit:Kill() - end - end - end - if not categoriesToKill then - sharedUnits[owner] = {} - end - end -end - -- used to make more expensive units transfer first, in case there's a unit cap issue local function TransferUnitsOwnershipComparator(a, b) a = a.Blueprint or a.Blueprint @@ -62,6 +34,8 @@ local function TransferUnitsOwnershipDelayedWeapons(weapon) end end +local sharedUnits = {} + --- Transfers units to an army, returning the new units (since changing the army --- replaces the units with new ones) ---@param units Unit[] @@ -625,44 +599,38 @@ function GiveUnitsToPlayer(data, units) end end ----@param data {Army: number, Value: boolean} -function SetResourceSharing(data) - local army = data.Army - if not OkayToMessWithArmy(army) then - return - end - local brain = GetArmyBrain(army) - brain:SetResourceSharing(data.Value) -end +--#endregion ----@param data {Army: number, Value: boolean} -function RequestAlliedVictory(data) - -- You cannot change this in a team game - if ScenarioInfo.TeamGame then - return - end - local army = data.Army - if not OkayToMessWithArmy(army) then - return - end - local brain = GetArmyBrain(army) - brain.RequestingAlliedVictory = data.Value -end +------------------------------------------------------------------------------------------------------------------------ +--#region Army Death Unit Transfer ----@param data {Army: number, Value: boolean} -function SetOfferDraw(data) - local army = data.Army - if not OkayToMessWithArmy(army) then - return +--- Functions related to dealing with unit ownership when an army dies based on share conditions. + +---@param owner number +-- categoriesToKill is an optional input (it defaults to all categories) +function KillSharedUnits(owner, categoriesToKill) + local sharedUnitOwner = sharedUnits[owner] + if sharedUnitOwner and not table.empty(sharedUnitOwner) then + local sharedUnitOwnerSize = table.getn(sharedUnitOwner) + for i = sharedUnitOwnerSize, 1, -1 do + local unit = sharedUnitOwner[i] + if not unit.Dead and unit.oldowner == owner then + if categoriesToKill then + if EntityCategoryContains(categoriesToKill, unit) then + table.remove(sharedUnits[owner], i) + unit:Kill() + end + else + unit:Kill() + end + end + end + if not categoriesToKill then + sharedUnits[owner] = {} + end end - local brain = GetArmyBrain(army) - brain.OfferingDraw = data.Value end --- ============================================================================== --- UNIT CAP --- ============================================================================== - --- Given that `deadArmy` just died, redistributes their unit cap based on the scenario options ---@param deadArmy number function UpdateUnitCap(deadArmy) @@ -696,6 +664,45 @@ function UpdateUnitCap(deadArmy) end end +--#endregion + +------------------------------------------------------------------------------------------------------------------------ +--#region Non-Unit Transfer Diplomacy + +---@param data {Army: number, Value: boolean} +function SetResourceSharing(data) + local army = data.Army + if not OkayToMessWithArmy(army) then + return + end + local brain = GetArmyBrain(army) + brain:SetResourceSharing(data.Value) +end + +---@param data {Army: number, Value: boolean} +function RequestAlliedVictory(data) + -- You cannot change this in a team game + if ScenarioInfo.TeamGame then + return + end + local army = data.Army + if not OkayToMessWithArmy(army) then + return + end + local brain = GetArmyBrain(army) + brain.RequestingAlliedVictory = data.Value +end + +---@param data {Army: number, Value: boolean} +function SetOfferDraw(data) + local army = data.Army + if not OkayToMessWithArmy(army) then + return + end + local brain = GetArmyBrain(army) + brain.OfferingDraw = data.Value +end + ---@param data {Sender: number, Msg: string} function SendChatToReplay(data) if data.Sender and data.Msg then @@ -771,6 +778,8 @@ import("/lua/simplayerquery.lua").AddResultListener("OfferAlliance", OnAllianceR local vectorCross = import('/lua/utilities.lua').Cross local upVector = Vector(0, 1, 0) +--#endregion + --- Draw XYZ axes of an entity's bone for one tick ---@param entity moho.entity_methods ---@param bone Bone From 5117faade4ad63790d1d5e3af62d65ea528d668e Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 12 Mar 2024 22:26:48 -0700 Subject: [PATCH 02/61] Move army death functions from aibrain to simutils --- lua/SimUtils.lua | 287 ++++++++++++++++++++++++++++++++++++ lua/aibrain.lua | 376 ++--------------------------------------------- 2 files changed, 299 insertions(+), 364 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index bf76ed8519..b8f3bba16b 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -664,8 +664,295 @@ function UpdateUnitCap(deadArmy) end end +--- Transfer a brain's units to other brains. +---@param self AIBrain +---@param brains AIBrain[] +---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) + if not table.empty(brains) then + local units + if shareOption == 'FullShare' then + local indexes = {} + for _, brain in brains do + table.insert(indexes, brain.index) + end + units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) + TransferUnfinishedUnitsAfterDeath(units, indexes) + end + + for k, brain in brains do + if categoriesToTransfer then + units = self:GetListOfUnits(categoriesToTransfer, false) + else + units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) + end + if units and not table.empty(units) then + local givenUnitCount = table.getn(TransferUnitsOwnership(units, brain.index)) + + -- only show message when we actually gift that player some units + if givenUnitCount > 0 then + Sync.ArmyTransfer = { { + from = self.index, + to = brain.index, + reason = "fullshare" + } } + end + + -- Prevent giving the same units to multiple armies + WaitSeconds(1) + end + end + end +end + +--- Transfer a brain's units to other brains, sorted by positive rating and then score. +---@param self AIBrain +---@param brains AIBrain[] +---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +function TransferUnitsToHighestBrain(self, brains, shareOption, categoriesToTransfer) + if not table.empty(brains) then + local ratings = ScenarioInfo.Options.Ratings + for _, brain in brains do + if ratings[brain.Nickname] then + brain.rating = ratings[brain.Nickname] + else + -- if there is no rating, create a fake negative rating based on score + brain.rating = -1 / brain.score + end + end + -- sort brains by rating + table.sort(brains, function(a, b) return a.rating > b.rating end) + TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) + end +end + +--local helper functions for KillArmy + +---@param self AIBrain +local function KillWalls(self) + local tokill = self:GetListOfUnits(categories.WALL, false) + if tokill and not table.empty(tokill) then + for index, unit in tokill do + unit:Kill() + end + end +end + +--- Remove the borrowed status from units we lent to allies. +---@param brains AIBrain[] +---@param selfIndex number +local function TransferOwnershipOfBorrowedUnits(brains, selfIndex) + for index, brain in brains do + local units = brain:GetListOfUnits(categories.ALLUNITS, false) + if units and not table.empty(units) then + for _, unit in units do + if unit.oldowner == selfIndex then + unit.oldowner = nil + end + end + end + end +end + +--- Return units transferred to me to their original owner (if alive) +---@param self AIBrain +local function ReturnBorrowedUnits(self) + local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) + local borrowed = {} + for index, unit in units do + local oldowner = unit.oldowner + if oldowner and oldowner ~= self:GetArmyIndex() and not GetArmyBrain(oldowner):IsDefeated() then + if not borrowed[oldowner] then + borrowed[oldowner] = {} + end + table.insert(borrowed[oldowner], unit) + end + end + + for owner, units in borrowed do + TransferUnitsOwnership(units, owner) + end + + WaitSeconds(1) +end + +--- Take back units I gave away. Mainly needed to stop mods that auto-give after death from bypassing share conditions. +---@param selfIndex number +---@param brains AIBrain[] +local function GetBackUnits(selfIndex, brains) + local given = {} + for index, brain in brains do + local units = brain:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) + if units and not table.empty(units) then + for _, unit in units do + if unit.oldowner == selfIndex then + table.insert(given, unit) + unit.oldowner = nil + end + end + end + end + + TransferUnitsOwnership(given, selfIndex) +end + +--- Transfer units to the player who killed me +---@param self AIBrain +local function TransferUnitsToKiller(self) + local selfIndex = self:GetArmyIndex() + local killerIndex = 0 + local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) + if units and not table.empty(units) then + if ScenarioInfo.Options.Victory == 'demoralization' then + killerIndex = ArmyBrains[selfIndex].CommanderKilledBy or selfIndex + TransferUnitsOwnership(units, killerIndex) + else + killerIndex = ArmyBrains[selfIndex].LastUnitKilledBy or selfIndex + TransferUnitsOwnership(units, killerIndex) + end + end + WaitSeconds(1) +end + +local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore + +--- Kills an army according to the given share condition. +---@param self AIBrain +---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +function KillArmy(self, shareOption) + + -- Kill all walls while the ACU is blowing up + if shareOption == 'ShareUntilDeath' then + ForkThread(KillWalls) + end + + WaitSeconds(10) -- Wait for commander explosion, then transfer units. + + local selfIndex = self:GetArmyIndex() + + local BrainCategories = { Enemies = {}, Civilians = {}, Allies = {} } + + -- Sort brains out into mutually exclusive categories + for index, brain in ArmyBrains do + brain.index = index + brain.score = CalculateBrainScore(brain) + + if not brain:IsDefeated() and selfIndex ~= index then + if ArmyIsCivilian(index) then + table.insert(BrainCategories.Civilians, brain) + elseif IsEnemy(selfIndex, brain:GetArmyIndex()) then + table.insert(BrainCategories.Enemies, brain) + else + table.insert(BrainCategories.Allies, brain) + end + end + end + + -- This part determines the share condition + if shareOption == 'ShareUntilDeath' then + KillSharedUnits(selfIndex) + ReturnBorrowedUnits(self) + elseif shareOption == 'FullShare' then + TransferUnitsToHighestBrain(self, BrainCategories.Allies, shareOption) + TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) + elseif shareOption == 'PartialShare' then + KillSharedUnits(selfIndex, categories.ALLUNITS - categories.STRUCTURE - categories.ENGINEER) + ReturnBorrowedUnits(self) + TransferUnitsToHighestBrain(self, BrainCategories.Allies, categories.STRUCTURE + categories.ENGINEER, shareOption) + TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) + else + GetBackUnits(selfIndex, BrainCategories.Allies) + if shareOption == 'CivilianDeserter' then + TransferUnitsToBrain(self, BrainCategories.Civilians, shareOption) + elseif shareOption == 'TransferToKiller' then + TransferUnitsToKiller(self) + elseif shareOption == 'Defectors' then + TransferUnitsToHighestBrain(self, BrainCategories.Enemies, shareOption) + else -- Something went wrong in settings. Act like share until death to avoid abuse + WARN('Invalid share condition was used for this game. Defaulting to killing all units') + KillSharedUnits(selfIndex) + ReturnBorrowedUnits(self) + end + end + + -- Kill all units left over + local tokill = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) + if tokill and not table.empty(tokill) then + for index, unit in tokill do + unit:Kill() + end + end +end + --#endregion +local SorianUtils = import("/lua/ai/sorianutilities.lua") + +--- Disables the AI for non-player armies. +---@param self AIBrain +function DisableAI(self) + local army = self.Army + -- print AI "ilost" text to chat + SorianUtils.AISendChat("enemies", ArmyBrains[army].Nickname, "ilost") + -- remove PlatoonHandle from all AI units before we kill / transfer the army + local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) + if units and units[1] then + local halt = 0 + local haltUnits = {} + for _, unit in units do + if not unit.Dead then + local handle = unit.PlatoonHandle + if handle and self:PlatoonExists(handle) then + handle:Stop() + handle:PlatoonDisbandNoAssign() + end + halt = halt + 1 + haltUnits[halt] = unit + end + end + IssueStop(haltUnits) + IssueClearCommands(haltUnits) + end + + -- Stop the AI from executing AI plans + self.RepeatExecution = false + + -- removing AI BrainConditionsMonitor + if self.ConditionsMonitor then + self.ConditionsMonitor:Destroy() + end + + -- removing AI BuilderManagers + if self.BuilderManagers then + for _, v in self.BuilderManagers do + local manager = v.EngineerManager + manager:SetEnabled(false) + manager:Destroy() + manager = v.FactoryManager + manager:SetEnabled(false) + manager:Destroy() + manager = v.PlatoonFormManager + manager:SetEnabled(false) + manager:Destroy() + manager = v.StrategyManager + if manager then + manager:SetEnabled(false) + manager:Destroy() + end + v.EngineerManager = nil + v.FactoryManager = nil + v.PlatoonFormManager = nil + v.BaseSettings = nil + v.BuilderHandles = nil + v.Position = nil + end + end + -- delete the AI pathcache + self.PathCache = nil +end + ------------------------------------------------------------------------------------------------------------------------ --#region Non-Unit Transfer Diplomacy diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 1427408d7c..d2eb753778 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -10,6 +10,10 @@ local SUtils = import("/lua/ai/sorianutilities.lua") local TransferUnitsOwnership = import("/lua/simutils.lua").TransferUnitsOwnership local TransferUnfinishedUnitsAfterDeath = import("/lua/simutils.lua").TransferUnfinishedUnitsAfterDeath +local KillArmy = import("/lua/simutils.lua").KillArmy +local DisableAI = import("/lua/simutils.lua").DisableAI +local TransferUnitsToBrain = import("/lua/simutils.lua").TransferUnitsToBrain +local TransferUnitsToHighestBrain = import("/lua/simutils.lua").TransferUnitsToHighestBrain local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore local StorageManagerBrainComponent = import("/lua/aibrains/components/StorageManagerBrainComponent.lua").StorageManagerBrainComponent @@ -438,272 +442,12 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM import("/lua/simping.lua").OnArmyDefeat(self:GetArmyIndex()) import("/lua/sim/Recall.lua").OnArmyDefeat(self:GetArmyIndex()) - local function KillArmy() - local shareOption = ScenarioInfo.Options.Share - - local function KillWalls() - -- Kill all walls while the ACU is blowing up - local tokill = self:GetListOfUnits(categories.WALL, false) - if tokill and not table.empty(tokill) then - for index, unit in tokill do - unit:Kill() - end - end - end - - if shareOption == 'ShareUntilDeath' then - ForkThread(KillWalls) - end - - WaitSeconds(10) -- Wait for commander explosion, then transfer units. - local selfIndex = self:GetArmyIndex() - local shareOption = ScenarioInfo.Options.Share - local victoryOption = ScenarioInfo.Options.Victory - local BrainCategories = { Enemies = {}, Civilians = {}, Allies = {} } - - -- Used to have units which were transferred to allies noted permanently as belonging to the new player - local function TransferOwnershipOfBorrowedUnits(brains) - for index, brain in brains do - local units = brain:GetListOfUnits(categories.ALLUNITS, false) - if units and not table.empty(units) then - for _, unit in units do - if unit.oldowner == selfIndex then - unit.oldowner = nil - end - end - end - end - end - - -- Transfer our units to other brains. Wait in between stops transfer of the same units to multiple armies. - -- Optional Categories input (defaults to all units except wall and command) - local function TransferUnitsToBrain(brains, categoriesToTransfer) - if not table.empty(brains) then - local units - if shareOption == 'FullShare' then - local indexes = {} - for _, brain in brains do - table.insert(indexes, brain.index) - end - units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) - TransferUnfinishedUnitsAfterDeath(units, indexes) - end - - for k, brain in brains do - if categoriesToTransfer then - units = self:GetListOfUnits(categoriesToTransfer, false) - else - units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) - end - if units and not table.empty(units) then - local givenUnits = TransferUnitsOwnership(units, brain.index) - - -- only show message when we actually gift that player some units - if not table.empty(givenUnits) then - Sync.ArmyTransfer = { { from = selfIndex, to = brain.index, reason = "fullshare" } } - end - - WaitSeconds(1) - end - end - end - end - - -- Sort the destiniation brains (armies/players) by rating (and if rating does not exist (such as with regular AI's), by score, after players with positive rating) - -- optional category input (default of everything but walls and command) - local function TransferUnitsToHighestBrain(brains, categoriesToTransfer) - if not table.empty(brains) then - local ratings = ScenarioInfo.Options.Ratings - for i, brain in brains do - if ratings[brain.Nickname] then - brain.rating = ratings[brain.Nickname] - else - -- if there is no rating, create a fake negative rating based on score - brain.rating = -(1 / brain.score) - end - end - -- sort brains by rating - table.sort(brains, function(a, b) return a.rating > b.rating end) - TransferUnitsToBrain(brains, categoriesToTransfer) - end - end - - -- Transfer units to the player who killed me - local function TransferUnitsToKiller() - local KillerIndex = 0 - local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) - if units and not table.empty(units) then - if victoryOption == 'demoralization' then - KillerIndex = ArmyBrains[selfIndex].CommanderKilledBy or selfIndex - TransferUnitsOwnership(units, KillerIndex) - else - KillerIndex = ArmyBrains[selfIndex].LastUnitKilledBy or selfIndex - TransferUnitsOwnership(units, KillerIndex) - end - end - WaitSeconds(1) - end - - -- Return units transferred during the game to me - local function ReturnBorrowedUnits() - local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - local borrowed = {} - for index, unit in units do - local oldowner = unit.oldowner - if oldowner and oldowner ~= self:GetArmyIndex() and not GetArmyBrain(oldowner):IsDefeated() then - if not borrowed[oldowner] then - borrowed[oldowner] = {} - end - table.insert(borrowed[oldowner], unit) - end - end - - for owner, units in borrowed do - TransferUnitsOwnership(units, owner) - end - - WaitSeconds(1) - end - - -- Return units I gave away to my control. Mainly needed to stop EcoManager mods bypassing all this stuff with auto-give - local function GetBackUnits(brains) - local given = {} - for index, brain in brains do - local units = brain:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - if units and not table.empty(units) then - for _, unit in units do - if unit.oldowner == selfIndex then -- The unit was built by me - table.insert(given, unit) - unit.oldowner = nil - end - end - end - end - - TransferUnitsOwnership(given, selfIndex) - end - - -- Sort brains out into mutually exclusive categories - for index, brain in ArmyBrains do - brain.index = index - brain.score = CalculateBrainScore(brain) - - if not brain:IsDefeated() and selfIndex ~= index then - if ArmyIsCivilian(index) then - table.insert(BrainCategories.Civilians, brain) - elseif IsEnemy(selfIndex, brain:GetArmyIndex()) then - table.insert(BrainCategories.Enemies, brain) - else - table.insert(BrainCategories.Allies, brain) - end - end - end - - local KillSharedUnits = import("/lua/simutils.lua").KillSharedUnits - - -- This part determines the share condition - if shareOption == 'ShareUntilDeath' then - KillSharedUnits(self:GetArmyIndex()) -- Kill things I gave away - ReturnBorrowedUnits() -- Give back things I was given by others - elseif shareOption == 'FullShare' then - TransferUnitsToHighestBrain(BrainCategories.Allies) -- Transfer things to allies, highest rating first - TransferOwnershipOfBorrowedUnits(BrainCategories.Allies) -- Give stuff away permanently - elseif shareOption == 'PartialShare' then - KillSharedUnits(self:GetArmyIndex(), categories.ALLUNITS - categories.STRUCTURE - categories.ENGINEER) -- Kill some things I gave away - ReturnBorrowedUnits() -- Give back things I was given by others - TransferUnitsToHighestBrain(BrainCategories.Allies, categories.STRUCTURE + categories.ENGINEER) -- Transfer some things to allies, highest rating first - TransferOwnershipOfBorrowedUnits(BrainCategories.Allies) -- Give stuff away permanently - else - GetBackUnits(BrainCategories.Allies) -- Get back units I gave away - if shareOption == 'CivilianDeserter' then - TransferUnitsToBrain(BrainCategories.Civilians) - elseif shareOption == 'TransferToKiller' then - TransferUnitsToKiller() - elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(BrainCategories.Enemies) - else -- Something went wrong in settings. Act like share until death to avoid abuse - WARN('Invalid share condition was used for this game. Defaulting to killing all units') - KillSharedUnits(self:GetArmyIndex()) -- Kill things I gave away - ReturnBorrowedUnits() -- Give back things I was given by other - end - end - - -- Kill all units left over - local tokill = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - if tokill and not table.empty(tokill) then - for index, unit in tokill do - unit:Kill() - end - end - end - -- AI if self.BrainType == 'AI' then - -- print AI "ilost" text to chat - SUtils.AISendChat('enemies', ArmyBrains[self:GetArmyIndex()].Nickname, 'ilost') - -- remove PlatoonHandle from all AI units before we kill / transfer the army - local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - if units and not table.empty(units) then - for _, unit in units do - if not unit.Dead then - if unit.PlatoonHandle and self:PlatoonExists(unit.PlatoonHandle) then - unit.PlatoonHandle:Stop() - unit.PlatoonHandle:PlatoonDisbandNoAssign() - end - IssueStop({ unit }) - IssueToUnitClearCommands(unit) - end - end - end - -- Stop the AI from executing AI plans - self.RepeatExecution = false - -- removing AI BrainConditionsMonitor - if self.ConditionsMonitor then - self.ConditionsMonitor:Destroy() - end - -- removing AI BuilderManagers - if self.BuilderManagers then - for k, manager in self.BuilderManagers do - if manager.EngineerManager then - manager.EngineerManager:SetEnabled(false) - end - - if manager.FactoryManager then - manager.FactoryManager:SetEnabled(false) - end - - if manager.PlatoonFormManager then - manager.PlatoonFormManager:SetEnabled(false) - end - - if manager.EngineerManager then - manager.EngineerManager:Destroy() - end - - if manager.FactoryManager then - manager.FactoryManager:Destroy() - end - - if manager.PlatoonFormManager then - manager.PlatoonFormManager:Destroy() - end - if manager.StrategyManager then - manager.StrategyManager:SetEnabled(false) - manager.StrategyManager:Destroy() - end - self.BuilderManagers[k].EngineerManager = nil - self.BuilderManagers[k].FactoryManager = nil - self.BuilderManagers[k].PlatoonFormManager = nil - self.BuilderManagers[k].BaseSettings = nil - self.BuilderManagers[k].BuilderHandles = nil - self.BuilderManagers[k].Position = nil - end - end - -- delete the AI pathcache - self.PathCache = nil + DisableAI(self) end - ForkThread(KillArmy) + ForkThread(KillArmy, self, ScenarioInfo.Options.Share) if self.Trash then self.Trash:Destroy() @@ -741,109 +485,11 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM -- AI if self.BrainType == "AI" then - -- print AI "ilost" text to chat - SUtils.AISendChat("enemies", ArmyBrains[army].Nickname, "ilost") - -- remove PlatoonHandle from all AI units before we kill / transfer the army - local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - if units and units[1] then - local halt = 0 - local haltUnits = {} - for _, unit in units do - if not unit.Dead then - local handle = unit.PlatoonHandle - if handle and self:PlatoonExists(handle) then - handle:Stop() - handle:PlatoonDisbandNoAssign() - end - halt = halt + 1 - haltUnits[halt] = unit - end - end - IssueStop(haltUnits) - IssueClearCommands(haltUnits) - end - - -- Stop the AI from executing AI plans - self.RepeatExecution = false - - -- removing AI BrainConditionsMonitor - if self.ConditionsMonitor then - self.ConditionsMonitor:Destroy() - end - - -- removing AI BuilderManagers - if self.BuilderManagers then - for _, v in self.BuilderManagers do - local manager = v.EngineerManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.FactoryManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.PlatoonFormManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.StrategyManager - if manager then - manager:SetEnabled(false) - manager:Destroy() - end - v.EngineerManager = nil - v.FactoryManager = nil - v.PlatoonFormManager = nil - v.BaseSettings = nil - v.BuilderHandles = nil - v.Position = nil - end - end - -- delete the AI pathcache - self.PathCache = nil + DisableAI(self) end local enemies, civilians = {}, {} - -- Transfer our units to other brains. Wait in between stops transfer of the same units to multiple armies. - local function TransferUnitsToBrain(brains) - if brains[1] then - local cat = categories.ALLUNITS - categories.WALL - categories.COMMAND - categories.SUBCOMMANDER - for _, brain in brains do - local units = self:GetListOfUnits(cat, false) - if units and units[1] then - local givenUnits = TransferUnitsOwnership(units, brain.index) - - -- only show message when we actually gift that player some units - if not table.empty(givenUnits) then - Sync.ArmyTransfer = { { - from = army, - to = brain.index, - reason = "fullshare", - } } - end - - WaitSeconds(1) - end - end - end - end - - -- Sort the destiniation brains (armies/players) by rating (and if rating does not exist (such as with regular AI's), by score, after players with positive rating) - local function TransferUnitsToHighestBrain(brains) - if not table.empty(brains) then - local ratings = ScenarioInfo.Options.Ratings - for _, brain in brains do - if ratings[brain.Nickname] then - brain.rating = ratings[brain.Nickname] - else - -- if there is no rating, create a fake negative rating based on score - brain.rating = -1 / brain.score - end - end - -- sort brains by rating - table.sort(brains, function(a, b) return a.rating > b.rating end) - TransferUnitsToBrain(brains) - end - end - -- Sort brains out into mutually exclusive categories for index, brain in ArmyBrains do brain.index = index @@ -858,12 +504,14 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM end end - -- This part determines the share condition + -- Recalling has different share conditions than defeat because the entire team recalls simultaneously. + -- Recalling recalls all SACU, so they shouldn't be transferred. + local cat = categories.ALLUNITS - categories.WALL - categories.COMMAND - categories.SUBCOMMANDER local shareOption = ScenarioInfo.Options.Share if shareOption == 'CivilianDeserter' then - TransferUnitsToBrain(civilians) + TransferUnitsToBrain(self, civilians, shareOption, cat) elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(enemies) + TransferUnitsToHighestBrain(self, enemies, shareOption, cat) end -- let the average, team vs team game end first From eba2944429f3bf7da740741cc1f004fc155bcdb5 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:37:54 -0700 Subject: [PATCH 03/61] Use newer version of DisableAI from OnDefeat instead of from OnRecalled --- lua/SimUtils.lua | 75 ++++++++++++++++++++++++++---------------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index b8f3bba16b..5d33ed7c47 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -895,58 +895,63 @@ local SorianUtils = import("/lua/ai/sorianutilities.lua") function DisableAI(self) local army = self.Army -- print AI "ilost" text to chat - SorianUtils.AISendChat("enemies", ArmyBrains[army].Nickname, "ilost") + SorianUtils.AISendChat('enemies', ArmyBrains[self:GetArmyIndex()].Nickname, 'ilost') -- remove PlatoonHandle from all AI units before we kill / transfer the army local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL, false) - if units and units[1] then - local halt = 0 - local haltUnits = {} + if units and not table.empty(units) then for _, unit in units do if not unit.Dead then - local handle = unit.PlatoonHandle - if handle and self:PlatoonExists(handle) then - handle:Stop() - handle:PlatoonDisbandNoAssign() + if unit.PlatoonHandle and self:PlatoonExists(unit.PlatoonHandle) then + unit.PlatoonHandle:Stop() + unit.PlatoonHandle:PlatoonDisbandNoAssign() end - halt = halt + 1 - haltUnits[halt] = unit + IssueStop({ unit }) + IssueToUnitClearCommands(unit) end end - IssueStop(haltUnits) - IssueClearCommands(haltUnits) end - -- Stop the AI from executing AI plans self.RepeatExecution = false - -- removing AI BrainConditionsMonitor if self.ConditionsMonitor then self.ConditionsMonitor:Destroy() end - -- removing AI BuilderManagers if self.BuilderManagers then - for _, v in self.BuilderManagers do - local manager = v.EngineerManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.FactoryManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.PlatoonFormManager - manager:SetEnabled(false) - manager:Destroy() - manager = v.StrategyManager - if manager then - manager:SetEnabled(false) - manager:Destroy() + for k, manager in self.BuilderManagers do + if manager.EngineerManager then + manager.EngineerManager:SetEnabled(false) + end + + if manager.FactoryManager then + manager.FactoryManager:SetEnabled(false) + end + + if manager.PlatoonFormManager then + manager.PlatoonFormManager:SetEnabled(false) + end + + if manager.EngineerManager then + manager.EngineerManager:Destroy() + end + + if manager.FactoryManager then + manager.FactoryManager:Destroy() + end + + if manager.PlatoonFormManager then + manager.PlatoonFormManager:Destroy() + end + if manager.StrategyManager then + manager.StrategyManager:SetEnabled(false) + manager.StrategyManager:Destroy() end - v.EngineerManager = nil - v.FactoryManager = nil - v.PlatoonFormManager = nil - v.BaseSettings = nil - v.BuilderHandles = nil - v.Position = nil + self.BuilderManagers[k].EngineerManager = nil + self.BuilderManagers[k].FactoryManager = nil + self.BuilderManagers[k].PlatoonFormManager = nil + self.BuilderManagers[k].BaseSettings = nil + self.BuilderManagers[k].BuilderHandles = nil + self.BuilderManagers[k].Position = nil end end -- delete the AI pathcache From bb99f92be90ad45aa7298212fa49402f2d1561ea Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:49:56 -0700 Subject: [PATCH 04/61] upvalue functions --- lua/aibrain.lua | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index d2eb753778..5be78c33a6 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -14,6 +14,9 @@ local KillArmy = import("/lua/simutils.lua").KillArmy local DisableAI = import("/lua/simutils.lua").DisableAI local TransferUnitsToBrain = import("/lua/simutils.lua").TransferUnitsToBrain local TransferUnitsToHighestBrain = import("/lua/simutils.lua").TransferUnitsToHighestBrain +local UpdateUnitCap = import("/lua/simutils.lua").UpdateUnitCap +local SimPingOnArmyDefeat = import("/lua/simping.lua").OnArmyDefeat +local RecallOnArmyDefeat = import("/lua/sim/Recall.lua") local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore local StorageManagerBrainComponent = import("/lua/aibrains/components/StorageManagerBrainComponent.lua").StorageManagerBrainComponent @@ -438,9 +441,10 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM OnDefeat = function(self) self.Status = 'Defeat' - import("/lua/simutils.lua").UpdateUnitCap(self:GetArmyIndex()) - import("/lua/simping.lua").OnArmyDefeat(self:GetArmyIndex()) - import("/lua/sim/Recall.lua").OnArmyDefeat(self:GetArmyIndex()) + local selfIndex = self:GetArmyIndex() + UpdateUnitCap(selfIndex) + SimPingOnArmyDefeat(selfIndex) + RecallOnArmyDefeat(selfIndex) -- AI if self.BrainType == 'AI' then @@ -470,7 +474,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM ---@param recallingUnits Unit[] RecallArmyThread = function(self, recallingUnits) if recallingUnits then - import("/lua/scenarioframework.lua").FakeTeleportUnits(recallingUnits, true) + FakeTeleportUnits(recallingUnits, true) end self:OnRecalled() end, @@ -479,9 +483,9 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM -- TODO: create a common function for `OnDefeat` and `OnRecall` self.Status = "Recalled" - local army = self.Army - import("/lua/simutils.lua").UpdateUnitCap(army) - import("/lua/simping.lua").OnArmyDefeat(army) + local selfIndex = self:GetArmyIndex() + UpdateUnitCap(selfIndex) + OnArmyDefeat(selfIndex) -- AI if self.BrainType == "AI" then @@ -495,10 +499,10 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM brain.index = index brain.score = CalculateBrainScore(brain) - if not brain:IsDefeated() and army ~= index then + if not brain:IsDefeated() and selfIndex ~= index then if ArmyIsCivilian(index) then table.insert(civilians, brain) - elseif IsEnemy(army, brain:GetArmyIndex()) then + elseif IsEnemy(selfIndex, brain:GetArmyIndex()) then table.insert(enemies, brain) end end @@ -506,12 +510,12 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM -- Recalling has different share conditions than defeat because the entire team recalls simultaneously. -- Recalling recalls all SACU, so they shouldn't be transferred. - local cat = categories.ALLUNITS - categories.WALL - categories.COMMAND - categories.SUBCOMMANDER + local recallCat = categories.ALLUNITS - categories.WALL - categories.COMMAND - categories.SUBCOMMANDER local shareOption = ScenarioInfo.Options.Share if shareOption == 'CivilianDeserter' then - TransferUnitsToBrain(self, civilians, shareOption, cat) + TransferUnitsToBrain(self, civilians, shareOption, recallCat) elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(self, enemies, shareOption, cat) + TransferUnitsToHighestBrain(self, enemies, shareOption, recallCat) end -- let the average, team vs team game end first From 18b76ddfe146f442007e36316c8cbdfc78217a04 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 11 Mar 2024 04:05:05 -0700 Subject: [PATCH 05/61] Add text for on-disconnect lobby options --- loc/US/strings_db.lua | 26 +++++-- lua/ui/lobby/lobbyOptions.lua | 139 ++++++++++++++++++++++++---------- 2 files changed, 119 insertions(+), 46 deletions(-) diff --git a/loc/US/strings_db.lua b/loc/US/strings_db.lua index 9110ab2628..e04a3b3615 100644 --- a/loc/US/strings_db.lua +++ b/loc/US/strings_db.lua @@ -7744,6 +7744,24 @@ lobui_0795="No manual sharing of units" lobui_0796="Partial Share" lobui_0797="Your buildings and engineers will be transferred to your highest rated ally when you die. Your other units will be destroyed when you die, except those captured by the enemy." +lobui_0798="DC Share Conditions" +lobui_0799="Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU is at full health/shield." +lobui_0800="Same as Share Condition" +lobui_0801="Treat disconnecting players the same as defeated players." + +-- unranked lobby option +lobui_0802="Unrate" +lobui_0803="Provides a toggle to unrate a game. Note that if this is set to no the game can still be unrated due to other lobby options, unrated sim mods and / or the map being unrated." +lobui_0804="No" +lobui_0805="This game will be rated if all the criteria for a rated game are met." +lobui_0806="Yes" +lobui_0807="This game will not be rated." + +lobui_0808="Recall Disconnected ACUs" +lobui_0809="Should disconnecting players' full health/shield ACUs be recalled, preventing their explosion?" +lobui_0810="ACUs explode when their player disconnects." +lobui_0811="ACUs that are at full health and shield are recalled when their player disconnects." + aisettings_0001="AIx Cheat Multiplier" aisettings_0002="Set the cheat multiplier for the cheating AIs." aisettings_0003="Cheat multiplier of %s" @@ -8194,14 +8212,6 @@ aireplace_0004="A disconnected player will cause the destruction of their units chat_send_type_title="Default recipient: allies" chat_send_type_description="When enabled, enter sends messages to allies and holding shift + enter sends to all. When not enabled, the behavior is reversed." --- unranked lobby option -lobui_0802="Unrate" -lobui_0803="Provides a toggle to unrate a game. Note that if this is set to no the game can still be unrated due to other lobby options, unrated sim mods and / or the map being unrated." -lobui_0804="No" -lobui_0805="This game will be rated if all the criteria for a rated game are met." -lobui_0806="Yes" -lobui_0807="This game will not be rated." - replay_id="Replay id" map_version="Map version" diff --git a/lua/ui/lobby/lobbyOptions.lua b/lua/ui/lobby/lobbyOptions.lua index dac9994032..b92e65a4df 100644 --- a/lua/ui/lobby/lobbyOptions.lua +++ b/lua/ui/lobby/lobbyOptions.lua @@ -29,10 +29,12 @@ ---@field RandomMap 'Off' | 'Official' | 'All' ---@field Score 'no' | 'yes' ---@field Share 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@field AbandonmentShare 'SameAsShare' | 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@field AbandonmentRecall boolean ---@field ShareUnitCap 'none' | 'allies' | 'all' ---@field Timeouts '0' | '3'| '-1' ---@field UnitCap '125' | '250' | '375' | '500' | '625' | '750' | '875' | '1000' | '1250' | '1500' ----@field UnRanked 'false' | 'true +---@field Unranked 'No' | 'Yes' ---@field Victory 'demoralization' | 'domination' | 'eradication' | 'sandbox' --- ---@field BuildMult AIMultiplierOptionValue @@ -194,43 +196,104 @@ teamOptions = ---@type ScenarioOption[] globalOpts = { { - default = 2, - label = "Share Conditions", - help = "Set what happens to a player's units when they are defeated", - key = 'Share', - values = { - { - text = "Full Share", - help = "Your units will be transferred to your highest rated ally when you die. Previously transferred units will stay where they are.", - key = 'FullShare', - }, - { - text = "Share Until Death", - help = "All units you have built this game will be destroyed when you die, except those captured by the enemy.", - key = 'ShareUntilDeath', - }, - { - text = "Partial Share", - help = "Your buildings and engineers will be transferred to your highest rated ally when you die. Your other units will be destroyed when you die, except those captured by the enemy.", - key = 'PartialShare', - }, - { - text = "Traitors", - help = "Your units will be transferred to the control of your killer.", - key = 'TransferToKiller', - }, - { - text = "Defectors", - help = "Your units will be transferred to the enemy with the highest score when you die.", - key = 'Defectors', - }, - { - text = "Civilian Desertion", - help = "Your units will be transferred to the Civilian AI, if there is one, when you die.", - key = 'CivilianDeserter', - }, - }, - }, + default = 2, + label = "Share Conditions", + help = "Set what happens to a player's units when they are defeated", + key = 'Share', + values = { + { + text = "Full Share", + help = "Your units will be transferred to your highest rated ally when you die. Previously transferred units will stay where they are.", + key = 'FullShare', + }, + { + text = "Share Until Death", + help = "All units you have built this game will be destroyed when you die, except those captured by the enemy.", + key = 'ShareUntilDeath', + }, + { + text = "Partial Share", + help = "Your buildings and engineers will be transferred to your highest rated ally when you die. Your other units will be destroyed when you die, except those captured by the enemy.", + key = 'PartialShare', + }, + { + text = "Traitors", + help = "Your units will be transferred to the control of your killer.", + key = 'TransferToKiller', + }, + { + text = "Defectors", + help = "Your units will be transferred to the enemy with the highest score when you die.", + key = 'Defectors', + }, + { + text = "Civilian Desertion", + help = "Your units will be transferred to the Civilian AI, if there is one, when you die.", + key = 'CivilianDeserter', + }, + }, + }, + { + default = 1, + label = "DC Share Conditions", + help = "Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU is at full health/shield.", + key = 'AbandonmentShare', + values = { + { + text = "Same as Share Condition", + help = "Treat disconnecting players the same as defeated players.", + key = 'SameAsShare', + }, + { + text = "Full Share", + help = "Your units will be transferred to your highest rated ally when you die. Previously transferred units will stay where they are.", + key = 'FullShare', + }, + { + text = "Share Until Death", + help = "All units you have built this game will be destroyed when you die, except those captured by the enemy.", + key = 'ShareUntilDeath', + }, + { + text = "Partial Share", + help = "Your buildings and engineers will be transferred to your highest rated ally when you die. Your other units will be destroyed when you die, except those captured by the enemy.", + key = 'PartialShare', + }, + { + text = "Traitors", + help = "Your units will be transferred to the control of your killer.", + key = 'TransferToKiller', + }, + { + text = "Defectors", + help = "Your units will be transferred to the enemy with the highest score when you die.", + key = 'Defectors', + }, + { + text = "Civilian Desertion", + help = "Your units will be transferred to the Civilian AI, if there is one, when you die.", + key = 'CivilianDeserter', + }, + }, + }, + { + default = 1, + label = "Recall Disconnected ACUs", + help = "Should disconnecting players' full health/shield ACUs be recalled, preventing their explosion?", + key = 'AbandonmentRecall', + values = { + { + text = "No", + help = "ACUs explode when their player disconnects.", + key = false, + }, + { + text = "Yes", + help = "ACUs that are at full health and shield are recalled when their player disconnects.", + key = true, + }, + }, + }, { default = 1, label = "Unrate", From 312d39da3e0d1224c8b1a74054117ac4e35cebb3 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 13 Mar 2024 00:39:17 -0700 Subject: [PATCH 06/61] Implement disconnect lobby options --- lua/aibrain.lua | 57 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 56 insertions(+), 1 deletion(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 5be78c33a6..9561fd04f0 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -18,6 +18,7 @@ local UpdateUnitCap = import("/lua/simutils.lua").UpdateUnitCap local SimPingOnArmyDefeat = import("/lua/simping.lua").OnArmyDefeat local RecallOnArmyDefeat = import("/lua/sim/Recall.lua") local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore +local FakeTeleportUnits = import("/lua/scenarioframework.lua").FakeTeleportUnits local StorageManagerBrainComponent = import("/lua/aibrains/components/StorageManagerBrainComponent.lua").StorageManagerBrainComponent local FactoryManagerBrainComponent = import("/lua/aibrains/components/FactoryManagerBrainComponent.lua").FactoryManagerBrainComponent @@ -439,6 +440,10 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM ---@param self AIBrain OnDefeat = function(self) + -- OnDefeat runs after AbandonedByPlayer, so we need to prevent killing the army twice + if self.Status == 'Defeat' then + return + end self.Status = 'Defeat' local selfIndex = self:GetArmyIndex() @@ -458,9 +463,59 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM end end, + --- Called by the engine when a player disconnects. + ---@param self AIBrain AbandonedByPlayer = function(self) if not IsGameOver() then - self:OnDefeat() + self.Status = 'Defeat' + + import("/lua/simutils.lua").UpdateUnitCap(self:GetArmyIndex()) + import("/lua/simping.lua").OnArmyDefeat(self:GetArmyIndex()) + + -- AI + if self.BrainType == 'AI' then + DisableAI(self) + end + + local shareOption = ScenarioInfo.Options.AbandonmentShare + local recallAcuOption = ScenarioInfo.Options.AbandonmentRecall + + -- Don't apply disconnect rules for players/ACUs that might be defeated soon, + -- and might have intentionally disconnected. + if recallAcuOption or shareOption ~= 'SameAsShare' then + local safeCommanders = {} + + local commanders = self:GetListOfUnits(categories.COMMAND, false) + for _, com in commanders do + if com:GetHealth() == com:GetMaxHealth() then + local comShield = com.MyShield + if comShield then + if comShield:GetHealth() == comShield:GetMaxHealth() then + table.insert(safeCommanders, com) + end + else + table.insert(safeCommanders, com) + end + end + end + + -- Only handle Assassination victory, as in other settings the player is unlikely to be defeated soon + local victoryOption = ScenarioInfo.Options.Victory + if shareOption == 'SameAsShare' or victoryOption == 'demoralization' and table.empty(safeCommanders) then + shareOption = ScenarioInfo.Options.Share + end + + if recallAcuOption then + -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. + ForkThread(FakeTeleportUnits, safeCommanders, true) + end + end + + ForkThread(KillArmy, self, shareOption) + + if self.Trash then + self.Trash:Destroy() + end end end, From f0105f01d2f310d9fd1ff22006e24e42edaf8809 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sun, 17 Mar 2024 03:11:00 -0700 Subject: [PATCH 07/61] Track ACUs last taking damage for disconnect rules --- loc/US/strings_db.lua | 6 +++--- lua/aibrain.lua | 12 +++--------- lua/sim/units/ACUUnit.lua | 14 ++++++++++++++ lua/ui/lobby/lobbyOptions.lua | 6 +++--- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/loc/US/strings_db.lua b/loc/US/strings_db.lua index e04a3b3615..f2df0eb496 100644 --- a/loc/US/strings_db.lua +++ b/loc/US/strings_db.lua @@ -7745,7 +7745,7 @@ lobui_0796="Partial Share" lobui_0797="Your buildings and engineers will be transferred to your highest rated ally when you die. Your other units will be destroyed when you die, except those captured by the enemy." lobui_0798="DC Share Conditions" -lobui_0799="Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU is at full health/shield." +lobui_0799="Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU has not been damaged in the last 2 minutes." lobui_0800="Same as Share Condition" lobui_0801="Treat disconnecting players the same as defeated players." @@ -7758,9 +7758,9 @@ lobui_0806="Yes" lobui_0807="This game will not be rated." lobui_0808="Recall Disconnected ACUs" -lobui_0809="Should disconnecting players' full health/shield ACUs be recalled, preventing their explosion?" +lobui_0809="Should disconnecting players' ACUs be recalled, preventing their explosion if they were not damaged in the last 2 minutes?" lobui_0810="ACUs explode when their player disconnects." -lobui_0811="ACUs that are at full health and shield are recalled when their player disconnects." +lobui_0811="ACUs not damaged in the last 2 minutes are recalled when their player disconnects." aisettings_0001="AIx Cheat Multiplier" aisettings_0002="Set the cheat multiplier for the cheating AIs." diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 9561fd04f0..905f50d904 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -487,15 +487,9 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) for _, com in commanders do - if com:GetHealth() == com:GetMaxHealth() then - local comShield = com.MyShield - if comShield then - if comShield:GetHealth() == comShield:GetMaxHealth() then - table.insert(safeCommanders, com) - end - else - table.insert(safeCommanders, com) - end + -- 2 minutes since last damaged + if com.LastTickDamaged == nil or com.LastTickDamaged + 1200 <= GetGameTick() then + table.insert(safeCommanders, com) end end diff --git a/lua/sim/units/ACUUnit.lua b/lua/sim/units/ACUUnit.lua index 90680f0b4e..67c3a1eb16 100644 --- a/lua/sim/units/ACUUnit.lua +++ b/lua/sim/units/ACUUnit.lua @@ -1,6 +1,7 @@ local CommandUnit = import("/lua/sim/units/commandunit.lua").CommandUnit ---@class ACUUnit : CommandUnit +---@field LastTickDamaged number ACUUnit = ClassUnit(CommandUnit) { -- The "commander under attack" warnings. ---@param self ACUUnit @@ -59,6 +60,19 @@ ACUUnit = ClassUnit(CommandUnit) { self.WeaponEnabled = {} end, + ---@param self ACUUnit + ---@param instigator Unit + ---@param amount number + ---@param vector Vector + ---@param damageType DamageType + OnDamage = function(self, instigator, amount, vector, damageType) + if self.CanTakeDamage and damageType ~= "TreeForce" and damageType ~= "TreeFire" then + self.LastTickDamaged = GetGameTick() + end + + CommandUnit.OnDamage(self, instigator, amount, vector, damageType) + end, + ---@param self ACUUnit ---@param instigator Unit ---@param amount number diff --git a/lua/ui/lobby/lobbyOptions.lua b/lua/ui/lobby/lobbyOptions.lua index b92e65a4df..7709a88779 100644 --- a/lua/ui/lobby/lobbyOptions.lua +++ b/lua/ui/lobby/lobbyOptions.lua @@ -236,7 +236,7 @@ globalOpts = { { default = 1, label = "DC Share Conditions", - help = "Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU is at full health/shield.", + help = "Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU has not been damaged in the last 2 minutes.", key = 'AbandonmentShare', values = { { @@ -279,7 +279,7 @@ globalOpts = { { default = 1, label = "Recall Disconnected ACUs", - help = "Should disconnecting players' full health/shield ACUs be recalled, preventing their explosion?", + help = "Should disconnecting players' ACUs be recalled, preventing their explosion if they were not damaged in the last 2 minutes?", key = 'AbandonmentRecall', values = { { @@ -289,7 +289,7 @@ globalOpts = { }, { text = "Yes", - help = "ACUs that are at full health and shield are recalled when their player disconnects.", + help = "ACUs not damaged in the last 2 minutes are recalled when their player disconnects.", key = true, }, }, From f89d89c97ceb2a1b8c53310bf99f338ec6c7945d Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 26 Mar 2024 01:13:38 -0700 Subject: [PATCH 08/61] Add ACU Sharing --- lua/SimUtils.lua | 138 ++++++++++++++++++++++++++-------- lua/aibrain.lua | 43 ++++++++--- lua/ui/lobby/lobbyOptions.lua | 36 +++++---- 3 files changed, 164 insertions(+), 53 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 5d33ed7c47..b68e2dfa9a 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -40,9 +40,10 @@ local sharedUnits = {} --- replaces the units with new ones) ---@param units Unit[] ---@param toArmy number ----@param captured boolean? +---@param captured? boolean +---@param noRestrictions boolean ---@return Unit[]? -function TransferUnitsOwnership(units, toArmy, captured) +function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) local toBrain = GetArmyBrain(toArmy) if not toBrain or toBrain:IsDefeated() or not units or table.empty(units) then return @@ -147,7 +148,7 @@ function TransferUnitsOwnership(units, toArmy, captured) unit.IsBeingTransferred = true -- changing owner - local newUnit = ChangeUnitArmy(unit, toArmy) + local newUnit = ChangeUnitArmy(unit, toArmy, noRestrictions or false) if not newUnit then continue end @@ -606,6 +607,9 @@ end --- Functions related to dealing with unit ownership when an army dies based on share conditions. +local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore +local FakeTeleportUnits = import("/lua/scenarioframework.lua").FakeTeleportUnits + ---@param owner number -- categoriesToKill is an optional input (it defaults to all categories) function KillSharedUnits(owner, categoriesToKill) @@ -669,6 +673,7 @@ end ---@param brains AIBrain[] ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' ---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +---@return Unit[]? function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) if not table.empty(brains) then local units @@ -681,6 +686,8 @@ function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) TransferUnfinishedUnitsAfterDeath(units, indexes) end + local totalNewUnits = {} + for k, brain in brains do if categoriesToTransfer then units = self:GetListOfUnits(categoriesToTransfer, false) @@ -688,7 +695,10 @@ function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) end if units and not table.empty(units) then - local givenUnitCount = table.getn(TransferUnitsOwnership(units, brain.index)) + local newUnits = TransferUnitsOwnership(units, brain.index, false, true) + table.destructiveCat(totalNewUnits, newUnits) + + local givenUnitCount = table.getn(newUnits) -- only show message when we actually gift that player some units if givenUnitCount > 0 then @@ -703,14 +713,40 @@ function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) WaitSeconds(1) end end + + return totalNewUnits end end +--- Returns a table of the allies and enemies of a brain, and civilians. +---@param armyIndex number +---@return { Civilians: AIBrain[], Enemies: AIBrain[], Allies: AIBrain[] } BrainCategories +function GetAllegianceCategories(armyIndex) + local BrainCategories = { Enemies = {}, Civilians = {}, Allies = {} } + + for index, brain in ArmyBrains do + brain.index = index + + if not brain:IsDefeated() and armyIndex ~= index then + if ArmyIsCivilian(index) then + table.insert(BrainCategories.Civilians, brain) + elseif IsEnemy(armyIndex, brain:GetArmyIndex()) then + table.insert(BrainCategories.Enemies, brain) + else + table.insert(BrainCategories.Allies, brain) + end + end + end + + return BrainCategories +end + --- Transfer a brain's units to other brains, sorted by positive rating and then score. ---@param self AIBrain ---@param brains AIBrain[] ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' ---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +---@return Unit[]? function TransferUnitsToHighestBrain(self, brains, shareOption, categoriesToTransfer) if not table.empty(brains) then local ratings = ScenarioInfo.Options.Ratings @@ -719,12 +755,12 @@ function TransferUnitsToHighestBrain(self, brains, shareOption, categoriesToTran brain.rating = ratings[brain.Nickname] else -- if there is no rating, create a fake negative rating based on score - brain.rating = -1 / brain.score + brain.rating = -1 / CalculateBrainScore(brain) end end -- sort brains by rating table.sort(brains, function(a, b) return a.rating > b.rating end) - TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) + return TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) end end @@ -772,7 +808,7 @@ local function ReturnBorrowedUnits(self) end for owner, units in borrowed do - TransferUnitsOwnership(units, owner) + TransferUnitsOwnership(units, owner, false, true) end WaitSeconds(1) @@ -795,7 +831,7 @@ local function GetBackUnits(selfIndex, brains) end end - TransferUnitsOwnership(given, selfIndex) + TransferUnitsOwnership(given, selfIndex, false, true) end --- Transfer units to the player who killed me @@ -807,17 +843,15 @@ local function TransferUnitsToKiller(self) if units and not table.empty(units) then if ScenarioInfo.Options.Victory == 'demoralization' then killerIndex = ArmyBrains[selfIndex].CommanderKilledBy or selfIndex - TransferUnitsOwnership(units, killerIndex) + TransferUnitsOwnership(units, killerIndex, false, true) else killerIndex = ArmyBrains[selfIndex].LastUnitKilledBy or selfIndex - TransferUnitsOwnership(units, killerIndex) + TransferUnitsOwnership(units, killerIndex, false, true) end end WaitSeconds(1) end -local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore - --- Kills an army according to the given share condition. ---@param self AIBrain ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' @@ -825,30 +859,14 @@ function KillArmy(self, shareOption) -- Kill all walls while the ACU is blowing up if shareOption == 'ShareUntilDeath' then - ForkThread(KillWalls) + ForkThread(KillWalls, self) end WaitSeconds(10) -- Wait for commander explosion, then transfer units. local selfIndex = self:GetArmyIndex() - local BrainCategories = { Enemies = {}, Civilians = {}, Allies = {} } - - -- Sort brains out into mutually exclusive categories - for index, brain in ArmyBrains do - brain.index = index - brain.score = CalculateBrainScore(brain) - - if not brain:IsDefeated() and selfIndex ~= index then - if ArmyIsCivilian(index) then - table.insert(BrainCategories.Civilians, brain) - elseif IsEnemy(selfIndex, brain:GetArmyIndex()) then - table.insert(BrainCategories.Enemies, brain) - else - table.insert(BrainCategories.Allies, brain) - end - end - end + local BrainCategories = GetAllegianceCategories(selfIndex) -- This part determines the share condition if shareOption == 'ShareUntilDeath' then @@ -860,7 +878,7 @@ function KillArmy(self, shareOption) elseif shareOption == 'PartialShare' then KillSharedUnits(selfIndex, categories.ALLUNITS - categories.STRUCTURE - categories.ENGINEER) ReturnBorrowedUnits(self) - TransferUnitsToHighestBrain(self, BrainCategories.Allies, categories.STRUCTURE + categories.ENGINEER, shareOption) + TransferUnitsToHighestBrain(self, BrainCategories.Allies, shareOption, categories.STRUCTURE + categories.ENGINEER) TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) else GetBackUnits(selfIndex, BrainCategories.Allies) @@ -886,6 +904,64 @@ function KillArmy(self, shareOption) end end +local StartCountdown = StartCountdown -- as defined in SymSync.lua + +--- When the shared ACUs die or recall after the share time expires, kills an army according to the given share condition. +---@param self AIBrain +---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@param shareTime number Game time in ticks +function KillArmyOnDelayedRecall(self, shareOption, shareTime) + -- Share units including ACUs and walls and keep track of ACUs + local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) + local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, 'FullShare', categories.ALLUNITS) + local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) + + -- create a countdown to show when the ACU recalls + for _, com in sharedCommanders do + StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) + end + + local oneComAlive = true + while GetGameTick() < shareTime and oneComAlive do + oneComAlive = false + for _, com in sharedCommanders do + if not com.Dead then + oneComAlive = true + break + end + end + WaitTicks(1) + end + + -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. + ForkThread(FakeTeleportUnits, sharedCommanders, true) + KillArmy(self, shareOption) +end + +--- When the shared ACUs die, kills an army according to the given share condition. +---@param self AIBrain +---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +function KillArmyOnACUDeath(self, shareOption) + -- Share units including ACUs and walls and keep track of ACUs + local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) + local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, 'FullShare', categories.ALLUNITS) + local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) + + local oneComAlive = true + while oneComAlive do + oneComAlive = false + for _, com in sharedCommanders do + if not com.Dead then + oneComAlive = true + break + end + end + WaitTicks(1) + end + + KillArmy(self, shareOption) +end + --#endregion local SorianUtils = import("/lua/ai/sorianutilities.lua") diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 905f50d904..0a9a387e49 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -11,6 +11,8 @@ local SUtils = import("/lua/ai/sorianutilities.lua") local TransferUnitsOwnership = import("/lua/simutils.lua").TransferUnitsOwnership local TransferUnfinishedUnitsAfterDeath = import("/lua/simutils.lua").TransferUnfinishedUnitsAfterDeath local KillArmy = import("/lua/simutils.lua").KillArmy +local KillArmyOnDelayedRecall = import("/lua/simutils.lua").KillArmyOnDelayedRecall +local KillArmyOnACUDeath = import("/lua/simutils.lua").KillArmyOnACUDeath local DisableAI = import("/lua/simutils.lua").DisableAI local TransferUnitsToBrain = import("/lua/simutils.lua").TransferUnitsToBrain local TransferUnitsToHighestBrain = import("/lua/simutils.lua").TransferUnitsToHighestBrain @@ -477,12 +479,17 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM DisableAI(self) end - local shareOption = ScenarioInfo.Options.AbandonmentShare - local recallAcuOption = ScenarioInfo.Options.AbandonmentRecall + local shareOption = ScenarioInfo.Options.DisconnectShare + local shareAcuOption = ScenarioInfo.Options.DisconnectShareCommanders + local victoryOption = ScenarioInfo.Options.Victory + + if shareOption == 'SameAsShare' then + shareOption = ScenarioInfo.Options.Share + end - -- Don't apply disconnect rules for players/ACUs that might be defeated soon, + -- Don't apply instant-effect disconnect rules for players/ACUs that might be defeated soon, -- and might have intentionally disconnected. - if recallAcuOption or shareOption ~= 'SameAsShare' then + if shareAcuOption == 'Explode' or shareAcuOption == 'Recall' then local safeCommanders = {} local commanders = self:GetListOfUnits(categories.COMMAND, false) @@ -494,18 +501,37 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM end -- Only handle Assassination victory, as in other settings the player is unlikely to be defeated soon - local victoryOption = ScenarioInfo.Options.Victory - if shareOption == 'SameAsShare' or victoryOption == 'demoralization' and table.empty(safeCommanders) then + if victoryOption == 'demoralization' and table.empty(safeCommanders) then shareOption = ScenarioInfo.Options.Share end - if recallAcuOption then + if shareAcuOption == 'Recall' then -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. ForkThread(FakeTeleportUnits, safeCommanders, true) end + + ForkThread(KillArmy, self, shareOption) + + elseif shareAcuOption == 'RecallDelayed' or shareAcuOption == 'Permanent' then + + if victoryOption ~= 'demoralization' then + shareOption = 'FullShare' + end + + if shareAcuOption == 'RecallDelayed' then + local shareTime = GetGameTick() + 1200 + if shareTime < 3000 then + shareTime = 3000 + end + ForkThread(KillArmyOnDelayedRecall, self, shareOption, shareTime) + else + ForkThread(KillArmyOnACUDeath, self, shareOption) + end + else + WARN('Invalid disconnection ACU share condition was used for this game. Defaulting to exploding ACU.') + ForkThread(KillArmy, self, shareOption) end - ForkThread(KillArmy, self, shareOption) if self.Trash then self.Trash:Destroy() @@ -546,7 +572,6 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM -- Sort brains out into mutually exclusive categories for index, brain in ArmyBrains do brain.index = index - brain.score = CalculateBrainScore(brain) if not brain:IsDefeated() and selfIndex ~= index then if ArmyIsCivilian(index) then diff --git a/lua/ui/lobby/lobbyOptions.lua b/lua/ui/lobby/lobbyOptions.lua index 7709a88779..6c9f1a0362 100644 --- a/lua/ui/lobby/lobbyOptions.lua +++ b/lua/ui/lobby/lobbyOptions.lua @@ -29,8 +29,8 @@ ---@field RandomMap 'Off' | 'Official' | 'All' ---@field Score 'no' | 'yes' ---@field Share 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' ----@field AbandonmentShare 'SameAsShare' | 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' ----@field AbandonmentRecall boolean +---@field DisconnectShare 'SameAsShare' | 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@field DisconnectShareCommanders 'Explode' | 'Recall' | 'RecallDelayed' | 'Permanent' ---@field ShareUnitCap 'none' | 'allies' | 'all' ---@field Timeouts '0' | '3'| '-1' ---@field UnitCap '125' | '250' | '375' | '500' | '625' | '750' | '875' | '1000' | '1250' | '1500' @@ -236,8 +236,8 @@ globalOpts = { { default = 1, label = "DC Share Conditions", - help = "Set what happens to a player's units when they disconnect. In Assassination, only applies if an ACU has not been damaged in the last 2 minutes.", - key = 'AbandonmentShare', + help = "Set what happens to a player's units when they disconnect.", + key = 'DisconnectShare', values = { { text = "Same as Share Condition", @@ -278,19 +278,29 @@ globalOpts = { }, { default = 1, - label = "Recall Disconnected ACUs", - help = "Should disconnecting players' ACUs be recalled, preventing their explosion if they were not damaged in the last 2 minutes?", - key = 'AbandonmentRecall', + label = "DC ACU Share Conditions", + help = "Set what happens to a player's ACU when they disconnect.", + key = 'DisconnectShareCommanders', values = { { - text = "No", - help = "ACUs explode when their player disconnects.", - key = false, + text = "Explode", + help = "ACUs explode when their player disconnects. In Assassination, the DC share condition is applied only if they have not been damaged in the last 2 minutes.", + key = 'Explode', }, { - text = "Yes", - help = "ACUs not damaged in the last 2 minutes are recalled when their player disconnects.", - key = true, + text = "Recall", + help = "ACUs not damaged in the last 2 minutes are recalled when their player disconnects. In Assassination, the DC share condition is applied only if they have not been damaged in the last 2 minutes.", + key = 'Recall', + }, + { + text = "Delayed Recall", + help = "Disconnected ACUs are shared to allies for 2 minutes or until 5 minutes into the match before it recalls. In Assassination, the DC share condition is applied to that player's units when the shared ACU dies or recalls.", + key = 'RecallDelayed', + }, + { + text = "Permanent", + help = "Disconnected ACUs are permanently shared to allies. In Assassination, the DC share condition is applied to that player's units when the shared ACU dies.", + key = 'Permanent', }, }, }, From 1c804bb0ebda4caa8a6d133b4c8956636d8561ad Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:13:43 -0700 Subject: [PATCH 09/61] Improve annotations --- lua/SimUtils.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index b68e2dfa9a..069fd80a5d 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -41,7 +41,7 @@ local sharedUnits = {} ---@param units Unit[] ---@param toArmy number ---@param captured? boolean ----@param noRestrictions boolean +---@param noRestrictions? boolean ---@return Unit[]? function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) local toBrain = GetArmyBrain(toArmy) @@ -776,8 +776,8 @@ local function KillWalls(self) end end ---- Remove the borrowed status from units we lent to allies. ----@param brains AIBrain[] +--- Remove the borrowed status from units we lent to a set of `brains`. +---@param brains AIBrain[] Usually our allies ---@param selfIndex number local function TransferOwnershipOfBorrowedUnits(brains, selfIndex) for index, brain in brains do @@ -852,7 +852,7 @@ local function TransferUnitsToKiller(self) WaitSeconds(1) end ---- Kills an army according to the given share condition. +--- Kills my army according to the given share condition. ---@param self AIBrain ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' function KillArmy(self, shareOption) @@ -906,7 +906,7 @@ end local StartCountdown = StartCountdown -- as defined in SymSync.lua ---- When the shared ACUs die or recall after the share time expires, kills an army according to the given share condition. +--- Shares all units including ACUs. When the shared ACUs die or recall after `shareTime`, kills my army according to the given share condition. ---@param self AIBrain ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' ---@param shareTime number Game time in ticks @@ -916,7 +916,7 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, 'FullShare', categories.ALLUNITS) local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) - -- create a countdown to show when the ACU recalls + -- create a countdown to show when the ACU recalls (similar to timed self-destruct) for _, com in sharedCommanders do StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) end @@ -938,7 +938,7 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) KillArmy(self, shareOption) end ---- When the shared ACUs die, kills an army according to the given share condition. +--- Shares all units including ACUs. When the shared ACUs die, kills my army according to the given share condition. ---@param self AIBrain ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' function KillArmyOnACUDeath(self, shareOption) From a6e015cc796a986310834e800e260a15e3f97116 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 1 Apr 2024 16:20:05 -0700 Subject: [PATCH 10/61] Implement sending a reason for units being shared Refactor parameter for transfer of unfinished units --- lua/SimUtils.lua | 32 +++++++++++++++++--------------- lua/aibrain.lua | 4 ++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 069fd80a5d..eacf426207 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -671,13 +671,14 @@ end --- Transfer a brain's units to other brains. ---@param self AIBrain ---@param brains AIBrain[] ----@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@param transferUnfinishedUnits boolean ---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +---@param reason? string # Defaults to "FullShare" ---@return Unit[]? -function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) +function TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason) if not table.empty(brains) then local units - if shareOption == 'FullShare' then + if transferUnfinishedUnits then local indexes = {} for _, brain in brains do table.insert(indexes, brain.index) @@ -705,7 +706,7 @@ function TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) Sync.ArmyTransfer = { { from = self.index, to = brain.index, - reason = "fullshare" + reason = reason or "FullShare" } } end @@ -744,10 +745,11 @@ end --- Transfer a brain's units to other brains, sorted by positive rating and then score. ---@param self AIBrain ---@param brains AIBrain[] ----@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' +---@param transferUnfinishedUnits boolean ---@param categoriesToTransfer? EntityCategory # Defaults to ALLUNITS - WALL - COMMAND +---@param reason? string Usually 'FullShare' ---@return Unit[]? -function TransferUnitsToHighestBrain(self, brains, shareOption, categoriesToTransfer) +function TransferUnitsToHighestBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason) if not table.empty(brains) then local ratings = ScenarioInfo.Options.Ratings for _, brain in brains do @@ -760,7 +762,7 @@ function TransferUnitsToHighestBrain(self, brains, shareOption, categoriesToTran end -- sort brains by rating table.sort(brains, function(a, b) return a.rating > b.rating end) - return TransferUnitsToBrain(self, brains, shareOption, categoriesToTransfer) + return TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason) end end @@ -843,10 +845,10 @@ local function TransferUnitsToKiller(self) if units and not table.empty(units) then if ScenarioInfo.Options.Victory == 'demoralization' then killerIndex = ArmyBrains[selfIndex].CommanderKilledBy or selfIndex - TransferUnitsOwnership(units, killerIndex, false, true) + TransferUnitsToBrain(self, { ArmyBrains[killerIndex] }, true, nil, "TransferToKiller") else killerIndex = ArmyBrains[selfIndex].LastUnitKilledBy or selfIndex - TransferUnitsOwnership(units, killerIndex, false, true) + TransferUnitsToBrain(self, { ArmyBrains[killerIndex] }, true, nil, "TransferToKiller") end end WaitSeconds(1) @@ -873,21 +875,21 @@ function KillArmy(self, shareOption) KillSharedUnits(selfIndex) ReturnBorrowedUnits(self) elseif shareOption == 'FullShare' then - TransferUnitsToHighestBrain(self, BrainCategories.Allies, shareOption) + TransferUnitsToHighestBrain(self, BrainCategories.Allies, true, nil, "FullShare") TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) elseif shareOption == 'PartialShare' then KillSharedUnits(selfIndex, categories.ALLUNITS - categories.STRUCTURE - categories.ENGINEER) ReturnBorrowedUnits(self) - TransferUnitsToHighestBrain(self, BrainCategories.Allies, shareOption, categories.STRUCTURE + categories.ENGINEER) + TransferUnitsToHighestBrain(self, BrainCategories.Allies, true, categories.STRUCTURE + categories.ENGINEER, "PartialShare") TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) else GetBackUnits(selfIndex, BrainCategories.Allies) if shareOption == 'CivilianDeserter' then - TransferUnitsToBrain(self, BrainCategories.Civilians, shareOption) + TransferUnitsToBrain(self, BrainCategories.Civilians, true) elseif shareOption == 'TransferToKiller' then TransferUnitsToKiller(self) elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(self, BrainCategories.Enemies, shareOption) + TransferUnitsToHighestBrain(self, BrainCategories.Enemies, true, "Defectors") else -- Something went wrong in settings. Act like share until death to avoid abuse WARN('Invalid share condition was used for this game. Defaulting to killing all units') KillSharedUnits(selfIndex) @@ -913,7 +915,7 @@ local StartCountdown = StartCountdown -- as defined in SymSync.lua function KillArmyOnDelayedRecall(self, shareOption, shareTime) -- Share units including ACUs and walls and keep track of ACUs local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) - local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, 'FullShare', categories.ALLUNITS) + local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectShareTemporary") local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) -- create a countdown to show when the ACU recalls (similar to timed self-destruct) @@ -944,7 +946,7 @@ end function KillArmyOnACUDeath(self, shareOption) -- Share units including ACUs and walls and keep track of ACUs local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) - local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, 'FullShare', categories.ALLUNITS) + local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectSharePermanent") local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) local oneComAlive = true diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 0a9a387e49..6e7d01617e 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -587,9 +587,9 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local recallCat = categories.ALLUNITS - categories.WALL - categories.COMMAND - categories.SUBCOMMANDER local shareOption = ScenarioInfo.Options.Share if shareOption == 'CivilianDeserter' then - TransferUnitsToBrain(self, civilians, shareOption, recallCat) + TransferUnitsToBrain(self, civilians, false, recallCat, "CivilianDeserter") elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(self, enemies, shareOption, recallCat) + TransferUnitsToHighestBrain(self, enemies, false, recallCat, "Defectors") end -- let the average, team vs team game end first From a86a62b4d49857600501d15df4d4a382396a0155 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 27 May 2024 18:16:24 -0700 Subject: [PATCH 11/61] Add outside 8-way positioning methods --- lua/maui/layouthelpers.lua | 55 +++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/lua/maui/layouthelpers.lua b/lua/maui/layouthelpers.lua index cd035e7e46..88c717dcbf 100644 --- a/lua/maui/layouthelpers.lua +++ b/lua/maui/layouthelpers.lua @@ -1733,9 +1733,62 @@ local LayouterAttributeControl = ClassSimple { ---------- - -- Outside edge positioning methods + -- Outside 8-way positioning methods ---------- + --- Lock bottom right corner of the control to the top left corner of a parent. + ---@generic T : LayouterAttributeControl + ---@param self T + ---@param parent Layoutable + ---@param bottomOffset? number offset of the control's bottom edge in the upward direction, scaled by the pixel scale factor + ---@param rightOffset? number offset of the control's right edge in the leftward direction, scaled by the pixel scale factor + ---@return T + TopLeftOf = function(self, parent, bottomOffset, rightOffset) + AnchorToTop(self.layoutControl, GetLayoutControl(parent), bottomOffset) + AnchorToLeft(self.layoutControl, GetLayoutControl(parent), rightOffset) + return self + end; + + --- Lock bottom left corner of the control to the top right corner of a parent. + ---@generic T : LayouterAttributeControl + ---@param self T + ---@param parent Layoutable + ---@param bottomOffset? number offset of the control's bottom edge in the upward direction, scaled by the pixel scale factor + ---@param leftOffset? number offset of the control's left edge in the rightward direction, scaled by the pixel scale factor + ---@return T + TopRightOf = function(self, parent, bottomOffset, leftOffset) + AnchorToTop(self.layoutControl, GetLayoutControl(parent), bottomOffset) + AnchorToRight(self.layoutControl, GetLayoutControl(parent), leftOffset) + return self + end; + + --- Lock bottom right corner of the control to the bottom left corner of a parent. + ---@generic T : LayouterAttributeControl + ---@param self T + ---@param parent Layoutable + ---@param topOffset? number offset of the control's top edge in the downward direction, scaled by the pixel scale factor + ---@param rightOffset? number offset of the control's right edge in the leftward direction, scaled by the pixel scale factor + ---@return T + BottomLeftOf = function(self, parent, topOffset, rightOffset) + AnchorToBottom(self.layoutControl, GetLayoutControl(parent), topOffset) + AnchorToLeft(self.layoutControl, GetLayoutControl(parent), rightOffset) + return self + end; + + --- Lock top left corner of the control to the bottom right corner of a parent. + ---@generic T : LayouterAttributeControl + ---@param self T + ---@param parent Layoutable + ---@param topOffset? number offset of the control's top edge in the downward direction, scaled by the pixel scale factor + ---@param leftOffset? number offset of the control's left edge in the rightward direction, scaled by the pixel scale factor + ---@return T + BottomRightOf = function(self, parent, topOffset, leftOffset) + AnchorToBottom(self.layoutControl, GetLayoutControl(parent), topOffset) + AnchorToRight(self.layoutControl, GetLayoutControl(parent), leftOffset) + return self + end; + + --- Lock right edge of the control to the left edge of a parent, centered vertically. --- This sets the control's right and top edges. ---@generic T : LayouterAttributeControl From a0aec7c43a46e0916589a662b16049c66bc347e4 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 30 May 2024 16:40:46 -0700 Subject: [PATCH 12/61] Refactor announcement.lua using layouter --- lua/ui/game/announcement.lua | 206 +++++++++++++++++--------------- lua/ui/game/announcementNew.lua | 197 ++++++++++++++++++++++++++++++ 2 files changed, 308 insertions(+), 95 deletions(-) create mode 100644 lua/ui/game/announcementNew.lua diff --git a/lua/ui/game/announcement.lua b/lua/ui/game/announcement.lua index 5af3c43bc3..f77084ba70 100644 --- a/lua/ui/game/announcement.lua +++ b/lua/ui/game/announcement.lua @@ -5,15 +5,34 @@ --* --* Copyright � 2007 Gas Powered Games, Inc. All rights reserved. --***************************************************************************** +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") +local Layouter = LayoutHelpers.ReusedLayoutFor local UIUtil = import("/lua/ui/uiutil.lua") -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") local Group = import("/lua/maui/group.lua").Group local Bitmap = import("/lua/maui/bitmap.lua").Bitmap +local MATH_Lerp = MATH_Lerp + local bg = false +--- Create an announcement UI for sending general messages to the user +---@param text string +---@param goalControl? Control The control where the announcement appears out of. +---@param secondaryText? string +---@param onFinished? function function CreateAnnouncement(text, goalControl, secondaryText, onFinished) + local frame = GetFrame(0) + + if not goalControl then + -- make it originate from the top + goalControl = Group(frame) + goalControl.Left:Set(function() return frame.Left() + 0.49 * frame.Right() end) + goalControl.Right:Set(function() return frame.Left() + 0.51 * frame.Right() end) + goalControl.Top = frame.Top + goalControl.Bottom = frame.Top + end + local scoreDlg = import("/lua/ui/dialogs/score.lua") if scoreDlg.dialog then if onFinished then @@ -21,6 +40,7 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) end return end + if bg then if bg.OnFinished then bg.OnFinished() @@ -38,85 +58,94 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) end return end - bg = Bitmap(GetFrame(0), UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_m.dds')) - bg.Height:Set(0) - bg.Width:Set(0) - bg.Depth:Set(GetFrame(0):GetTopmostDepth()+1) - bg.border = CreateBorder(bg) PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Open'})) - local textGroup = Group(bg) + + bg = Layouter(Bitmap(frame, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_m.dds'))) + :Height(0):Width(0):Over(frame, 1) + + local textGroup = Group(bg:Get()) + if goalControl == nil then goalControl = textGroup end - LayoutHelpers.AtCenterIn(bg, goalControl) - local text = UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont) - LayoutHelpers.AtCenterIn(text, GetFrame(0), -250) - text:SetDropShadow(true) - text:SetColor(UIUtil.fontColor) - text:SetNeedsFrameUpdate(true) + bg = bg:AtCenterIn(goalControl):End() + + bg.border = CreateBorder(bg) + + local text = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) + :AtCenterIn(frame, -250) + :DropShadow(true):Color(UIUtil.fontColor) + :NeedsFrameUpdate(true):End() if secondaryText then - secText = UIUtil.CreateText(textGroup, secondaryText, 18, UIUtil.bodyFont) - secText:SetDropShadow(true) - secText:SetColor(UIUtil.fontColor) - LayoutHelpers.Below(secText, text, 10) - LayoutHelpers.AtHorizontalCenterIn(secText, text) - textGroup.Top:Set(text.Top) - textGroup.Left:Set(function() return math.min(secText.Left(), text.Left()) end) - textGroup.Right:Set(function() return math.max(secText.Right(), text.Right()) end) - textGroup.Bottom:Set(secText.Bottom) + local secText = Layouter(UIUtil.CreateText(textGroup, secondaryText, 18, UIUtil.bodyFont)) + :DropShadow(true):Color(UIUtil.fontColor) + :Below(text, 10):AtHorizontalCenterIn(text):End() + Layouter(textGroup):Top(text.Top) + :Left(function() return math.min(secText.Left(), text.Left()) end) + :Right(function() return math.max(secText.Right(), text.Right()) end) + :Bottom(secText.Bottom):End() else LayoutHelpers.FillParent(textGroup, text) end + bg:DisableHitTest(true) + textGroup:SetAlpha(0, true) - bg:DisableHitTest(true) + bg:SetNeedsFrameUpdate(true) bg.OnFinished = onFinished - bg.time = 0 - bg:SetNeedsFrameUpdate(true) - bg.CloseSoundPlayed = false + local tGTop, tGLeft, tGRight, tGBottom, tGHeight, tGWidth = textGroup.Top(), textGroup.Left(), textGroup.Right(), textGroup.Bottom(), textGroup.Height(), textGroup.Width() + local gCTop, gCLeft, gCRight, gCBottom, gCHeight, gCWidth = goalControl.Top(), goalControl.Left(), goalControl.Right(), goalControl.Bottom(), goalControl.Height(), goalControl.Width() + bg.OnFrame = function(self, delta) - self.time = self.time + delta - if self.time >= 3.5 and self.time < 3.7 then + local time = self.time + delta + self.time = time + + -- expansion animation + if time < .2 then + local lerpMult = MATH_Lerp(time, 0, 0.2, 0, 1) + self.Top:Set(MATH_Lerp(lerpMult, gCTop, tGTop)) + self.Left:Set(MATH_Lerp(lerpMult, gCLeft, tGLeft)) + self.Right:Set(MATH_Lerp(lerpMult, gCRight, tGRight)) + self.Bottom:Set(MATH_Lerp(lerpMult, gCBottom, tGBottom)) + self.Height:Set(MATH_Lerp(lerpMult, gCHeight, tGHeight)) + self.Width:Set(MATH_Lerp(lerpMult, gCWidth, tGWidth)) + -- stationary + elseif time > .2 and time < 3.5 and not self.TextGroupReached then + Layouter(self):Fill(textGroup):End() + self.TextGroupReached = true + -- contraction animation + elseif time >= 3.5 and time < 3.7 then if not self.CloseSoundPlayed then PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Close'})) - self.CloseSoundPlayed = false + self.CloseSoundPlayed = true end - self.Top:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Top(), goalControl.Top())) - self.Left:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Left(), goalControl.Left())) - self.Right:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Right(), goalControl.Right())) - self.Bottom:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Bottom(), goalControl.Bottom())) - self.Height:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Height(), goalControl.Height())) - self.Width:Set(MATH_Lerp(self.time, 3.5, 3.7, textGroup.Width(), goalControl.Width())) - elseif self.time < .2 then - self.Top:Set(MATH_Lerp(self.time, 0, .2, goalControl.Top(), textGroup.Top())) - self.Left:Set(MATH_Lerp(self.time, 0, .2, goalControl.Left(), textGroup.Left())) - self.Right:Set(MATH_Lerp(self.time, 0, .2, goalControl.Right(), textGroup.Right())) - self.Bottom:Set(MATH_Lerp(self.time, 0, .2, goalControl.Bottom(), textGroup.Bottom())) - self.Height:Set(MATH_Lerp(self.time, 0, .2, goalControl.Height(), textGroup.Height())) - self.Width:Set(MATH_Lerp(self.time, 0, .2, goalControl.Width(), textGroup.Width())) - elseif self.time > .2 and self.time < 3.5 then - self.Top:Set(textGroup.Top) - self.Left:Set(textGroup.Left) - self.Right:Set(textGroup.Right) - self.Bottom:Set(textGroup.Bottom) - self.Height:Set(textGroup.Height) - self.Width:Set(textGroup.Width) + local lerpMult = MATH_Lerp(time, 3.5, 3.7, 0, 1) + self.Top:Set(MATH_Lerp(lerpMult, tGTop, gCTop)) + self.Left:Set(MATH_Lerp(lerpMult, tGLeft, gCLeft)) + self.Right:Set(MATH_Lerp(lerpMult, tGRight, gCRight)) + self.Bottom:Set(MATH_Lerp(lerpMult, tGBottom, gCBottom)) + self.Height:Set(MATH_Lerp(lerpMult, tGHeight, gCHeight)) + self.Width:Set(MATH_Lerp(lerpMult, tGWidth, gCWidth)) end - if self.time > 3 and textGroup:GetAlpha() != 0 then - textGroup:SetAlpha(math.max(textGroup:GetAlpha()-(delta*2), 0), true) - elseif self.time > .2 and self.time < 3 and text:GetAlpha() != 1 then - textGroup:SetAlpha(math.min(text:GetAlpha()+(delta*2), 1), true) + local textGroupAlpha = textGroup:GetAlpha() + local textAlpha = text:GetAlpha() + -- fade out the text at the end of the announcement + if time > 3 and textGroupAlpha ~= 0 then + textGroup:SetAlpha(math.max(textGroupAlpha - (delta * 2), 0), true) + -- fade in the text when the announcement appears + elseif time > .2 and time < 3 and textAlpha ~= 1 then + textGroup:SetAlpha(math.min(textAlpha + (delta * 2), 1), true) end if goalControl == textGroup then - self:SetAlpha(textGroup:GetAlpha(), true) + self:SetAlpha(textGroupAlpha, true) end - if self.time > 3.7 then + if time > 3.7 then if bg.OnFinished then bg.OnFinished() end @@ -125,62 +154,49 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) bg = false end end + if import("/lua/ui/game/gamemain.lua").gameUIHidden then bg:Hide() end end +--- Instantly hides the current announcement function Contract() if bg then bg:Hide() end end +--- Instantly shows the current announcement function Expand() if bg then bg:Show() end end +--- Create a border around the `parent` with the `filter-ping-list-panel` files +---@param parent Control +---@return Bitmap[] border # 8 Bitmap objects: top left, top middle, top right, middle left, middle right, bottom left, bottom middle, bottom right function CreateBorder(parent) - local border = {} - - border.tl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ul.dds')) - border.tm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_horz_um.dds')) - border.tr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ur.dds')) - border.ml = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_l.dds')) - border.mr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_r.dds')) - border.bl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ll.dds')) - border.bm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lm.dds')) - border.br = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lr.dds')) - - border.tl.Bottom:Set(parent.Top) - border.tl.Right:Set(parent.Left) - - border.bl.Top:Set(parent.Bottom) - border.bl.Right:Set(parent.Left) - - border.tr.Bottom:Set(parent.Top) - border.tr.Left:Set(parent.Right) - - border.br.Top:Set(parent.Bottom) - border.br.Left:Set(parent.Right) - - border.tm.Bottom:Set(parent.Top) - border.tm.Left:Set(parent.Left) - border.tm.Right:Set(parent.Right) - - border.bm.Top:Set(parent.Bottom) - border.bm.Left:Set(parent.Left) - border.bm.Right:Set(parent.Right) - - border.ml.Top:Set(parent.Top) - border.ml.Bottom:Set(parent.Bottom) - border.ml.Right:Set(parent.Left) - - border.mr.Top:Set(parent.Top) - border.mr.Bottom:Set(parent.Bottom) - border.mr.Left:Set(parent.Right) - - return border + -- t, m, b = top, middle, bottm + -- l, m, r = left, middle, right + local tl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ul.dds')) + local tm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_horz_um.dds')) + local tr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ur.dds')) + local ml = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_l.dds')) + local mr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_r.dds')) + local bl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ll.dds')) + local bm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lm.dds')) + local br = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lr.dds')) + + Layouter(tl):TopLeftOf(parent):End() + Layouter(tm):CenteredAbove(parent):FillHorizontally(parent):End() + Layouter(tr):TopRightOf(parent):End() + Layouter(ml):CenteredLeftOf(parent):FillVertically(parent):End() + Layouter(mr):CenteredRightOf(parent):FillVertically(parent):End() + Layouter(bl):BottomLeftOf(parent):End() + Layouter(bm):CenteredBelow(parent):FillHorizontally(parent):End() + Layouter(br):BottomRightOf(parent):End() + + return { tl, tm, tr, ml, mr, bl, bm, br } end \ No newline at end of file diff --git a/lua/ui/game/announcementNew.lua b/lua/ui/game/announcementNew.lua new file mode 100644 index 0000000000..dfa9f35d9f --- /dev/null +++ b/lua/ui/game/announcementNew.lua @@ -0,0 +1,197 @@ +local LayoutHelpers = import("/lua/maui/layouthelpers.lua") +local Layouter = LayoutHelpers.ReusedLayoutFor + +local UIUtil = import("/lua/ui/uiutil.lua") +local Group = import("/lua/maui/group.lua").Group +local Bitmap = import("/lua/maui/bitmap.lua").Bitmap + +local MATH_Lerp = MATH_Lerp + +local bg = false + +--- Create an announcement UI for sending general messages to the user +---@param text string +---@param goalControl? Control The control where the announcement appears out of. +---@param secondaryText? string +---@param onFinished? function +function CreateAnnouncement(text, goalControl, secondaryText, onFinished) + local frame = GetFrame(0) + + if not goalControl then + -- make it originate from the top + goalControl = Group(frame) + goalControl.Left:Set(function() return frame.Left() + 0.49 * frame.Right() end) + goalControl.Right:Set(function() return frame.Left() + 0.51 * frame.Right() end) + goalControl.Top = frame.Top + goalControl.Bottom = frame.Top + end + + local scoreDlg = import("/lua/ui/dialogs/score.lua") + if scoreDlg.dialog then + if onFinished then + onFinished() + end + return + end + + if bg then + if bg.OnFinished then + bg.OnFinished() + end + bg.OnFrame = function(self, delta) + local newAlpha = self:GetAlpha() - (delta*2) + if newAlpha < 0 then + newAlpha = 0 + self:Destroy() + bg.OnFinished = nil + bg = false + CreateAnnouncement(text, goalControl, secondaryText, onFinished) + end + self:SetAlpha(newAlpha, true) + end + return + end + + PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Open'})) + + bg = Layouter(Bitmap(frame, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_m.dds'))) + :Height(0):Width(0):Over(frame, 1) + + local textGroup = Group(bg:Get()) + + if goalControl == nil then + goalControl = textGroup + end + bg = bg:AtCenterIn(goalControl):End() + + bg.border = CreateBorder(bg) + + local text = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) + :AtCenterIn(frame, -250) + :DropShadow(true):Color(UIUtil.fontColor) + :NeedsFrameUpdate(true):End() + + if secondaryText then + local secText = Layouter(UIUtil.CreateText(textGroup, secondaryText, 18, UIUtil.bodyFont)) + :DropShadow(true):Color(UIUtil.fontColor) + :Below(text, 10):AtHorizontalCenterIn(text):End() + Layouter(textGroup):Top(text.Top) + :Left(function() return math.min(secText.Left(), text.Left()) end) + :Right(function() return math.max(secText.Right(), text.Right()) end) + :Bottom(secText.Bottom):End() + else + LayoutHelpers.FillParent(textGroup, text) + end + bg:DisableHitTest(true) + + textGroup:SetAlpha(0, true) + + bg.OnFinished = onFinished + + bg.time = 0 + bg:SetNeedsFrameUpdate(true) + bg.CloseSoundPlayed = false + + local tGTop, tGLeft, tGRight, tGBottom, tGHeight, tGWidth = textGroup.Top(), textGroup.Left(), textGroup.Right(), textGroup.Bottom(), textGroup.Height(), textGroup.Width() + local gCTop, gCLeft, gCRight, gCBottom, gCHeight, gCWidth = goalControl.Top(), goalControl.Left(), goalControl.Right(), goalControl.Bottom(), goalControl.Height(), goalControl.Width() + + bg.OnFrame = function(self, delta) + local time = self.time + delta + self.time = time + + -- expansion animation + if time < .2 then + local lerpMult = MATH_Lerp(time, 0, 0.2, 0, 1) + self.Top:Set(MATH_Lerp(lerpMult, gCTop, tGTop)) + self.Left:Set(MATH_Lerp(lerpMult, gCLeft, tGLeft)) + self.Right:Set(MATH_Lerp(lerpMult, gCRight, tGRight)) + self.Bottom:Set(MATH_Lerp(lerpMult, gCBottom, tGBottom)) + self.Height:Set(MATH_Lerp(lerpMult, gCHeight, tGHeight)) + self.Width:Set(MATH_Lerp(lerpMult, gCWidth, tGWidth)) + -- stationary + elseif time > .2 and time < 3.5 and not self.TextGroupReached then + Layouter(self):Fill(textGroup):End() + self.TextGroupReached = true + -- contraction animation + elseif time >= 3.5 and time < 3.7 then + if not self.CloseSoundPlayed then + PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Close'})) + self.CloseSoundPlayed = true + end + local lerpMult = MATH_Lerp(time, 3.5, 3.7, 0, 1) + self.Top:Set(MATH_Lerp(lerpMult, tGTop, gCTop)) + self.Left:Set(MATH_Lerp(lerpMult, tGLeft, gCLeft)) + self.Right:Set(MATH_Lerp(lerpMult, tGRight, gCRight)) + self.Bottom:Set(MATH_Lerp(lerpMult, tGBottom, gCBottom)) + self.Height:Set(MATH_Lerp(lerpMult, tGHeight, gCHeight)) + self.Width:Set(MATH_Lerp(lerpMult, tGWidth, gCWidth)) + end + + local textGroupAlpha = textGroup:GetAlpha() + local textAlpha = text:GetAlpha() + -- fade out the text at the end of the announcement + if time > 3 and textGroupAlpha ~= 0 then + textGroup:SetAlpha(math.max(textGroupAlpha - (delta * 2), 0), true) + -- fade in the text when the announcement appears + elseif time > .2 and time < 3 and textAlpha ~= 1 then + textGroup:SetAlpha(math.min(textAlpha + (delta * 2), 1), true) + end + if goalControl == textGroup then + self:SetAlpha(textGroupAlpha, true) + end + + if time > 3.7 then + if bg.OnFinished then + bg.OnFinished() + end + bg:Destroy() + bg.OnFinished = nil + bg = false + end + end + + if import("/lua/ui/game/gamemain.lua").gameUIHidden then + bg:Hide() + end +end + +--- Instantly hides the current announcement +function Contract() + if bg then + bg:Hide() + end +end + +--- Instantly shows the current announcement +function Expand() + if bg then + bg:Show() + end +end + +--- Create a border around the `parent` with the `filter-ping-list-panel` files +---@param parent Control +---@return Bitmap[] border # 8 Bitmap objects: top left, top middle, top right, middle left, middle right, bottom left, bottom middle, bottom right +function CreateBorder(parent) + -- t, m, b = top, middle, bottm + -- l, m, r = left, middle, right + local tl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ul.dds')) + local tm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_horz_um.dds')) + local tr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ur.dds')) + local ml = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_l.dds')) + local mr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_r.dds')) + local bl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ll.dds')) + local bm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lm.dds')) + local br = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lr.dds')) + + Layouter(tl):TopLeftOf(parent):End() + Layouter(tm):CenteredAbove(parent):FillHorizontally(parent):End() + Layouter(tr):TopRightOf(parent):End() + Layouter(ml):CenteredLeftOf(parent):FillVertically(parent):End() + Layouter(mr):CenteredRightOf(parent):FillVertically(parent):End() + Layouter(bl):BottomLeftOf(parent):End() + Layouter(bm):CenteredBelow(parent):FillHorizontally(parent):End() + Layouter(br):BottomRightOf(parent):End() + + return { tl, tm, tr, ml, mr, bl, bm, br } +end \ No newline at end of file From d41189fed78b9d8119f959d7736bdfc18217b18b Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 30 May 2024 16:49:47 -0700 Subject: [PATCH 13/61] Get rid of goalControl == textGroup case it is impossible because GoalControl cannot be nil due to its creation in the beginning of the function if it is nil --- lua/ui/game/announcement.lua | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/lua/ui/game/announcement.lua b/lua/ui/game/announcement.lua index f77084ba70..44e192600b 100644 --- a/lua/ui/game/announcement.lua +++ b/lua/ui/game/announcement.lua @@ -62,17 +62,11 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Open'})) bg = Layouter(Bitmap(frame, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_m.dds'))) - :Height(0):Width(0):Over(frame, 1) - - local textGroup = Group(bg:Get()) - - if goalControl == nil then - goalControl = textGroup - end - bg = bg:AtCenterIn(goalControl):End() - + :Height(0):Width(0):Over(frame, 1):AtCenterIn(goalControl):End() bg.border = CreateBorder(bg) + local textGroup = Group(bg) + local text = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) :AtCenterIn(frame, -250) :DropShadow(true):Color(UIUtil.fontColor) @@ -141,9 +135,6 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) elseif time > .2 and time < 3 and textAlpha ~= 1 then textGroup:SetAlpha(math.min(textAlpha + (delta * 2), 1), true) end - if goalControl == textGroup then - self:SetAlpha(textGroupAlpha, true) - end if time > 3.7 then if bg.OnFinished then From 1aca9724a0a90d4a2c95db91b28440bc93f03868 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:44:59 -0700 Subject: [PATCH 14/61] Optimize imports of LazyVar in UI classes --- lua/maui/bitmap.lua | 6 +++--- lua/maui/border.lua | 15 +++++++-------- lua/maui/edit.lua | 14 +++++++------- lua/maui/itemlist.lua | 16 ++++++++-------- lua/maui/scrollbar.lua | 11 ++++++----- lua/maui/text.lua | 6 +++--- 6 files changed, 34 insertions(+), 34 deletions(-) diff --git a/lua/maui/bitmap.lua b/lua/maui/bitmap.lua index b0e23a25bf..503642a67c 100644 --- a/lua/maui/bitmap.lua +++ b/lua/maui/bitmap.lua @@ -29,6 +29,7 @@ local Control = import("/lua/maui/control.lua").Control local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@class BitmapTexture ---@field _texture LazyVar @@ -48,9 +49,8 @@ Bitmap = ClassUI(moho.bitmap_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua") - self._filename = {_texture = LazyVar.Create(), _border = 1} - self._color = LazyVar.Create() + self._filename = {_texture = LazyVarCreate(), _border = 1} + self._color = LazyVarCreate() self._color.OnDirty = function(var) self:InternalSetSolidColor(self._color()) end diff --git a/lua/maui/border.lua b/lua/maui/border.lua index 77f6fe86f0..40b9e5467e 100644 --- a/lua/maui/border.lua +++ b/lua/maui/border.lua @@ -13,7 +13,7 @@ -- border textures. local Control = import("/lua/maui/control.lua").Control - +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@class MauiBorder : moho.border_methods, Control, InternalObject ---@field BorderWidth LazyVar ---@field BorderHeight LazyVar @@ -25,33 +25,32 @@ Border = ClassUI(moho.border_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua") - self._v = LazyVar.Create() + self._v = LazyVarCreate() self._v.OnDirty = function(var) self:SetNewTextures(var(), nil, nil, nil, nil, nil) end - self._h = LazyVar.Create() + self._h = LazyVarCreate() self._h.OnDirty = function(var) self:SetNewTextures(nil, var(), nil, nil, nil, nil) end - self._ul = LazyVar.Create() + self._ul = LazyVarCreate() self._ul.OnDirty = function(var) self:SetNewTextures(nil, nil, var(), nil, nil, nil) end - self._ur = LazyVar.Create() + self._ur = LazyVarCreate() self._ur.OnDirty = function(var) self:SetNewTextures(nil, nil, nil, var(), nil, nil) end - self._ll = LazyVar.Create() + self._ll = LazyVarCreate() self._ll.OnDirty = function(var) self:SetNewTextures(nil, nil, nil, nil, var(), nil) end - self._lr = LazyVar.Create() + self._lr = LazyVarCreate() self._lr.OnDirty = function(var) self:SetNewTextures(nil, nil, nil, nil, nil, var()) end diff --git a/lua/maui/edit.lua b/lua/maui/edit.lua index 879a627218..f35983e9dd 100644 --- a/lua/maui/edit.lua +++ b/lua/maui/edit.lua @@ -33,6 +33,7 @@ local Control = import("/lua/maui/control.lua").Control local AddUnicodeCharToEditText = import("/lua/utf.lua").AddUnicodeCharToEditText local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@class Edit : moho.edit_methods, Control, InternalObject Edit = ClassUI(moho.edit_methods, Control) { @@ -43,9 +44,8 @@ Edit = ClassUI(moho.edit_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua") self._lockFontChanges = false - self._font = {_family = LazyVar.Create(), _pointsize = LazyVar.Create()} + self._font = {_family = LazyVarCreate(), _pointsize = LazyVarCreate()} self._font._family.OnDirty = function(var) self:_internalSetFont() end @@ -53,27 +53,27 @@ Edit = ClassUI(moho.edit_methods, Control) { self:_internalSetFont() end - self._fg = LazyVar.Create() + self._fg = LazyVarCreate() self._fg.OnDirty = function(var) self:SetNewForegroundColor(var()) end - self._bg = LazyVar.Create() + self._bg = LazyVarCreate() self._bg.OnDirty = function(var) self:SetNewBackgroundColor(var()) end - self._cc = LazyVar.Create() + self._cc = LazyVarCreate() self._cc.OnDirty = function(var) self:SetNewCaretColor(var()) end - self._hfg = LazyVar.Create() + self._hfg = LazyVarCreate() self._hfg.OnDirty = function(var) self:SetNewHighlightForegroundColor(var()) end - self._hbg = LazyVar.Create() + self._hbg = LazyVarCreate() self._hbg.OnDirty = function(var) self:SetNewHighlightBackgroundColor(var()) end diff --git a/lua/maui/itemlist.lua b/lua/maui/itemlist.lua index fcf7962bb5..33e4a6821d 100644 --- a/lua/maui/itemlist.lua +++ b/lua/maui/itemlist.lua @@ -18,6 +18,7 @@ local Control = import("/lua/maui/control.lua").Control local Dragger = import("/lua/maui/dragger.lua").Dragger local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@class ItemList : moho.item_list_methods, Control, InternalObject ItemList = ClassUI(moho.item_list_methods, Control) { @@ -28,9 +29,8 @@ ItemList = ClassUI(moho.item_list_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua") self._lockFontChanges = false - self._font = {_family = LazyVar.Create(), _pointsize = LazyVar.Create()} + self._font = {_family = LazyVarCreate(), _pointsize = LazyVarCreate()} self._font._family.OnDirty = function(var) self:_internalSetFont() end @@ -38,32 +38,32 @@ ItemList = ClassUI(moho.item_list_methods, Control) { self:_internalSetFont() end - self._fg = LazyVar.Create() + self._fg = LazyVarCreate() self._fg.OnDirty = function(var) self:SetNewColors(var(), nil, nil, nil, nil, nil) end - self._bg = LazyVar.Create() + self._bg = LazyVarCreate() self._bg.OnDirty = function(var) self:SetNewColors(nil, var(), nil, nil, nil, nil) end - self._sfg = LazyVar.Create() + self._sfg = LazyVarCreate() self._sfg.OnDirty = function(var) self:SetNewColors(nil, nil, var(), nil, nil, nil) end - self._sbg = LazyVar.Create() + self._sbg = LazyVarCreate() self._sbg.OnDirty = function(var) self:SetNewColors(nil, nil, nil, var(), nil, nil) end - self._mofg = LazyVar.Create() + self._mofg = LazyVarCreate() self._mofg.OnDirty = function(var) self:SetNewColors(nil, nil, nil, nil, var(), nil) end - self._mobg = LazyVar.Create() + self._mobg = LazyVarCreate() self._mobg.OnDirty = function(var) self:SetNewColors(nil, nil, nil, nil, nil, var()) end diff --git a/lua/maui/scrollbar.lua b/lua/maui/scrollbar.lua index 81c0ced798..741abaa516 100644 --- a/lua/maui/scrollbar.lua +++ b/lua/maui/scrollbar.lua @@ -5,6 +5,7 @@ -- ScrollBar:ScrollPages(float pages) local Control = import("/lua/maui/control.lua").Control +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@alias ScrollAxis "Horz" | "Vert" @@ -31,11 +32,11 @@ Scrollbar = ClassUI(moho.scrollbar_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua").Create - self._bg = LazyVar() - self._tm = LazyVar() - self._tt = LazyVar() - self._tb = LazyVar() + + self._bg = LazyVarCreate() + self._tm = LazyVarCreate() + self._tt = LazyVarCreate() + self._tb = LazyVarCreate() self._bg.OnDirty = function(var) self:SetNewTextures(var(), nil, nil, nil) diff --git a/lua/maui/text.lua b/lua/maui/text.lua index 9cd72d640e..d070616a9d 100644 --- a/lua/maui/text.lua +++ b/lua/maui/text.lua @@ -10,6 +10,7 @@ local Control = import("/lua/maui/control.lua").Control local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber +local LazyVarCreate = import("/lua/lazyvar.lua").Create ---@class Text : moho.text_methods, Control, InternalObject Text = ClassUI(moho.text_methods, Control) { @@ -20,9 +21,8 @@ Text = ClassUI(moho.text_methods, Control) { self:SetName(debugname) end - local LazyVar = import("/lua/lazyvar.lua") self._lockFontChanges = false - self._font = {_family = LazyVar.Create(), _pointsize = LazyVar.Create()} + self._font = {_family = LazyVarCreate(), _pointsize = LazyVarCreate()} self._font._family.OnDirty = function(var) self:_internalSetFont() end @@ -30,7 +30,7 @@ Text = ClassUI(moho.text_methods, Control) { self:_internalSetFont() end - self._color = LazyVar.Create() + self._color = LazyVarCreate() self._color.OnDirty = function(var) self:SetNewColor(var()) end From 39877cda9f6dc803a21577a6e75608a32ae72dc6 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:48:26 -0700 Subject: [PATCH 15/61] Implement ItemList:SetAlphaOfColors --- engine/User/CMauiItemList.lua | 7 +++++++ lua/maui/itemlist.lua | 30 ++++++++++++++++++++++++------ lua/shared/color.lua | 24 ++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/engine/User/CMauiItemList.lua b/engine/User/CMauiItemList.lua index 3d3cfa30bb..421f17d3c7 100644 --- a/engine/User/CMauiItemList.lua +++ b/engine/User/CMauiItemList.lua @@ -70,6 +70,13 @@ end function CMauiItemList:ScrollToTop() end +--- Sets the alpha of a given item list's background, if children is true, also set children's alpha +---@param alpha number +---@param children? boolean +---@see itemlist.lua:SetAlphaOfColors Lua implementation that can set the alpha of the text in an item list. +function CMauiItemList:SetAlpha(alpha, children) +end + --- ---@param foreground Color ---@param background Color diff --git a/lua/maui/itemlist.lua b/lua/maui/itemlist.lua index 33e4a6821d..7ae0bea30f 100644 --- a/lua/maui/itemlist.lua +++ b/lua/maui/itemlist.lua @@ -19,6 +19,7 @@ local Control = import("/lua/maui/control.lua").Control local Dragger = import("/lua/maui/dragger.lua").Dragger local ScaleNumber = import("/lua/maui/layouthelpers.lua").ScaleNumber local LazyVarCreate = import("/lua/lazyvar.lua").Create +local MultiplyAlpha = import("/lua/shared/color.lua").MultiplyAlpha ---@class ItemList : moho.item_list_methods, Control, InternalObject ItemList = ClassUI(moho.item_list_methods, Control) { @@ -38,34 +39,47 @@ ItemList = ClassUI(moho.item_list_methods, Control) { self:_internalSetFont() end + self._alpha = LazyVarCreate(1) + self._alpha.OnDirty = function(var) + local alpha = var() + self:SetNewColors( + self._fg() ~= 0 and MultiplyAlpha(self._fg(), alpha) or nil, + self._bg() ~= 0 and MultiplyAlpha(self._bg(), alpha) or nil, + self._sfg() ~= 0 and MultiplyAlpha(self._sfg(), alpha) or nil, + self._sbg() ~= 0 and MultiplyAlpha(self._sbg(), alpha) or nil, + self._mofg() ~= 0 and MultiplyAlpha(self._mofg(), alpha) or nil, + self._mobg() ~= 0 and MultiplyAlpha(self._mobg(), alpha) or nil + ) + end + self._fg = LazyVarCreate() self._fg.OnDirty = function(var) - self:SetNewColors(var(), nil, nil, nil, nil, nil) + self:SetNewColors(MultiplyAlpha(var(), self._alpha()), nil, nil, nil, nil, nil) end self._bg = LazyVarCreate() self._bg.OnDirty = function(var) - self:SetNewColors(nil, var(), nil, nil, nil, nil) + self:SetNewColors(nil, MultiplyAlpha(var(), self._alpha()), nil, nil, nil, nil) end self._sfg = LazyVarCreate() self._sfg.OnDirty = function(var) - self:SetNewColors(nil, nil, var(), nil, nil, nil) + self:SetNewColors(nil, nil, MultiplyAlpha(var(), self._alpha()), nil, nil, nil) end self._sbg = LazyVarCreate() self._sbg.OnDirty = function(var) - self:SetNewColors(nil, nil, nil, var(), nil, nil) + self:SetNewColors(nil, nil, nil, MultiplyAlpha(var(), self._alpha()), nil, nil) end self._mofg = LazyVarCreate() self._mofg.OnDirty = function(var) - self:SetNewColors(nil, nil, nil, nil, var(), nil) + self:SetNewColors(nil, nil, nil, nil, MultiplyAlpha(var(), self._alpha()), nil) end self._mobg = LazyVarCreate() self._mobg.OnDirty = function(var) - self:SetNewColors(nil, nil, nil, nil, nil, var()) + self:SetNewColors(nil, nil, nil, nil, nil, MultiplyAlpha(var(), self._alpha())) end end, @@ -95,6 +109,10 @@ ItemList = ClassUI(moho.item_list_methods, Control) { if mouseover_background and self._mobg then self._mobg:Set(mouseover_background) end end, + SetAlphaOfColors = function(self, alpha) + if alpha and self._alpha then self._alpha:Set(alpha) end + end, + OnDestroy = function(self) self._font._family:Destroy() self._font = nil diff --git a/lua/shared/color.lua b/lua/shared/color.lua index 9af8c30462..360a9527ee 100644 --- a/lua/shared/color.lua +++ b/lua/shared/color.lua @@ -57,6 +57,30 @@ function ParseColor(color) return r / 255, g / 255, b / 255 end +--- Returns the alpha component of a color string if present, `1.0` otherwise. +---@param color Color +---@return number alpha +function GetAlpha(color) + color = EnumColors[color] or color + if color:sub(7,8) == "" then + return 0 + else + return tonumber(color:sub(1,2), 16) / 255 + end +end + +--- Returns the color with its alpha multiplied by a number +---@param color Color +---@param mult number +---@return Color +function MultiplyAlpha(color, mult) + color = EnumColors[color] or color + if color:sub(7, 8) == "" then + return string.format("%02X", math.clamp(255 * mult, 0, 255)) .. color:sub(1, 6) + else + return string.format("%02X", math.clamp(tonumber(color:sub(1, 2), 16) * mult, 0, 255)) .. color:sub(3, 8) + end +end ---------------------------------------- -- Color Conversions From 77a026b060ca2c33d0e4ce0dfd4d144304c2fb02 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 15:09:22 -0700 Subject: [PATCH 16/61] Implement `TextArea:FitToText()` and its helpers Helpers: `TextArea:GetTextWidth()` and `TextArea:GetTextHeight()` --- lua/ui/controls/textarea.lua | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/lua/ui/controls/textarea.lua b/lua/ui/controls/textarea.lua index c76d56a5f1..fbae612d59 100644 --- a/lua/ui/controls/textarea.lua +++ b/lua/ui/controls/textarea.lua @@ -1,7 +1,11 @@ +local mathMax = math.max + local Text = import("/lua/maui/text.lua") local ItemList = import("/lua/maui/itemlist.lua").ItemList + local UIUtil = import("/lua/ui/uiutil.lua") local LayoutHelpers = import("/lua/maui/layouthelpers.lua") +local PixelScaleFactor = LayoutHelpers:GetPixelScaleFactor() --- A multi-line textfield -- @@ -28,6 +32,7 @@ TextArea = ClassUI(ItemList) { LayoutHelpers.SetDimensions(self, width, height) self.text = "" + self._textWidth = 0 -- By default, inherit colour and font from UIUtil (this will update with the skin, too, -- because LazyVars are magical. @@ -66,6 +71,14 @@ TextArea = ClassUI(ItemList) { return self.text end, + GetTextHeight = function(self) + return mathMax(0, self:GetItemCount() * self:GetRowHeight() - PixelScaleFactor) + end, + + GetTextWidth = function(self) + return self._textWidth + end, + --- Add more text to the textfield starting on a new line (high-performance append operation -- that avoids incurring a complete reflow). ---@param self TextArea @@ -77,20 +90,38 @@ TextArea = ClassUI(ItemList) { self.text = self.text .. "\n" .. text end local wrapped = Text.WrapText(text, self.Width(), self.advanceFunction) + local newTextWidth = 0 for i, line in wrapped do self:AddItem(line) + + local lineWidth = self.advanceFunction(line) + if lineWidth > newTextWidth then + newTextWidth = lineWidth + end end + self._textWidth = newTextWidth end, ---@param self TextArea ReflowText = function(self) local wrapped = Text.WrapText(self.text, self.Width(), self.advanceFunction) - + local newTextWidth = 0 -- Replace the old lines with the newly-wrapped ones. self:DeleteAllItems() for i, line in wrapped do self:AddItem(line) + + local lineWidth = self.advanceFunction(line) + if lineWidth > newTextWidth then + newTextWidth = lineWidth + end end + self._textWidth = newTextWidth + end, + + ---@param self TextArea + FitToText = function(self) + LayoutHelpers.SetDimensions(self, self:GetTextWidth(), self:GetTextHeight()) end } From 9adf16d179dc1d1d07e132189ebe15b527506ea0 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:20:47 -0700 Subject: [PATCH 17/61] Implement `AlignText` function --- lua/maui/text.lua | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lua/maui/text.lua b/lua/maui/text.lua index d070616a9d..683724e969 100644 --- a/lua/maui/text.lua +++ b/lua/maui/text.lua @@ -269,3 +269,31 @@ function WrapText(text, lineWidth, advanceFunction) end return result end + +--- Returns text aligned proportionally along a line width. +---@param text string +---@param lineWidth number +---@param advanceFunction function +---@param alignmentProportion number How far towards the right of the line the text should be aligned. 0.5 for middle alignment, 1.0 for right alignment. +---@return string +---@overload fun(text: string[], lineWidth: number, advanceFunction: function, alignmentProportion: number): string[] +function AlignText(text, lineWidth, advanceFunction, alignmentProportion) + local spaceWidth = advanceFunction(" ") + + if type(text) == "string" then + local textWidth = advanceFunction(text) + local spacesToAdd = math.floor((lineWidth - textWidth) / spaceWidth * alignmentProportion) + if spacesToAdd > 0 then + return string.rep(" ", spacesToAdd) .. text + else + return text + end + else + -- text is a table of strings + local alignedText = {} + for i, line in text do + alignedText[i] = AlignText(line, lineWidth, advanceFunction, alignmentProportion) + end + return alignedText + end +end From 2ab77c7b7508618a30086bd7d511577da0a05590 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 22:26:42 -0700 Subject: [PATCH 18/61] Implement `ItemList:GetAllItems` --- lua/maui/itemlist.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lua/maui/itemlist.lua b/lua/maui/itemlist.lua index 7ae0bea30f..55a4fca4f8 100644 --- a/lua/maui/itemlist.lua +++ b/lua/maui/itemlist.lua @@ -130,6 +130,16 @@ ItemList = ClassUI(moho.item_list_methods, Control) { self._mobg = nil end, + ---@param self ItemList + ---@return LocalizedString[] + GetAllItems = function(self) + local items = {} + for i = 0, self:GetItemCount() - 1 do + items[i] = self:GetItem(i) + end + return items + end, + -- default override methods, event has the whole event so you can get modifiers OnClick = function(self, row, event) self:SetSelection(row) @@ -147,4 +157,3 @@ ItemList = ClassUI(moho.item_list_methods, Control) { OnMouseoverItem = function(self, row) end, } - From 49f79a8630fdaeded0d769987c2548839158b4d3 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:38:25 -0700 Subject: [PATCH 19/61] Implement TextArea SetTextAlignment --- lua/ui/controls/textarea.lua | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/lua/ui/controls/textarea.lua b/lua/ui/controls/textarea.lua index fbae612d59..3639da539b 100644 --- a/lua/ui/controls/textarea.lua +++ b/lua/ui/controls/textarea.lua @@ -19,6 +19,7 @@ local PixelScaleFactor = LayoutHelpers:GetPixelScaleFactor() -- self-harm long enough to finish writing this class so we can call this a solved problem and never -- look in this file ever again. ---@class TextArea : ItemList +---@field advanceFunction function The advance function for wrapping text. TextArea = ClassUI(ItemList) { ---@param self TextArea @@ -105,11 +106,18 @@ TextArea = ClassUI(ItemList) { ---@param self TextArea ReflowText = function(self) - local wrapped = Text.WrapText(self.text, self.Width(), self.advanceFunction) + local width = self.Width() + local advanceFunction = self.advanceFunction + local alignmentProportion = self._alignmentProportion + + local wrapped = Text.WrapText(self.text, width, advanceFunction) local newTextWidth = 0 -- Replace the old lines with the newly-wrapped ones. self:DeleteAllItems() for i, line in wrapped do + if alignmentProportion then + line = Text.AlignText(line, width, advanceFunction, alignmentProportion) + end self:AddItem(line) local lineWidth = self.advanceFunction(line) @@ -123,5 +131,17 @@ TextArea = ClassUI(ItemList) { ---@param self TextArea FitToText = function(self) LayoutHelpers.SetDimensions(self, self:GetTextWidth(), self:GetTextHeight()) - end + end, + + --- Aligns all the text proportionally along the TextArea's width. + ---@param self TextArea + ---@param alignmentProportion number How far towards the right of the line the text should be aligned. 0.5 for middle alignment, 1.0 for right alignment. + SetTextAlignment = function(self, alignmentProportion) + self._alignmentProportion = alignmentProportion + local width = self.Width() + local advanceFunction = self.advanceFunction + for i = 0, self:GetItemCount() - 1 do + self:ModifyItem(i, Text.AlignText(self:GetItem(i), width, advanceFunction, alignmentProportion)) + end + end, } From 05a99e3ff5697b8abfbe3330557e3d1040caf22a Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:43:18 -0700 Subject: [PATCH 20/61] Make color.lua enum support case insensitive --- lua/shared/color.lua | 284 +++++++++++++++++++++---------------------- 1 file changed, 142 insertions(+), 142 deletions(-) diff --git a/lua/shared/color.lua b/lua/shared/color.lua index 360a9527ee..666c3cacad 100644 --- a/lua/shared/color.lua +++ b/lua/shared/color.lua @@ -46,7 +46,7 @@ function ParseColor(color) if color == "transparent" then return 0, 0, 0, 0 end - color = EnumColors[color] + color = EnumColors[string.upper(color)] if not color then return false end @@ -606,145 +606,145 @@ end --- Map of named colors the Moho engine can recognize and their representation ---@see EnumColorNames() EnumColors = { - AliceBlue = "F7FBFF", - AntiqueWhite = "FFEBD6", - Aqua = "00FFFF", - Aquamarine = "7BFFD6", - Azure = "F7FFFF", - Beige = "F7F7DE", - Bisque = "FFE7C6", - Black = "000000", - BlanchedAlmond = "FFEBCE", - Blue = "0000FF", - BlueViolet = "8C28E7", - Brown = "A52829", - BurlyWood = "DEBA84", - CadetBlue = "5A9EA5", - Chartreuse = "7BFF00", - Chocolate = "D66918", - Coral = "FF7D52", - CornflowerBlue = "6396EF", - Cornsilk = "FFFBDE", - Crimson = "DE1439", - Cyan = "00FFFF", - DarkBlue = "00008C", - DarkCyan = "008A8C", - DarkGoldenrod = "BD8608", - DarkGray = "ADAAAD", - DarkGreen = "006500", - DarkKhaki = "BDB66B", - DarkMagenta = "8C008C", - DarkOliveGreen = "526929", - DarkOrange = "FF8E00", - DarkOrchid = "9C30CE", - DarkRed = "8C0000", - DarkSalmon = "EF967B", - DarkSeaGreen = "8CBE8C", - DarkSlateBlue = "4A3C8C", - DarkSlateGray = "294D4A", - DarkTurquoise = "00CFD6", - DarkViolet = "9400D6", - DeepPink = "FF1494", - DeepSkyBlue = "00BEFF", - DimGray = "6B696B", - DodgerBlue = "1892FF", - Firebrick = "B52021", - FloralWhite = "FFFBF7", - ForestGreen = "218A21", - Fuchsia = "FF00FF", - Gainsboro = "DEDFDE", - GhostWhite = "FFFBFF", - Gold = "FFD700", - Goldenrod = "DEA621", - Gray = "848284", - Green = "008200", - GreenYellow = "ADFF29", - Honeydew = "F7FFF7", - HotPink = "FF69B5", - IndianRed = "CE5D5A", - Indigo = "4A0084", - Ivory = "FFFFF7", - Khaki = "F7E78C", - Lavender = "E7E7FF", - LavenderBlush = "FFF3F7", - LawnGreen = "7BFF00", - LemonChiffon = "FFFBCE", - LightBlue = "ADDBE7", - LightCoral = "F78284", - LightCyan = "E7FFFF", - LightGoldenrodYellow = "FFFBD6", - LightGray = "D6D3D6", - LightGreen = "94EF94", - LightPink = "FFB6C6", - LightSalmon = "FFA27B", - LightSeaGreen = "21B2AD", - LightSkyBlue = "84CFFF", - LightSlateGray = "738A9C", - LightSteelBlue = "B5C7DE", - LightYellow = "FFFFE7", - Lime = "00FF00", - LimeGreen = "31CF31", - Linen = "FFF3E7", - Magenta = "FF00FF", - Maroon = "840000", - MediumAquamarine = "63CFAD", - MediumBlue = "0000CE", - MediumOrchid = "BD55D6", - MediumPurple = "9471DE", - MediumSeaGreen = "39B273", - MediumSlateBlue = "7B69EF", - MediumSpringGreen = "00FB9C", - MediumTurquoise = "4AD3CE", - MediumVioletRed = "C61484", - MidnightBlue = "181873", - MintCream = "F7FFFF", - MistyRose = "FFE7E7", - Moccasin = "FFE7B5", - NavajoWhite = "FFDFAD", - Navy = "000084", - OldLace = "FFF7E7", - Olive = "848200", - OliveDrab = "6B8E21", - Orange = "FFA600", - OrangeRed = "FF4500", - Orchid = "DE71D6", - PaleGoldenrod = "EFEBAD", - PaleGreen = "9CFB9C", - PaleTurquoise = "ADEFEF", - PaleVioletRed = "DE7194", - PapayaWhip = "FFEFD6", - PeachPuff = "FFDBBD", - Peru = "CE8639", - Pink = "FFC3CE", - Plum = "DEA2DE", - PowderBlue = "B5E3E7", - Purple = "840084", - Red = "FF0000", - RosyBrown = "BD8E8C", - RoyalBlue = "4269E7", - SaddleBrown = "8C4510", - Salmon = "FF8273", - SandyBrown = "F7A663", - SeaGreen = "298A52", - SeaShell = "FFF7EF", - Sienna = "A55129", - Silver = "C6C3C6", - SkyBlue = "84CFEF", - SlateBlue = "6B59CE", - SlateGray = "738294", - Snow = "FFFBFF", - SpringGreen = "00FF7B", - SteelBlue = "4282B5", - Tan = "D6B68C", - Teal = "008284", - Thistle = "DEBEDE", - Tomato = "FF6142", - Turquoise = "42E3D6", - Violet = "EF82EF", - Wheat = "F7DFB5", - White = "FFFFFF", - WhiteSmoke = "F7F7F7", - Yellow = "FFFF00", - YellowGreen = "9CCF31", - transparent = "00000000", + ALICEBLUE = "F7FBFF", + ANTIQUEWHITE = "FFEBD6", + AQUA = "00FFFF", + AQUAMARINE = "7BFFD6", + AZURE = "F7FFFF", + BEIGE = "F7F7DE", + BISQUE = "FFE7C6", + BLACK = "000000", + BLANCHEDALMOND = "FFEBCE", + BLUE = "0000FF", + BLUEVIOLET = "8C28E7", + BROWN = "A52829", + BURLYWOOD = "DEBA84", + CADETBLUE = "5A9EA5", + CHARTREUSE = "7BFF00", + CHOCOLATE = "D66918", + CORAL = "FF7D52", + CORNFLOWERBLUE = "6396EF", + CORNSILK = "FFFBDE", + CRIMSON = "DE1439", + CYAN = "00FFFF", + DARKBLUE = "00008C", + DARKCYAN = "008A8C", + DARKGOLDENROD = "BD8608", + DARKGRAY = "ADAAAD", + DARKGREEN = "006500", + DARKKHAKI = "BDB66B", + DARKMAGENTA = "8C008C", + DARKOLIVEGREEN = "526929", + DARKORANGE = "FF8E00", + DARKORCHID = "9C30CE", + DARKRED = "8C0000", + DARKSALMON = "EF967B", + DARKSEAGREEN = "8CBE8C", + DARKSLATEBLUE = "4A3C8C", + DARKSLATEGRAY = "294D4A", + DARKTURQUOISE = "00CFD6", + DARKVIOLET = "9400D6", + DEEPPINK = "FF1494", + DEEPSKYBLUE = "00BEFF", + DIMGRAY = "6B696B", + DODGERBLUE = "1892FF", + FIREBRICK = "B52021", + FLORALWHITE = "FFFBF7", + FORESTGREEN = "218A21", + FUCHSIA = "FF00FF", + GAINSBORO = "DEDFDE", + GHOSTWHITE = "FFFBFF", + GOLD = "FFD700", + GOLDENROD = "DEA621", + GRAY = "848284", + GREEN = "008200", + GREENYELLOW = "ADFF29", + HONEYDEW = "F7FFF7", + HOTPINK = "FF69B5", + INDIANRED = "CE5D5A", + INDIGO = "4A0084", + IVORY = "FFFFF7", + KHAKI = "F7E78C", + LAVENDER = "E7E7FF", + LAVENDERBLUSH = "FFF3F7", + LAWNGREEN = "7BFF00", + LEMONCHIFFON = "FFFBCE", + LIGHTBLUE = "ADDBE7", + LIGHTCORAL = "F78284", + LIGHTCYAN = "E7FFFF", + LIGHTGOLDENRODYELLOW = "FFFBD6", + LIGHTGRAY = "D6D3D6", + LIGHTGREEN = "94EF94", + LIGHTPINK = "FFB6C6", + LIGHTSALMON = "FFA27B", + LIGHTSEAGREEN = "21B2AD", + LIGHTSKYBLUE = "84CFFF", + LIGHTSLATEGRAY = "738A9C", + LIGHTSTEELBLUE = "B5C7DE", + LIGHTYELLOW = "FFFFE7", + LIME = "00FF00", + LIMEGREEN = "31CF31", + LINEN = "FFF3E7", + MAGENTA = "FF00FF", + MAROON = "840000", + MEDIUMAQUAMARINE = "63CFAD", + MEDIUMBLUE = "0000CE", + MEDIUMORCHID = "BD55D6", + MEDIUMPURPLE = "9471DE", + MEDIUMSEAGREEN = "39B273", + MEDIUMSLATEBLUE = "7B69EF", + MEDIUMSPRINGGREEN = "00FB9C", + MEDIUMTURQUOISE = "4AD3CE", + MEDIUMVIOLETRED = "C61484", + MIDNIGHTBLUE = "181873", + MINTCREAM = "F7FFFF", + MISTYROSE = "FFE7E7", + MOCCASIN = "FFE7B5", + NAVAJOWHITE = "FFDFAD", + NAVY = "000084", + OLDLACE = "FFF7E7", + OLIVE = "848200", + OLIVEDRAB = "6B8E21", + ORANGE = "FFA600", + ORANGERED = "FF4500", + ORCHID = "DE71D6", + PALEGOLDENROD = "EFEBAD", + PALEGREEN = "9CFB9C", + PALETURQUOISE = "ADEFEF", + PALEVIOLETRED = "DE7194", + PAPAYAWHIP = "FFEFD6", + PEACHPUFF = "FFDBBD", + PERU = "CE8639", + PINK = "FFC3CE", + PLUM = "DEA2DE", + POWDERBLUE = "B5E3E7", + PURPLE = "840084", + RED = "FF0000", + ROSYBROWN = "BD8E8C", + ROYALBLUE = "4269E7", + SADDLEBROWN = "8C4510", + SALMON = "FF8273", + SANDYBROWN = "F7A663", + SEAGREEN = "298A52", + SEASHELL = "FFF7EF", + SIENNA = "A55129", + SILVER = "C6C3C6", + SKYBLUE = "84CFEF", + SLATEBLUE = "6B59CE", + SLATEGRAY = "738294", + SNOW = "FFFBFF", + SPRINGGREEN = "00FF7B", + STEELBLUE = "4282B5", + TAN = "D6B68C", + TEAL = "008284", + THISTLE = "DEBEDE", + TOMATO = "FF6142", + TURQUOISE = "42E3D6", + VIOLET = "EF82EF", + WHEAT = "F7DFB5", + WHITE = "FFFFFF", + WHITESMOKE = "F7F7F7", + YELLOW = "FFFF00", + YELLOWGREEN = "9CCF31", + TRANSPARENT = "00000000", } From d7fafc9bb6cbd7e3f207805138e03a353d08beb1 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:43:38 -0700 Subject: [PATCH 21/61] Make new color functions enum support case insensitive --- lua/shared/color.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/shared/color.lua b/lua/shared/color.lua index 666c3cacad..bb97f42a7b 100644 --- a/lua/shared/color.lua +++ b/lua/shared/color.lua @@ -61,7 +61,7 @@ end ---@param color Color ---@return number alpha function GetAlpha(color) - color = EnumColors[color] or color + color = EnumColors[string.upper(color)] or color if color:sub(7,8) == "" then return 0 else @@ -74,7 +74,7 @@ end ---@param mult number ---@return Color function MultiplyAlpha(color, mult) - color = EnumColors[color] or color + color = EnumColors[string.upper(color)] or color if color:sub(7, 8) == "" then return string.format("%02X", math.clamp(255 * mult, 0, 255)) .. color:sub(1, 6) else From d9c2eee82193e09dd9ce5e9572fe79a5204a3992 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:53:26 -0700 Subject: [PATCH 22/61] Implement using TextArea for announcements --- lua/ui/game/announcement.lua | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/lua/ui/game/announcement.lua b/lua/ui/game/announcement.lua index 44e192600b..3643de58dd 100644 --- a/lua/ui/game/announcement.lua +++ b/lua/ui/game/announcement.lua @@ -11,15 +11,16 @@ local Layouter = LayoutHelpers.ReusedLayoutFor local UIUtil = import("/lua/ui/uiutil.lua") local Group = import("/lua/maui/group.lua").Group local Bitmap = import("/lua/maui/bitmap.lua").Bitmap +local TextArea = import("/lua/ui/controls/textarea.lua").TextArea local MATH_Lerp = MATH_Lerp local bg = false --- Create an announcement UI for sending general messages to the user ----@param text string +---@param text UnlocalizedString # title text ---@param goalControl? Control The control where the announcement appears out of. ----@param secondaryText? string +---@param secondaryText? UnlocalizedString # body text ---@param onFinished? function function CreateAnnouncement(text, goalControl, secondaryText, onFinished) local frame = GetFrame(0) @@ -55,6 +56,9 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) CreateAnnouncement(text, goalControl, secondaryText, onFinished) end self:SetAlpha(newAlpha, true) + if bg.secText then + bg.secText:SetAlphaOfColors(newAlpha) + end end return end @@ -72,10 +76,18 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) :DropShadow(true):Color(UIUtil.fontColor) :NeedsFrameUpdate(true):End() + local secText if secondaryText then - local secText = Layouter(UIUtil.CreateText(textGroup, secondaryText, 18, UIUtil.bodyFont)) - :DropShadow(true):Color(UIUtil.fontColor) - :Below(text, 10):AtHorizontalCenterIn(text):End() + secText = TextArea(textGroup, 600, 60) + secText:SetFont(UIUtil.bodyFont, 18) + secText:SetText(secondaryText) + secText:FitToText() + secText:SetTextAlignment(0.5) + secText:SetColors(UIUtil.fontColor) + secText:SetAlphaOfColors(0) + Layouter(secText):CenteredBelow(text, 10):End() + bg.secText = secText + Layouter(textGroup):Top(text.Top) :Left(function() return math.min(secText.Left(), text.Left()) end) :Right(function() return math.max(secText.Right(), text.Right()) end) @@ -130,10 +142,18 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) local textAlpha = text:GetAlpha() -- fade out the text at the end of the announcement if time > 3 and textGroupAlpha ~= 0 then - textGroup:SetAlpha(math.max(textGroupAlpha - (delta * 2), 0), true) + local newAlpha = math.max(textGroupAlpha - (delta * 2), 0) + textGroup:SetAlpha(newAlpha, true) + if secText then + secText:SetAlphaOfColors(newAlpha) + end -- fade in the text when the announcement appears elseif time > .2 and time < 3 and textAlpha ~= 1 then - textGroup:SetAlpha(math.min(textAlpha + (delta * 2), 1), true) + local newAlpha = math.min(textAlpha + (delta * 2), 1) + textGroup:SetAlpha(newAlpha, true) + if secText then + secText:SetAlphaOfColors(newAlpha) + end end if time > 3.7 then From d24c4f12b1f846bb2b52894f4591bfae1c23ac3b Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:54:08 -0700 Subject: [PATCH 23/61] Refactor "text" variable to "title" in announcement --- lua/ui/game/announcement.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lua/ui/game/announcement.lua b/lua/ui/game/announcement.lua index 3643de58dd..ff23c1a394 100644 --- a/lua/ui/game/announcement.lua +++ b/lua/ui/game/announcement.lua @@ -71,7 +71,7 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) local textGroup = Group(bg) - local text = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) + local title = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) :AtCenterIn(frame, -250) :DropShadow(true):Color(UIUtil.fontColor) :NeedsFrameUpdate(true):End() @@ -85,15 +85,15 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) secText:SetTextAlignment(0.5) secText:SetColors(UIUtil.fontColor) secText:SetAlphaOfColors(0) - Layouter(secText):CenteredBelow(text, 10):End() + Layouter(secText):CenteredBelow(title, 10):End() bg.secText = secText - Layouter(textGroup):Top(text.Top) - :Left(function() return math.min(secText.Left(), text.Left()) end) - :Right(function() return math.max(secText.Right(), text.Right()) end) + Layouter(textGroup):Top(title.Top) + :Left(function() return math.min(secText.Left(), title.Left()) end) + :Right(function() return math.max(secText.Right(), title.Right()) end) :Bottom(secText.Bottom):End() else - LayoutHelpers.FillParent(textGroup, text) + LayoutHelpers.FillParent(textGroup, title) end bg:DisableHitTest(true) @@ -139,7 +139,7 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) end local textGroupAlpha = textGroup:GetAlpha() - local textAlpha = text:GetAlpha() + local titleAlpha = title:GetAlpha() -- fade out the text at the end of the announcement if time > 3 and textGroupAlpha ~= 0 then local newAlpha = math.max(textGroupAlpha - (delta * 2), 0) @@ -148,8 +148,8 @@ function CreateAnnouncement(text, goalControl, secondaryText, onFinished) secText:SetAlphaOfColors(newAlpha) end -- fade in the text when the announcement appears - elseif time > .2 and time < 3 and textAlpha ~= 1 then - local newAlpha = math.min(textAlpha + (delta * 2), 1) + elseif time > .2 and time < 3 and titleAlpha ~= 1 then + local newAlpha = math.min(titleAlpha + (delta * 2), 1) textGroup:SetAlpha(newAlpha, true) if secText then secText:SetAlphaOfColors(newAlpha) From 0ec268b0da4ec1f266454cce980f2823fc9c4c85 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 5 Jun 2024 00:51:48 -0700 Subject: [PATCH 24/61] Use share reasons in the share announcement --- loc/US/strings_db.lua | 8 ++++++++ lua/UserSync.lua | 30 ++++++++++++++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/loc/US/strings_db.lua b/loc/US/strings_db.lua index f2df0eb496..5d31a65b1c 100644 --- a/loc/US/strings_db.lua +++ b/loc/US/strings_db.lua @@ -6748,10 +6748,17 @@ urs0304_name="Plan B" urs0305_desc="Sonar Platform" urs0305_help="Sonar Platform" urs0305_name="Flood XR" + usersync_0001="%s wins!" usersync_0002="%s has been defeated!" usersync_0003="%s receives a draw." usersync_0004="Game Over." +usersync_0005="Disconnect Temporary Share" +usersync_0006="Disconnect Share" +usersync_0007="%s\'s units and ACU transferred to you until ACU dies/recalls" +usersync_0008="%s\'s units and ACU transferred to you until ACU dies" +usersync_0009="\nShare Condition after: %s" + uvd_0000="Build Cost (Rate)" uvd_0002="Yield" uvd_0003="Description" @@ -8214,5 +8221,6 @@ chat_send_type_description="When enabled, enter sends messages to allies and hol replay_id="Replay id" map_version="Map version" +fullshare_announcement="%s\'s units have been transferred to you" ChangelogDescriptionIdentifier="description" diff --git a/lua/UserSync.lua b/lua/UserSync.lua index 3f5f24a87d..4f3f2f6740 100644 --- a/lua/UserSync.lua +++ b/lua/UserSync.lua @@ -49,6 +49,21 @@ function RemoveOnSyncHashedCallback(cat, id) end end +local transferReasonToAnnouncementTitle = { + ["FullShare"] = "Full Share", + ["PartialShare"] = "Partial Share", + ["TransferToKiller"] = "Traitors", + ["Defectors"] = "Defectors", + ["CivilianDeserter"] = "Civilian Desertion", + ["DisconnectShareTemporary"] = "Disconnect Temporary Share", + ["DisconnectSharePermanent"] = "Disconnect Share", +} + +local transferReasonToAnnouncementText = { + ["DisconnectShareTemporary"] = "%s\'s units and ACU transferred to you until ACU dies/recalls", + ["DisconnectSharePermanent"] = "%s\'s units and ACU transferred to you until ACU dies", +} + -- Here's an opportunity for user side script to examine the Sync table for the new tick function OnSync() @@ -154,14 +169,21 @@ function OnSync() end end - if Sync.ArmyTransfer then + if Sync.ArmyTransfer --[[@as { [1]: { from: number, to: number, reason: string } }]] then local army = GetFocusArmy() for k, transfer in Sync.ArmyTransfer do local other = GetArmiesTable().armiesTable[transfer.from].nickname if transfer.to == army then - local primary = "Fullshare" - local secondary = LOCF('%s\'s units have been transferred to you', other) - local control = nil + local reason = transfer.reason + + local primary = LOC(transferReasonToAnnouncementTitle[reason]) + local secondary = LOCF(transferReasonToAnnouncementText[reason] or "%s\'s units have been transferred to you", other) + + if reason == "DisconnectShareTemporary" or reason == "DisconnectSharePermanent" then + local shareOption = SessionGetScenarioInfo().Options.DisconnectShare + secondary = secondary .. LOCF("\nShare Condition after: %s", LOC(transferReasonToAnnouncementTitle[shareOption])) + end + UIUtil.CreateAnnouncementStd(primary, secondary, control) end end From 35da54eaa606af1d30416e51da086cfc4242692f Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 4 Jun 2024 23:50:51 -0700 Subject: [PATCH 25/61] Fix typo in a uiutil comment --- lua/ui/uiutil.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/ui/uiutil.lua b/lua/ui/uiutil.lua index d5076e2806..4de5ffc29f 100644 --- a/lua/ui/uiutil.lua +++ b/lua/ui/uiutil.lua @@ -1421,7 +1421,7 @@ end ---@param primary UnlocalizedString ---@param secondary UnlocalizedString ----@param control? Control defaults to duumy control at center of screen +---@param control? Control defaults to dummy control at center of screen function CreateAnnouncementStd(primary, secondary, control) -- make it originate from the top if not control then From e08721bfd95d5e17e10cba2eff953f8f9bf89e84 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:23:40 -0700 Subject: [PATCH 26/61] Add guards for share option announcement text --- lua/UserSync.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lua/UserSync.lua b/lua/UserSync.lua index 4f3f2f6740..4bee6d543b 100644 --- a/lua/UserSync.lua +++ b/lua/UserSync.lua @@ -176,12 +176,12 @@ function OnSync() if transfer.to == army then local reason = transfer.reason - local primary = LOC(transferReasonToAnnouncementTitle[reason]) + local primary = LOC(transferReasonToAnnouncementTitle[reason] or "Full Share") local secondary = LOCF(transferReasonToAnnouncementText[reason] or "%s\'s units have been transferred to you", other) if reason == "DisconnectShareTemporary" or reason == "DisconnectSharePermanent" then local shareOption = SessionGetScenarioInfo().Options.DisconnectShare - secondary = secondary .. LOCF("\nShare Condition after: %s", LOC(transferReasonToAnnouncementTitle[shareOption])) + secondary = secondary .. LOCF("\nShare Condition after: %s", LOC(transferReasonToAnnouncementTitle[shareOption] or "Share Until Death")) end UIUtil.CreateAnnouncementStd(primary, secondary, control) From 13e4fd3a11f61053e062f0c4489b8db43460f30e Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:26:23 -0700 Subject: [PATCH 27/61] Use a variable for 2 the minute acu safety timer --- lua/SimUtils.lua | 4 ++++ lua/aibrain.lua | 7 ++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index eacf426207..8c8d00b4af 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -908,6 +908,10 @@ end local StartCountdown = StartCountdown -- as defined in SymSync.lua +-- The time in ticks after taking damage that commanders are considered safe and not abusing disconnect rules +---@see aibrain.lua:AbandonedByPlayer +CommanderSafeTime = 1200 + --- Shares all units including ACUs. When the shared ACUs die or recall after `shareTime`, kills my army according to the given share condition. ---@param self AIBrain ---@param shareOption 'FullShare' | 'ShareUntilDeath' | 'PartialShare' | 'TransferToKiller' | 'Defectors' | 'CivilianDeserter' diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 6e7d01617e..6a00e8af68 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -28,6 +28,8 @@ local JammerManagerBrainComponent = import("/lua/aibrains/components/JammerManag local StatManagerBrainComponent = import("/lua/aibrains/components/StatManagerBrainComponent.lua").StatManagerBrainComponent local EnergyManagerBrainComponent = import("/lua/aibrains/components/EnergyManagerBrainComponent.lua").EnergyManagerBrainComponent +local CommanderSafeTime = import("/lua/simutils.lua").CommanderSafeTime + ---@class TriggerSpec ---@field Callback function ---@field ReconTypes ReconTypes @@ -494,8 +496,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) for _, com in commanders do - -- 2 minutes since last damaged - if com.LastTickDamaged == nil or com.LastTickDamaged + 1200 <= GetGameTick() then + if com.LastTickDamaged == nil or com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then table.insert(safeCommanders, com) end end @@ -519,7 +520,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM end if shareAcuOption == 'RecallDelayed' then - local shareTime = GetGameTick() + 1200 + local shareTime = GetGameTick() + CommanderSafeTime if shareTime < 3000 then shareTime = 3000 end From f5923de934650ab6274fab47ea5c059a32c60cb9 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:41:00 -0700 Subject: [PATCH 28/61] Guard against abandoning without an ACU could happen in non-assassination modes --- lua/SimUtils.lua | 51 ++++++++++++++++++++++++++---------------------- lua/aibrain.lua | 3 ++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 8c8d00b4af..9868f74288 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -922,25 +922,28 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectShareTemporary") local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) - -- create a countdown to show when the ACU recalls (similar to timed self-destruct) - for _, com in sharedCommanders do - StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) - end - - local oneComAlive = true - while GetGameTick() < shareTime and oneComAlive do - oneComAlive = false + -- non-assassination games could have an army abandon without having any commanders + if not table.empty(sharedCommanders) then + -- create a countdown to show when the ACU recalls (similar to timed self-destruct) for _, com in sharedCommanders do - if not com.Dead then - oneComAlive = true - break + StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) + end + + local oneComAlive = true + while GetGameTick() < shareTime and oneComAlive do + oneComAlive = false + for _, com in sharedCommanders do + if not com.Dead then + oneComAlive = true + break + end end + WaitTicks(1) end - WaitTicks(1) - end - -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. - ForkThread(FakeTeleportUnits, sharedCommanders, true) + -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. + ForkThread(FakeTeleportUnits, sharedCommanders, true) + end KillArmy(self, shareOption) end @@ -953,16 +956,18 @@ function KillArmyOnACUDeath(self, shareOption) local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectSharePermanent") local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) - local oneComAlive = true - while oneComAlive do - oneComAlive = false - for _, com in sharedCommanders do - if not com.Dead then - oneComAlive = true - break + if not table.empty(sharedCommanders) then + local oneComAlive = true + while oneComAlive do + oneComAlive = false + for _, com in sharedCommanders do + if not com.Dead then + oneComAlive = true + break + end end + WaitTicks(1) end - WaitTicks(1) end KillArmy(self, shareOption) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 6a00e8af68..489906f68c 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -506,7 +506,8 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM shareOption = ScenarioInfo.Options.Share end - if shareAcuOption == 'Recall' then + -- non-assassination modes can have armies abandon without commanders + if shareAcuOption == 'Recall' and not table.empty(safeCommanders) then -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. ForkThread(FakeTeleportUnits, safeCommanders, true) end From e546af97a4634625868bcc281e1a9e35d7a783a8 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:44:40 -0700 Subject: [PATCH 29/61] Update a comment --- lua/SimUtils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 9868f74288..d9654755f8 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -924,7 +924,7 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) -- non-assassination games could have an army abandon without having any commanders if not table.empty(sharedCommanders) then - -- create a countdown to show when the ACU recalls (similar to timed self-destruct) + -- create a countdown to show when the ACU recalls (similar to the one used for timed self-destruct) for _, com in sharedCommanders do StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) end From 29c0b40da8575c60ac5665bcda40b26e1e0a2948 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:50:05 -0700 Subject: [PATCH 30/61] Guard against disconnect abuse in ACU sharing Applies the standard share condition if the ACU dies within the safety timer after being shared --- lua/SimUtils.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index d9654755f8..0258931786 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -941,6 +941,13 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) WaitTicks(1) end + -- if all the commanders die early, assume disconnect abuse and apply standard share condition. Only makes sense in Assassination. + local scenarioOptions = ScenarioInfo.Options + if not oneComAlive and scenarioOptions.Victory == "demoralization" then + KillArmy(self, scenarioOptions.Share) + return + end + -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. ForkThread(FakeTeleportUnits, sharedCommanders, true) end @@ -957,6 +964,8 @@ function KillArmyOnACUDeath(self, shareOption) local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) if not table.empty(sharedCommanders) then + local shareTick = GetGameTick() + local oneComAlive = true while oneComAlive do oneComAlive = false @@ -968,6 +977,13 @@ function KillArmyOnACUDeath(self, shareOption) end WaitTicks(1) end + + -- if all the commanders die early, assume disconnect abuse and apply standard share condition. Only makes sense in Assassination. + local scenarioOptions = ScenarioInfo.Options + if not oneComAlive and shareTime + CommanderSafeTime <= GetGameTick() and scenarioOptions.Victory == "demoralization" then + KillArmy(self, scenarioOptions.Share) + return + end end KillArmy(self, shareOption) From 421603997d02c2577cc401f9440435555ca7bf53 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 19:52:17 -0700 Subject: [PATCH 31/61] Don't delayed recall ACUs in danger prevents abusing the recall timer to, for example, not explode armies while fighting --- lua/SimUtils.lua | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 0258931786..24e009f73d 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -97,6 +97,7 @@ function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) local fuelRatio = 0 local activeEnhancements local oldowner = unit.oldowner + local LastTickDamaged = unit.LastTickDamaged local upgradesTo = unit.UpgradesTo local defaultBuildRate local upgradeBuildTimeComplete @@ -173,6 +174,11 @@ function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) -- A F T E R + -- for the disconnect ACU share option + if LastTickDamaged then + newUnit.LastTickDamaged = LastTickDamaged + end + newUnit:SetOrientation(orientation, true) if massKilled and massKilled > 0 then @@ -948,6 +954,14 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) return end + -- filter out commanders that are not currently safe and should explode + local gameTick = GetGameTick() + for i, com in sharedCommanders do + if com.LastTickDamaged + CommanderSafeTime <= gameTick then + sharedCommanders[i] = nil + end + end + -- KillArmy waits 10 seconds before acting, while FakeTeleport waits 3 seconds, so the ACU shouldn't explode. ForkThread(FakeTeleportUnits, sharedCommanders, true) end From 9d97d468977bb41df8a012255d39c8a774566005 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 5 Jun 2024 16:24:08 -0700 Subject: [PATCH 32/61] Final iteration of lobby options text --- loc/US/strings_db.lua | 16 ++++++++++++---- lua/ui/lobby/lobbyOptions.lua | 18 +++++++++--------- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/loc/US/strings_db.lua b/loc/US/strings_db.lua index 5d31a65b1c..ed0cc95899 100644 --- a/loc/US/strings_db.lua +++ b/loc/US/strings_db.lua @@ -7742,6 +7742,7 @@ lobui_0781="Teams will be balanced with up to 5%% tolerance of best setup to mak lobui_0782="Optimal balance (Mirrored)" lobui_0783="Teams will be optimally balanced, mirrored start locations" +-- Share Options lobui_0790="Manual Unit Sharing" lobui_0791="Are players allowed to manually give units?" lobui_0792="Manual unit sharing are allowed" @@ -7764,10 +7765,17 @@ lobui_0805="This game will be rated if all the criteria for a rated game are met lobui_0806="Yes" lobui_0807="This game will not be rated." -lobui_0808="Recall Disconnected ACUs" -lobui_0809="Should disconnecting players' ACUs be recalled, preventing their explosion if they were not damaged in the last 2 minutes?" -lobui_0810="ACUs explode when their player disconnects." -lobui_0811="ACUs not damaged in the last 2 minutes are recalled when their player disconnects." +-- More share options +lobui_0808="DC ACU Share Conditions" +lobui_0809="Set what happens to a player's ACU when they disconnect. In Assassination, the DC share condition is *not* applied if the ACU was damaged 2 minutes ago or dies within 2 minutes." +lobui_0810="Explode" +lobui_0811="ACUs explode when their player disconnects." +lobui_0812="Recall" +lobui_0813="ACUs not damaged in the last 2 minutes are recalled when their player disconnects." +lobui_0814="Delayed Recall" +lobui_0815="Disconnected ACUs are shared to allies for 2 minutes or until 5 minutes into the match before it recalls." +lobui_0816="Permanent" +lobui_0817="Disconnected ACUs are permanently shared to allies." aisettings_0001="AIx Cheat Multiplier" aisettings_0002="Set the cheat multiplier for the cheating AIs." diff --git a/lua/ui/lobby/lobbyOptions.lua b/lua/ui/lobby/lobbyOptions.lua index 6c9f1a0362..2f742df9eb 100644 --- a/lua/ui/lobby/lobbyOptions.lua +++ b/lua/ui/lobby/lobbyOptions.lua @@ -279,27 +279,27 @@ globalOpts = { { default = 1, label = "DC ACU Share Conditions", - help = "Set what happens to a player's ACU when they disconnect.", + help = "Set what happens to a player's ACU when they disconnect. In Assassination, the DC share condition is *not* applied if the ACU was damaged 2 minutes ago or dies within 2 minutes.", key = 'DisconnectShareCommanders', values = { { - text = "Explode", - help = "ACUs explode when their player disconnects. In Assassination, the DC share condition is applied only if they have not been damaged in the last 2 minutes.", + text = "Explode", + help = "ACUs explode when their player disconnects.", key = 'Explode', }, { - text = "Recall", - help = "ACUs not damaged in the last 2 minutes are recalled when their player disconnects. In Assassination, the DC share condition is applied only if they have not been damaged in the last 2 minutes.", + text = "Recall", + help = "ACUs not damaged in the last 2 minutes are recalled when their player disconnects.", key = 'Recall', }, { - text = "Delayed Recall", - help = "Disconnected ACUs are shared to allies for 2 minutes or until 5 minutes into the match before it recalls. In Assassination, the DC share condition is applied to that player's units when the shared ACU dies or recalls.", + text = "Delayed Recall", + help = "Disconnected ACUs are shared to allies for 2 minutes or until 5 minutes into the match before it recalls.", key = 'RecallDelayed', }, { - text = "Permanent", - help = "Disconnected ACUs are permanently shared to allies. In Assassination, the DC share condition is applied to that player's units when the shared ACU dies.", + text = "Permanent", + help = "Disconnected ACUs are permanently shared to allies.", key = 'Permanent', }, }, From 1b91d1bb758d79b50281db5dc122359d2f149b61 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 22:18:21 -0700 Subject: [PATCH 33/61] First draft of changelog snippet Too wordy? Are the examples needed? Needs images/video. --- changelog/snippets/features.5971.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 changelog/snippets/features.5971.md diff --git a/changelog/snippets/features.5971.md b/changelog/snippets/features.5971.md new file mode 100644 index 0000000000..28d9213854 --- /dev/null +++ b/changelog/snippets/features.5971.md @@ -0,0 +1,18 @@ +- (#5971) Add two new lobby options that determine how to share units of players who disconnect (DC) from the game: DC Share Conditions and DC ACU Share Conditions +This gives players of all types a way to lessen the impact of a disconnect in their games to a varying degree. + - DC Share Conditions: Similar to the standard Share Conditions option, this determines how units are shared when a player disconnects. + - The default is to copy the standard share condition, but it can be set independently. + - To prevent abuse, the DC Share Conditions are not applied in Assassination games if the ACU takes damage or dies in 2 minutes (depending on the DC ACU Share Conditions). + - DC ACU Share Conditions: Determines how ACUs are treated when a player disconnects. They can be split into two categories: + - Instant conditions: These are applied instantly, so to prevent abuse, the DC Share Conditions are only applied if the ACU has not taken damage within the last 2 minutes. + - Explode (Default): The current behavior of ACUs, where they explode when their player leaves. + - Recall: Instead of exploding, the ACU peacefully recalls, keeping everything around intact. This can only happen if they weren't damaged in the last 2 minutes to prevent abuse. + - ACU Sharing conditions: These share the disconnecting ACU. To prevent abuse, the DC Share Conditions are only applied if the ACU doesn't die within 2 minutes of being shared. After those 2 minutes, the DC Share Conditions apply when the ACU recalls or dies. + - Delayed Recall: ACUs are very powerful, but also necessary early on, so this option shares the ACU for 2 minutes or until 5 minutes pass in the game (displayed as a countdown on the ACU). Afterwards, it recalls (except if it was damaged in 2 min, then it explodes). This gives players an opportunity to stabilize the new situation without getting a large long term advantage by controlling two ACUs. + - Permanent: For those who don't mind it, or when the ACU is too precious to lose after a timer, the ACU can be shared permanently. + + Some examples: + - Share until death players can use "Same as Share" and "Permanent" to keep players who disconnect snipeable, but not lose their entire base. + - Survival/modded players can use "FullShare" and "Permanent" to keep everything. + - Competitive players can use "FullShare" and "Delayed Recall" to not have to restart the lobby or draw because of an early disconnect, but without having an OP double ACU strategy. + - Think getting bases or reclaim from disconnects is unfair? Make them recall and desert as civilians! From 7ddb3dfbde99733b41faf7de55dc088ce0f781d6 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 22:42:21 -0700 Subject: [PATCH 34/61] Fix unknown variable error --- lua/UserSync.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/UserSync.lua b/lua/UserSync.lua index 4bee6d543b..fdeeae3461 100644 --- a/lua/UserSync.lua +++ b/lua/UserSync.lua @@ -184,7 +184,7 @@ function OnSync() secondary = secondary .. LOCF("\nShare Condition after: %s", LOC(transferReasonToAnnouncementTitle[shareOption] or "Share Until Death")) end - UIUtil.CreateAnnouncementStd(primary, secondary, control) + UIUtil.CreateAnnouncementStd(primary, secondary, nil) end end end From 7e6a3baedd223b85623e22ddf9540e0703040a53 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 22:52:58 -0700 Subject: [PATCH 35/61] Fix sharing ACU in partial share --- lua/SimUtils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 24e009f73d..3af6f3eb75 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -886,7 +886,7 @@ function KillArmy(self, shareOption) elseif shareOption == 'PartialShare' then KillSharedUnits(selfIndex, categories.ALLUNITS - categories.STRUCTURE - categories.ENGINEER) ReturnBorrowedUnits(self) - TransferUnitsToHighestBrain(self, BrainCategories.Allies, true, categories.STRUCTURE + categories.ENGINEER, "PartialShare") + TransferUnitsToHighestBrain(self, BrainCategories.Allies, true, categories.STRUCTURE + categories.ENGINEER - categories.COMMAND, "PartialShare") TransferOwnershipOfBorrowedUnits(BrainCategories.Allies, selfIndex) else GetBackUnits(selfIndex, BrainCategories.Allies) From 021358d3cc0fc41efa930836e92e45984168c8f1 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 23:11:53 -0700 Subject: [PATCH 36/61] Fix concating nil when transferring 0 units --- lua/SimUtils.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 3af6f3eb75..18c9ad5a06 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -703,12 +703,11 @@ function TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesT end if units and not table.empty(units) then local newUnits = TransferUnitsOwnership(units, brain.index, false, true) - table.destructiveCat(totalNewUnits, newUnits) - local givenUnitCount = table.getn(newUnits) + -- we might not transfer any newUnits + if not table.empty(newUnits) then + table.destructiveCat(totalNewUnits, newUnits) - -- only show message when we actually gift that player some units - if givenUnitCount > 0 then Sync.ArmyTransfer = { { from = self.index, to = brain.index, From ae92573c66584da1b732293419c6ca55cef2ee95 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Sat, 8 Jun 2024 23:17:04 -0700 Subject: [PATCH 37/61] Simplify TransferUnitsToKiller Also fix transferring 0 units to self --- lua/SimUtils.lua | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 18c9ad5a06..fc7596ecae 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -844,17 +844,21 @@ end --- Transfer units to the player who killed me ---@param self AIBrain local function TransferUnitsToKiller(self) - local selfIndex = self:GetArmyIndex() - local killerIndex = 0 local units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) + if units and not table.empty(units) then + local killerIndex + if ScenarioInfo.Options.Victory == 'demoralization' then - killerIndex = ArmyBrains[selfIndex].CommanderKilledBy or selfIndex - TransferUnitsToBrain(self, { ArmyBrains[killerIndex] }, true, nil, "TransferToKiller") + killerIndex = self.CommanderKilledBy else - killerIndex = ArmyBrains[selfIndex].LastUnitKilledBy or selfIndex + killerIndex = self.LastUnitKilledBy + end + + if killerIndex then TransferUnitsToBrain(self, { ArmyBrains[killerIndex] }, true, nil, "TransferToKiller") end + -- if not transferred, units will simply be killed end WaitSeconds(1) end From 68dc86b27a86848ee39a8cd462922bef20cf4d5e Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Mon, 10 Jun 2024 23:49:18 -0700 Subject: [PATCH 38/61] Allow `noRestrictions` to transfer to defeated brains Fixes GetBackUnits(), specifically for shared units in traitor mode, compared to the base develop branch. --- lua/SimUtils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index fc7596ecae..42c95b6478 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -45,7 +45,7 @@ local sharedUnits = {} ---@return Unit[]? function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) local toBrain = GetArmyBrain(toArmy) - if not toBrain or toBrain:IsDefeated() or not units or table.empty(units) then + if not toBrain or (not noRestrictions and toBrain:IsDefeated()) or not units or table.empty(units) then return end local categoriesENGINEERSTATION = categories.ENGINEERSTATION From 002ec2482596f437cdd98dfcd8f757340e98d7d1 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 11 Jun 2024 01:11:58 -0700 Subject: [PATCH 39/61] Delete announcementNew.lua --- lua/ui/game/announcementNew.lua | 197 -------------------------------- 1 file changed, 197 deletions(-) delete mode 100644 lua/ui/game/announcementNew.lua diff --git a/lua/ui/game/announcementNew.lua b/lua/ui/game/announcementNew.lua deleted file mode 100644 index dfa9f35d9f..0000000000 --- a/lua/ui/game/announcementNew.lua +++ /dev/null @@ -1,197 +0,0 @@ -local LayoutHelpers = import("/lua/maui/layouthelpers.lua") -local Layouter = LayoutHelpers.ReusedLayoutFor - -local UIUtil = import("/lua/ui/uiutil.lua") -local Group = import("/lua/maui/group.lua").Group -local Bitmap = import("/lua/maui/bitmap.lua").Bitmap - -local MATH_Lerp = MATH_Lerp - -local bg = false - ---- Create an announcement UI for sending general messages to the user ----@param text string ----@param goalControl? Control The control where the announcement appears out of. ----@param secondaryText? string ----@param onFinished? function -function CreateAnnouncement(text, goalControl, secondaryText, onFinished) - local frame = GetFrame(0) - - if not goalControl then - -- make it originate from the top - goalControl = Group(frame) - goalControl.Left:Set(function() return frame.Left() + 0.49 * frame.Right() end) - goalControl.Right:Set(function() return frame.Left() + 0.51 * frame.Right() end) - goalControl.Top = frame.Top - goalControl.Bottom = frame.Top - end - - local scoreDlg = import("/lua/ui/dialogs/score.lua") - if scoreDlg.dialog then - if onFinished then - onFinished() - end - return - end - - if bg then - if bg.OnFinished then - bg.OnFinished() - end - bg.OnFrame = function(self, delta) - local newAlpha = self:GetAlpha() - (delta*2) - if newAlpha < 0 then - newAlpha = 0 - self:Destroy() - bg.OnFinished = nil - bg = false - CreateAnnouncement(text, goalControl, secondaryText, onFinished) - end - self:SetAlpha(newAlpha, true) - end - return - end - - PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Open'})) - - bg = Layouter(Bitmap(frame, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_m.dds'))) - :Height(0):Width(0):Over(frame, 1) - - local textGroup = Group(bg:Get()) - - if goalControl == nil then - goalControl = textGroup - end - bg = bg:AtCenterIn(goalControl):End() - - bg.border = CreateBorder(bg) - - local text = Layouter(UIUtil.CreateText(textGroup, text, 22, UIUtil.titleFont)) - :AtCenterIn(frame, -250) - :DropShadow(true):Color(UIUtil.fontColor) - :NeedsFrameUpdate(true):End() - - if secondaryText then - local secText = Layouter(UIUtil.CreateText(textGroup, secondaryText, 18, UIUtil.bodyFont)) - :DropShadow(true):Color(UIUtil.fontColor) - :Below(text, 10):AtHorizontalCenterIn(text):End() - Layouter(textGroup):Top(text.Top) - :Left(function() return math.min(secText.Left(), text.Left()) end) - :Right(function() return math.max(secText.Right(), text.Right()) end) - :Bottom(secText.Bottom):End() - else - LayoutHelpers.FillParent(textGroup, text) - end - bg:DisableHitTest(true) - - textGroup:SetAlpha(0, true) - - bg.OnFinished = onFinished - - bg.time = 0 - bg:SetNeedsFrameUpdate(true) - bg.CloseSoundPlayed = false - - local tGTop, tGLeft, tGRight, tGBottom, tGHeight, tGWidth = textGroup.Top(), textGroup.Left(), textGroup.Right(), textGroup.Bottom(), textGroup.Height(), textGroup.Width() - local gCTop, gCLeft, gCRight, gCBottom, gCHeight, gCWidth = goalControl.Top(), goalControl.Left(), goalControl.Right(), goalControl.Bottom(), goalControl.Height(), goalControl.Width() - - bg.OnFrame = function(self, delta) - local time = self.time + delta - self.time = time - - -- expansion animation - if time < .2 then - local lerpMult = MATH_Lerp(time, 0, 0.2, 0, 1) - self.Top:Set(MATH_Lerp(lerpMult, gCTop, tGTop)) - self.Left:Set(MATH_Lerp(lerpMult, gCLeft, tGLeft)) - self.Right:Set(MATH_Lerp(lerpMult, gCRight, tGRight)) - self.Bottom:Set(MATH_Lerp(lerpMult, gCBottom, tGBottom)) - self.Height:Set(MATH_Lerp(lerpMult, gCHeight, tGHeight)) - self.Width:Set(MATH_Lerp(lerpMult, gCWidth, tGWidth)) - -- stationary - elseif time > .2 and time < 3.5 and not self.TextGroupReached then - Layouter(self):Fill(textGroup):End() - self.TextGroupReached = true - -- contraction animation - elseif time >= 3.5 and time < 3.7 then - if not self.CloseSoundPlayed then - PlaySound(Sound({Bank = 'Interface', Cue = 'UI_Announcement_Close'})) - self.CloseSoundPlayed = true - end - local lerpMult = MATH_Lerp(time, 3.5, 3.7, 0, 1) - self.Top:Set(MATH_Lerp(lerpMult, tGTop, gCTop)) - self.Left:Set(MATH_Lerp(lerpMult, tGLeft, gCLeft)) - self.Right:Set(MATH_Lerp(lerpMult, tGRight, gCRight)) - self.Bottom:Set(MATH_Lerp(lerpMult, tGBottom, gCBottom)) - self.Height:Set(MATH_Lerp(lerpMult, tGHeight, gCHeight)) - self.Width:Set(MATH_Lerp(lerpMult, tGWidth, gCWidth)) - end - - local textGroupAlpha = textGroup:GetAlpha() - local textAlpha = text:GetAlpha() - -- fade out the text at the end of the announcement - if time > 3 and textGroupAlpha ~= 0 then - textGroup:SetAlpha(math.max(textGroupAlpha - (delta * 2), 0), true) - -- fade in the text when the announcement appears - elseif time > .2 and time < 3 and textAlpha ~= 1 then - textGroup:SetAlpha(math.min(textAlpha + (delta * 2), 1), true) - end - if goalControl == textGroup then - self:SetAlpha(textGroupAlpha, true) - end - - if time > 3.7 then - if bg.OnFinished then - bg.OnFinished() - end - bg:Destroy() - bg.OnFinished = nil - bg = false - end - end - - if import("/lua/ui/game/gamemain.lua").gameUIHidden then - bg:Hide() - end -end - ---- Instantly hides the current announcement -function Contract() - if bg then - bg:Hide() - end -end - ---- Instantly shows the current announcement -function Expand() - if bg then - bg:Show() - end -end - ---- Create a border around the `parent` with the `filter-ping-list-panel` files ----@param parent Control ----@return Bitmap[] border # 8 Bitmap objects: top left, top middle, top right, middle left, middle right, bottom left, bottom middle, bottom right -function CreateBorder(parent) - -- t, m, b = top, middle, bottm - -- l, m, r = left, middle, right - local tl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ul.dds')) - local tm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_horz_um.dds')) - local tr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ur.dds')) - local ml = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_l.dds')) - local mr = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_vert_r.dds')) - local bl = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_ll.dds')) - local bm = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lm.dds')) - local br = Bitmap(parent, UIUtil.SkinnableFile('/game/filter-ping-list-panel/panel_brd_lr.dds')) - - Layouter(tl):TopLeftOf(parent):End() - Layouter(tm):CenteredAbove(parent):FillHorizontally(parent):End() - Layouter(tr):TopRightOf(parent):End() - Layouter(ml):CenteredLeftOf(parent):FillVertically(parent):End() - Layouter(mr):CenteredRightOf(parent):FillVertically(parent):End() - Layouter(bl):BottomLeftOf(parent):End() - Layouter(bm):CenteredBelow(parent):FillHorizontally(parent):End() - Layouter(br):BottomRightOf(parent):End() - - return { tl, tm, tr, ml, mr, bl, bm, br } -end \ No newline at end of file From 6838b665d7b7901634ab52e46422aa061d424e95 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 12 Jun 2024 02:41:44 -0700 Subject: [PATCH 40/61] Fix typo in annotation comment --- lua/maui/control.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/maui/control.lua b/lua/maui/control.lua index 734a97ef10..13bf2b5bb5 100644 --- a/lua/maui/control.lua +++ b/lua/maui/control.lua @@ -155,7 +155,7 @@ Control = ClassUI(moho.control_methods) { ScrollSetTop = function(self, axis, top) end, - --- Called to determine if the control is scrollable on a particular access. Must return true or false. + --- Called to determine if the control is scrollable on a particular axis. Must return true or false. ---@param self Control ---@param axis ScrollAxis ---@return boolean From 62396c936f6111c1c5a98558b432e9d1c3ceee72 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 12 Jun 2024 05:22:07 -0700 Subject: [PATCH 41/61] Init LastTickDamaged to 0 instead of checking for nil Fixes KillArmyOnDelayedRecall function's LastTickDamaged check --- lua/aibrain.lua | 2 +- lua/sim/units/ACUUnit.lua | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 489906f68c..1de5bc9146 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -496,7 +496,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) for _, com in commanders do - if com.LastTickDamaged == nil or com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then + if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then table.insert(safeCommanders, com) end end diff --git a/lua/sim/units/ACUUnit.lua b/lua/sim/units/ACUUnit.lua index 67c3a1eb16..63d5db4150 100644 --- a/lua/sim/units/ACUUnit.lua +++ b/lua/sim/units/ACUUnit.lua @@ -3,6 +3,8 @@ local CommandUnit = import("/lua/sim/units/commandunit.lua").CommandUnit ---@class ACUUnit : CommandUnit ---@field LastTickDamaged number ACUUnit = ClassUnit(CommandUnit) { + LastTickDamaged = 0, + -- The "commander under attack" warnings. ---@param self ACUUnit ---@param bpShield any From 4725ad4e267cf0f54f94710dd07a5ecb0781207e Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 12 Jun 2024 05:22:49 -0700 Subject: [PATCH 42/61] Fix KillArmyOnDelayedRecall damage check logic --- lua/SimUtils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 42c95b6478..efa37b95ce 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -960,7 +960,7 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) -- filter out commanders that are not currently safe and should explode local gameTick = GetGameTick() for i, com in sharedCommanders do - if com.LastTickDamaged + CommanderSafeTime <= gameTick then + if com.LastTickDamaged + CommanderSafeTime > gameTick then sharedCommanders[i] = nil end end From 71f0695b46889d240275794af164a574ddb2d293 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 12 Jun 2024 06:40:14 -0700 Subject: [PATCH 43/61] Kill ACUs when needed to prevent sharing Explode -> all acus die Recall/Recall Delayed -> unsafe acus die --- lua/SimUtils.lua | 2 ++ lua/aibrain.lua | 16 +++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index efa37b95ce..36e924e4cd 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -962,6 +962,8 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) for i, com in sharedCommanders do if com.LastTickDamaged + CommanderSafeTime > gameTick then sharedCommanders[i] = nil + -- explode unsafe ACUs because KillArmy might not + com:Kill() end end diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 1de5bc9146..4832b72c69 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -495,9 +495,19 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local safeCommanders = {} local commanders = self:GetListOfUnits(categories.COMMAND, false) - for _, com in commanders do - if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then - table.insert(safeCommanders, com) + if shareAcuOption == 'Recall' then + for _, com in commanders do + if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then + table.insert(safeCommanders, com) + else + -- explode unsafe ACUs because KillArmy might not + com:Kill() + end + end + else + -- explode all the ACUs so they don't get shared + for _, com in commanders do + com:Kill() end end From d705d9ab73e8f90f0b634acf8d0575856351eb6e Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Wed, 12 Jun 2024 06:44:37 -0700 Subject: [PATCH 44/61] Handle recalling ACUs don't stack recalls on them also don't count them as an ACU that keeps your army alive --- lua/SimUtils.lua | 8 ++++++++ lua/sim/MatchState.lua | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 36e924e4cd..8491cce809 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -935,6 +935,14 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) if not table.empty(sharedCommanders) then -- create a countdown to show when the ACU recalls (similar to the one used for timed self-destruct) for _, com in sharedCommanders do + -- don't recall shared ACUs + if com.RecallingAfterDefeat then + sharedCommanders[i] = nil + continue + end + -- The shared ACUs don't count as keeping the army in the game since they will eventually be removed from the game. + -- see MatchState.lua CollectDefeatedBrains + com.RecallingAfterDefeat = true StartCountdown(com.EntityId, math.floor((shareTime - GetGameTick())/10)) end diff --git a/lua/sim/MatchState.lua b/lua/sim/MatchState.lua index ed51975a8d..228cb52fa6 100644 --- a/lua/sim/MatchState.lua +++ b/lua/sim/MatchState.lua @@ -41,7 +41,7 @@ local function CollectDefeatedBrains(aliveBrains, condition, delay) -- critical units found, make sure they all exist properly local oneCriticalUnitAlive = false for _, unit in criticalUnits do - if (not IsDestroyed(unit)) and (unit:GetFractionComplete() == 1) then + if (not IsDestroyed(unit)) and (unit:GetFractionComplete() == 1) and not unit.RecallingAfterDefeat then oneCriticalUnitAlive = true break end From 3bb863cfa9ab1f5264682f84d2ab84644718e9f1 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:37:17 -0800 Subject: [PATCH 45/61] Specify invalid share condition --- lua/SimUtils.lua | 2 +- lua/aibrain.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 8491cce809..b96c542313 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -900,7 +900,7 @@ function KillArmy(self, shareOption) elseif shareOption == 'Defectors' then TransferUnitsToHighestBrain(self, BrainCategories.Enemies, true, "Defectors") else -- Something went wrong in settings. Act like share until death to avoid abuse - WARN('Invalid share condition was used for this game. Defaulting to killing all units') + WARN('Invalid share condition was used for this game: `' .. (shareOption or 'nil') .. '` Defaulting to killing all units') KillSharedUnits(selfIndex) ReturnBorrowedUnits(self) end diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 4832b72c69..99b39e023a 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -484,7 +484,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local shareOption = ScenarioInfo.Options.DisconnectShare local shareAcuOption = ScenarioInfo.Options.DisconnectShareCommanders local victoryOption = ScenarioInfo.Options.Victory - + if shareOption == 'SameAsShare' then shareOption = ScenarioInfo.Options.Share end @@ -540,7 +540,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM ForkThread(KillArmyOnACUDeath, self, shareOption) end else - WARN('Invalid disconnection ACU share condition was used for this game. Defaulting to exploding ACU.') + WARN('Invalid disconnection ACU share condition was used for this game: `' .. (shareAcuOption or 'nil') .. '` Defaulting to exploding ACU.') ForkThread(KillArmy, self, shareOption) end From dba06c3f338b088b5731e92d17b9863ca1661a8f Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 19:52:33 -0800 Subject: [PATCH 46/61] Improve `TransferUnitsToHighestBrain` - Don't put a `ratings` field into AIBrains when transferring units - Give negative rated players some room - Don't count AI rating when transferring --- lua/SimUtils.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index b96c542313..f5be2d70d1 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -757,16 +757,20 @@ end function TransferUnitsToHighestBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason) if not table.empty(brains) then local ratings = ScenarioInfo.Options.Ratings + ---@type table + local brainRatings = {} for _, brain in brains do - if ratings[brain.Nickname] then - brain.rating = ratings[brain.Nickname] + -- AI can have a rating set in the lobby + if brain.BrainType == "Human" and ratings[brain.Nickname] then + brainRatings[brain] = ratings[brain.Nickname] else -- if there is no rating, create a fake negative rating based on score - brain.rating = -1 / CalculateBrainScore(brain) + -- leave -1000 rating for negative rated players + brainRatings[brain] = -1000 - 1 / CalculateBrainScore(brain) end end -- sort brains by rating - table.sort(brains, function(a, b) return a.rating > b.rating end) + table.sort(brains, function(a, b) return brainRatings[a] > brainRatings[b] end) return TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesToTransfer, reason) end end From d5a3edcc65ad262c1bff779248f0851dff847d20 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 21:14:52 -0800 Subject: [PATCH 47/61] Annotate `CMauiItemList:SetNewColors` --- engine/User/CMauiItemList.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/engine/User/CMauiItemList.lua b/engine/User/CMauiItemList.lua index 421f17d3c7..b6e597c794 100644 --- a/engine/User/CMauiItemList.lua +++ b/engine/User/CMauiItemList.lua @@ -82,7 +82,9 @@ end ---@param background Color ---@param selectedForeground Color ---@param selectedBackground Color -function CMauiItemList:SetNewColors(foreground, background, selectedForeground, selectedBackground) +---@param mouseoverForeground Color +---@param mouseoverBackground Color +function CMauiItemList:SetNewColors(foreground, background, selectedForeground, selectedBackground, mouseoverForeground, mouseoverBackground) end --- Sets the font to use in this ItemList control From bea1ba0f23790a69795aa45aa5fe13f6266cf7fc Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 21:25:44 -0800 Subject: [PATCH 48/61] Fix AutoLobbyConnectionMatrixDot --- .../lobby/autolobby/AutolobbyConnectionMatrixDot.lua | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua index 8d7ef1a853..e4fa67f671 100644 --- a/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua +++ b/lua/ui/lobby/autolobby/AutolobbyConnectionMatrixDot.lua @@ -20,8 +20,6 @@ --** SOFTWARE. --****************************************************************************************************** -local EnumColors = import("/lua/shared/color.lua").EnumColors - local Bitmap = import("/lua/maui/bitmap.lua").Bitmap --- A small dot that represents the connection status between players. @@ -79,13 +77,13 @@ local AutolobbyConnectionMatrixDot = Class(Bitmap) { ---@param status UIPeerLaunchStatus SetStatus = function(self, status) if status == 'Unknown' then - self:SetSolidColor(EnumColors.Blue) + self:SetSolidColor("Blue") elseif status == 'Rejoining' then - self:SetSolidColor(EnumColors.HotPink) + self:SetSolidColor("HotPink") elseif status == 'Missing local peers' then - self:SetSolidColor(EnumColors.Orange) + self:SetSolidColor("Orange") elseif status == 'Ready' then - self:SetSolidColor(EnumColors.Green) + self:SetSolidColor("Green") end end, From 0e5cacfe3d6e884c6853ed234e9c04899a60e73a Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:27:45 -0800 Subject: [PATCH 49/61] Update LaunchFAInstances.ps1 --- scripts/LaunchFAInstances.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/LaunchFAInstances.ps1 b/scripts/LaunchFAInstances.ps1 index 123faab0e1..895a76d53c 100644 --- a/scripts/LaunchFAInstances.ps1 +++ b/scripts/LaunchFAInstances.ps1 @@ -25,7 +25,7 @@ if (Test-Path $debuggerExecutable) { } # Command-line arguments common for all instances -$baseArguments = '/init "init_dev.lua" /EnableDiskWatch /nomovie /RunWithTheWind /gameoptions CheatsEnabled:true GameSpeed:adjustable ' +$baseArguments = '/init "init_dev.lua" /EnableDiskWatch /nomovie /RunWithTheWind /gameoptions CheatsEnabled:true GameSpeed:adjustable DisconnectShareCommanders:Permanent DisconnectShare:SameAsShare' # Game-specific settings $hostProtocol = "udp" From d1442ea69ce139a3b438202e98bccac2cf9413a4 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:28:29 -0800 Subject: [PATCH 50/61] Fix in SimUtils TransferUnitsToHighestBrain usage --- lua/SimUtils.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index f5be2d70d1..68ad58bf19 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -902,7 +902,7 @@ function KillArmy(self, shareOption) elseif shareOption == 'TransferToKiller' then TransferUnitsToKiller(self) elseif shareOption == 'Defectors' then - TransferUnitsToHighestBrain(self, BrainCategories.Enemies, true, "Defectors") + TransferUnitsToHighestBrain(self, BrainCategories.Enemies, true, nil, "Defectors") else -- Something went wrong in settings. Act like share until death to avoid abuse WARN('Invalid share condition was used for this game: `' .. (shareOption or 'nil') .. '` Defaulting to killing all units') KillSharedUnits(selfIndex) From 72475a42e98f179ae4485ce7d54dc0ede5f94201 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Tue, 24 Dec 2024 23:28:45 -0800 Subject: [PATCH 51/61] Remove brain.index --- engine/Sim.lua | 3 +++ lua/SimUtils.lua | 14 ++++++-------- lua/aibrain.lua | 3 ++- lua/simInit.lua | 3 ++- lua/system/utils.lua | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) diff --git a/engine/Sim.lua b/engine/Sim.lua index 666da4fd85..3ce2615cf6 100644 --- a/engine/Sim.lua +++ b/engine/Sim.lua @@ -34,6 +34,9 @@ ---@alias ReclaimObject moho.prop_methods | moho.unit_methods ---@alias TargetObject moho.prop_methods | moho.unit_methods | moho.projectile_methods +---@type table +ArmyBrains = {} + --- restricts the army from building the unit category ---@param army Army ---@param category EntityCategory diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index 68ad58bf19..6080011529 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -651,13 +651,13 @@ function UpdateUnitCap(deadArmy) return end local aliveCount = 0 + ---@type table local alive = {} local caps = {} for index, brain in ArmyBrains do if (mode == 'all' or (mode == 'allies' and IsAlly(deadArmy, index))) and not ArmyIsCivilian(index) then if not brain:IsDefeated() then - brain.index = index aliveCount = aliveCount + 1 alive[aliveCount] = brain local cap = GetArmyUnitCap(index) @@ -669,7 +669,7 @@ function UpdateUnitCap(deadArmy) if aliveCount > 0 then local capChng = GetArmyUnitCap(deadArmy) / aliveCount for i, brain in alive do - SetArmyUnitCap(brain.index, caps[i] + capChng) + SetArmyUnitCap(brain.Army, caps[i] + capChng) end end end @@ -687,7 +687,7 @@ function TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesT if transferUnfinishedUnits then local indexes = {} for _, brain in brains do - table.insert(indexes, brain.index) + table.insert(indexes, brain.Army) end units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) TransferUnfinishedUnitsAfterDeath(units, indexes) @@ -702,15 +702,15 @@ function TransferUnitsToBrain(self, brains, transferUnfinishedUnits, categoriesT units = self:GetListOfUnits(categories.ALLUNITS - categories.WALL - categories.COMMAND, false) end if units and not table.empty(units) then - local newUnits = TransferUnitsOwnership(units, brain.index, false, true) + local newUnits = TransferUnitsOwnership(units, brain.Army, false, true) -- we might not transfer any newUnits if not table.empty(newUnits) then table.destructiveCat(totalNewUnits, newUnits) Sync.ArmyTransfer = { { - from = self.index, - to = brain.index, + from = self.Army, + to = brain.Army, reason = reason or "FullShare" } } end @@ -731,8 +731,6 @@ function GetAllegianceCategories(armyIndex) local BrainCategories = { Enemies = {}, Civilians = {}, Allies = {} } for index, brain in ArmyBrains do - brain.index = index - if not brain:IsDefeated() and armyIndex ~= index then if ArmyIsCivilian(index) then table.insert(BrainCategories.Civilians, brain) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 99b39e023a..52635d9b2c 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -57,6 +57,7 @@ local CategoriesDummyUnit = categories.DUMMYUNIT ---@class AIBrain: FactoryManagerBrainComponent, StatManagerBrainComponent, JammerManagerBrainComponent, EnergyManagerBrainComponent, StorageManagerBrainComponent, moho.aibrain_methods ---@field AI boolean +---@field Army Army # self:GetArmyIndex() ---@field Name string # Army name ---@field Nickname string # Player / AI / character name ---@field Status BrainState @@ -447,7 +448,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM -- OnDefeat runs after AbandonedByPlayer, so we need to prevent killing the army twice if self.Status == 'Defeat' then return - end + end self.Status = 'Defeat' local selfIndex = self:GetArmyIndex() diff --git a/lua/simInit.lua b/lua/simInit.lua index 46ce9a35ad..bc311664ad 100644 --- a/lua/simInit.lua +++ b/lua/simInit.lua @@ -103,7 +103,8 @@ function SetupSession() end -- LOG('SetupSession: ', repr(ScenarioInfo)) - ---@type AIBrain[] + + ---@type table ArmyBrains = {} diff --git a/lua/system/utils.lua b/lua/system/utils.lua index 4b108ed8ed..b289b3c0b0 100644 --- a/lua/system/utils.lua +++ b/lua/system/utils.lua @@ -63,7 +63,7 @@ if not rawget(table, 'empty') then -- - https://github.com/FAForever/FA-Binary-Patches/pull/98 --- table.empty(t) returns true iff t has no keys/values. - ---@param t table + ---@param t? table ---@return boolean function table.empty(t) if type(t) ~= 'table' then return true end From fa1a3fb4c7025685f1827f5c65b7c3d1bd62f3e8 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 01:37:29 -0800 Subject: [PATCH 52/61] Fix aibrain RecallArmyOnDefeat --- lua/aibrain.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 52635d9b2c..21206668b7 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -18,8 +18,7 @@ local TransferUnitsToBrain = import("/lua/simutils.lua").TransferUnitsToBrain local TransferUnitsToHighestBrain = import("/lua/simutils.lua").TransferUnitsToHighestBrain local UpdateUnitCap = import("/lua/simutils.lua").UpdateUnitCap local SimPingOnArmyDefeat = import("/lua/simping.lua").OnArmyDefeat -local RecallOnArmyDefeat = import("/lua/sim/Recall.lua") -local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore +local RecallOnArmyDefeat = import("/lua/sim/Recall.lua").OnArmyDefeat local FakeTeleportUnits = import("/lua/scenarioframework.lua").FakeTeleportUnits local StorageManagerBrainComponent = import("/lua/aibrains/components/StorageManagerBrainComponent.lua").StorageManagerBrainComponent @@ -1262,3 +1261,9 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM --#endregion ------------------------------------------------------------------------------- } + +---#region backwards compatibility + +local CalculateBrainScore = import("/lua/sim/score.lua").CalculateBrainScore + +--#endregion From 642903ea5efeb6b6714726d55d728aa2bc20d04b Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 01:39:11 -0800 Subject: [PATCH 53/61] Fix intellisense warning The same file is required with different names.Lua Diagnostics.(different-requires) simInit.lua(310, 12): --- lua/simInit.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/simInit.lua b/lua/simInit.lua index bc311664ad..1f1c87b67a 100644 --- a/lua/simInit.lua +++ b/lua/simInit.lua @@ -308,7 +308,7 @@ function BeginSession() import("/lua/sim/scenarioutilities.lua").CreateResources() import("/lua/sim/score.lua").init() - import("/lua/sim/recall.lua").init() + import("/lua/sim/Recall.lua").init() -- other logic at the start of the game -- From 028e2e91b47241d0fe96f7303eef6d21d73ec486 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 01:39:39 -0800 Subject: [PATCH 54/61] Fix the return type of CreateBorder --- lua/ui/game/announcement.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/ui/game/announcement.lua b/lua/ui/game/announcement.lua index ff23c1a394..5cf034c6be 100644 --- a/lua/ui/game/announcement.lua +++ b/lua/ui/game/announcement.lua @@ -187,7 +187,7 @@ end --- Create a border around the `parent` with the `filter-ping-list-panel` files ---@param parent Control ----@return Bitmap[] border # 8 Bitmap objects: top left, top middle, top right, middle left, middle right, bottom left, bottom middle, bottom right +---@return { tl: Bitmap, tm: Bitmap, tr: Bitmap, ml: Bitmap, mr: Bitmap, bl: Bitmap, bm: Bitmap, br: Bitmap } border function CreateBorder(parent) -- t, m, b = top, middle, bottm -- l, m, r = left, middle, right @@ -209,5 +209,5 @@ function CreateBorder(parent) Layouter(bm):CenteredBelow(parent):FillHorizontally(parent):End() Layouter(br):BottomRightOf(parent):End() - return { tl, tm, tr, ml, mr, bl, bm, br } -end \ No newline at end of file + return { ["tl"] = tl, ["tm"] = tm, ["tr"] = tr, ["ml"] = ml, ["mr"] = mr, ["bl"] = bl, ["bm"] = bm, ["br"] = br } +end From 59c3ae297bfa2255f7b1ff9576baa3e7275d18ca Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:13:11 -0800 Subject: [PATCH 55/61] Fix runtime errors/intellisense warnings --- lua/SimUtils.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index eae4e0dc25..c884bd6d76 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -935,12 +935,12 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) -- Share units including ACUs and walls and keep track of ACUs local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectShareTemporary") - local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) + local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits or {}) -- non-assassination games could have an army abandon without having any commanders if not table.empty(sharedCommanders) then -- create a countdown to show when the ACU recalls (similar to the one used for timed self-destruct) - for _, com in sharedCommanders do + for i, com in sharedCommanders do -- don't recall shared ACUs if com.RecallingAfterDefeat then sharedCommanders[i] = nil @@ -994,7 +994,7 @@ function KillArmyOnACUDeath(self, shareOption) -- Share units including ACUs and walls and keep track of ACUs local brainCategories = GetAllegianceCategories(self:GetArmyIndex()) local newUnits = TransferUnitsToHighestBrain(self, brainCategories.Allies, true, categories.ALLUNITS, "DisconnectSharePermanent") - local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits) + local sharedCommanders = EntityCategoryFilterDown(categories.COMMAND, newUnits or {}) if not table.empty(sharedCommanders) then local shareTick = GetGameTick() @@ -1013,7 +1013,7 @@ function KillArmyOnACUDeath(self, shareOption) -- if all the commanders die early, assume disconnect abuse and apply standard share condition. Only makes sense in Assassination. local scenarioOptions = ScenarioInfo.Options - if not oneComAlive and shareTime + CommanderSafeTime <= GetGameTick() and scenarioOptions.Victory == "demoralization" then + if not oneComAlive and shareTick + CommanderSafeTime <= GetGameTick() and scenarioOptions.Victory == "demoralization" then KillArmy(self, scenarioOptions.Share) return end From 4c84ede9876a1d631cf5c18878c7ade9e0d50e84 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:13:21 -0800 Subject: [PATCH 56/61] Update ChangeUnitArmy annotation --- engine/Sim.lua | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/engine/Sim.lua b/engine/Sim.lua index 3ce2615cf6..368dfb73d2 100644 --- a/engine/Sim.lua +++ b/engine/Sim.lua @@ -87,13 +87,14 @@ end function AttachBeamToEntity(emitter, entity, bone, army) end --- engine patched to allow commanders to be able to be shared - ---- changes the army of a unit, returning the new unit and destroying the old one +--- Changes the army of a unit, returning the new unit and destroying the old one +--- Modified by an engine patch to allow commanders to be given. +--- `COMMAND` units are filtered out in SimHooks.lua for legacy compatibility. ---@param unit Unit ---@param army Army ----@return Unit -function ChangeUnitArmy(unit, army) +---@param allowCommanders? boolean +---@return Unit|nil +function ChangeUnitArmy(unit, army, allowCommanders) end --- returns true if cheats are enabled and logs the cheat attempt no matter what From b9b0b02bdf4e652cb3778e8736bda1386dcc6dd2 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:21:45 -0800 Subject: [PATCH 57/61] Fix the enhancement disabling script apparently I didn't notice the scope issue with `activeEnhancements` --- lua/SimUtils.lua | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index c884bd6d76..b85d1a1255 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -264,6 +264,17 @@ function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) if unit.OnGiven then unit:OnGiven(newUnit) end + + -- disable all weapons, enable with a delay + for k = 1, unit.WeaponCount do + local weapon = unit:GetWeapon(k) + -- Weapons disabled by enhancement shouldn't be re-enabled unless the enhancement is built + local enablingEnhancement = weapon.Blueprint.EnabledByEnhancement + if not enablingEnhancement or (activeEnhancements and activeEnhancements[enablingEnhancement]) then + weapon:SetEnabled(false) + weapon:ForkThread(TransferUnitsOwnershipDelayedWeapons) + end + end end if not captured then @@ -278,20 +289,6 @@ function TransferUnitsOwnership(units, toArmy, captured, noRestrictions) end end - -- add delay on turning on each weapon - for _, unit in newUnits do - -- disable all weapons, enable with a delay - for k = 1, unit.WeaponCount do - local weapon = unit:GetWeapon(k) - -- Weapons disabled by enhancement shouldn't be re-enabled unless the enhancement is built - local enablingEnhancement = weapon.Blueprint.EnabledByEnhancement - if not enablingEnhancement or (activeEnhancements and activeEnhancements[enablingEnhancement]) then - weapon:SetEnabled(false) - weapon:ForkThread(TransferUnitsOwnershipDelayedWeapons) - end - end - end - return newUnits end From 1ee4618987a09898c4cc50823b608c2da998a59c Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:29:06 -0800 Subject: [PATCH 58/61] Fix recall conditional --- lua/aibrain.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 21206668b7..46e959d30a 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -497,7 +497,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) if shareAcuOption == 'Recall' then for _, com in commanders do - if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then + if com.LastTickDamaged + CommanderSafeTime > GetGameTick() then table.insert(safeCommanders, com) else -- explode unsafe ACUs because KillArmy might not From c56f7713f2182ddecda90466537bee3156e3575b Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:34:20 -0800 Subject: [PATCH 59/61] Revert "Fix recall conditional" This reverts commit 1ee4618987a09898c4cc50823b608c2da998a59c. --- lua/aibrain.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 46e959d30a..21206668b7 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -497,7 +497,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) if shareAcuOption == 'Recall' then for _, com in commanders do - if com.LastTickDamaged + CommanderSafeTime > GetGameTick() then + if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then table.insert(safeCommanders, com) else -- explode unsafe ACUs because KillArmy might not From b5c2b9df5182ab05c25867d844b47f93b83c2c13 Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:37:59 -0800 Subject: [PATCH 60/61] Reuse GetGameTick call --- lua/aibrain.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 21206668b7..431c83079e 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -496,8 +496,9 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM local commanders = self:GetListOfUnits(categories.COMMAND, false) if shareAcuOption == 'Recall' then + local gameTick = GetGameTick() for _, com in commanders do - if com.LastTickDamaged + CommanderSafeTime <= GetGameTick() then + if com.LastTickDamaged + CommanderSafeTime <= gameTick then table.insert(safeCommanders, com) else -- explode unsafe ACUs because KillArmy might not From 3af9ca230f0c1a15aac3b675ece8b54f0bc0cd7f Mon Sep 17 00:00:00 2001 From: lL1l1 <82986251+lL1l1@users.noreply.github.com> Date: Thu, 26 Dec 2024 02:46:27 -0800 Subject: [PATCH 61/61] Fix commander safe time logic at the start of game --- lua/SimUtils.lua | 2 +- lua/aibrain.lua | 2 +- lua/sim/units/ACUUnit.lua | 4 +--- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lua/SimUtils.lua b/lua/SimUtils.lua index b85d1a1255..71212dd58f 100644 --- a/lua/SimUtils.lua +++ b/lua/SimUtils.lua @@ -971,7 +971,7 @@ function KillArmyOnDelayedRecall(self, shareOption, shareTime) -- filter out commanders that are not currently safe and should explode local gameTick = GetGameTick() for i, com in sharedCommanders do - if com.LastTickDamaged + CommanderSafeTime > gameTick then + if com.LastTickDamaged and com.LastTickDamaged > gameTick - CommanderSafeTime then sharedCommanders[i] = nil -- explode unsafe ACUs because KillArmy might not com:Kill() diff --git a/lua/aibrain.lua b/lua/aibrain.lua index 431c83079e..72d09aa082 100644 --- a/lua/aibrain.lua +++ b/lua/aibrain.lua @@ -498,7 +498,7 @@ AIBrain = Class(FactoryManagerBrainComponent, StatManagerBrainComponent, JammerM if shareAcuOption == 'Recall' then local gameTick = GetGameTick() for _, com in commanders do - if com.LastTickDamaged + CommanderSafeTime <= gameTick then + if com.LastTickDamaged <= gameTick - CommanderSafeTime then table.insert(safeCommanders, com) else -- explode unsafe ACUs because KillArmy might not diff --git a/lua/sim/units/ACUUnit.lua b/lua/sim/units/ACUUnit.lua index 63d5db4150..78f1b8984b 100644 --- a/lua/sim/units/ACUUnit.lua +++ b/lua/sim/units/ACUUnit.lua @@ -1,10 +1,8 @@ local CommandUnit = import("/lua/sim/units/commandunit.lua").CommandUnit ---@class ACUUnit : CommandUnit ----@field LastTickDamaged number +---@field LastTickDamaged? number ACUUnit = ClassUnit(CommandUnit) { - LastTickDamaged = 0, - -- The "commander under attack" warnings. ---@param self ACUUnit ---@param bpShield any