Download Lua/Satellite/SatelliteView.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 79.6 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Satellite/SatelliteView.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/Satellite/SatelliteView.lua
-
curl -L -o SatelliteView.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/Satellite/SatelliteView.lua
79.6 kB
| MapTypesCombo = { "game", "system", "satellite" } | |
| function GetCurrentCampaignPreset() | |
| if Game and Game.Campaign then return CampaignPresets[Game.Campaign] end | |
| return CampaignPresets[DefaultCampaign] | |
| end | |
| function LoadCampaignInitialMap(campaign) | |
| campaign = campaign or GetCurrentCampaignPreset() | |
| local sector = table.find_value(campaign.Sectors, "Id", campaign.InitialSector) | |
| if sector and sector.Map and sector.Map ~= GetMapName() then | |
| ChangeMap(sector.Map) | |
| end | |
| end | |
| function CreateSatelliteSectors() | |
| local campaign = GetCurrentCampaignPreset() | |
| if not campaign then | |
| assert(false, "Current map does not correspond to any campaign") | |
| return | |
| end | |
| local sectors = {} | |
| for col = campaign.sector_rowsstart, campaign.sector_columns do | |
| for row = 1, campaign.sector_rows do | |
| local sector = PlaceObject("SatelliteSector") | |
| sector:SetId(sector_pack(row, col)) | |
| sectors[#sectors + 1] = sector | |
| end | |
| end | |
| return sectors | |
| end | |
| function GetSatelliteSectors(bCreate, campaign_preset) | |
| local campaign = campaign_preset or GetCurrentCampaignPreset() | |
| if not campaign then | |
| assert(false, "Current map does not correspond to any campaign") | |
| return | |
| end | |
| local sectors = campaign.Sectors or {} | |
| if not next(sectors) and bCreate then | |
| sectors = CreateSatelliteSectors() or {} | |
| table.sort(sectors, function(a, b) return a.Id < b.Id end) | |
| end | |
| campaign.Sectors = sectors | |
| return sectors | |
| end | |
| function GetCampaignSectorsCombo(default, filter) | |
| local campaign = GetCurrentCampaignPreset() | |
| if not campaign then | |
| assert(false, "Current map does not correspond to any campaign") | |
| return | |
| end | |
| local items = default and {{ value = default, text = default}} or {} | |
| for _, sector in ipairs(campaign.Sectors or empty_table) do | |
| if not filter or filter(sector) then | |
| local id = sector.Id | |
| items[#items + 1] = {value = id, text = sector.name} | |
| end | |
| end | |
| return items | |
| end | |
| function GetGuardpostCampaignSectorsCombo(default) | |
| return GetCampaignSectorsCombo(default, function(s) return s.Guardpost end) | |
| end | |
| TFormat.ActionName_ActivitiesCount = function(context) | |
| if context then return T(520902754959, "Operations") end -- Hack to not get dynamic text portion in the options menu | |
| local squad, sector_id = GetSatelliteContextMenuValidSquad() | |
| local operationsInSector = GetOperationsInSector(sector_id) | |
| local available = table.count(operationsInSector, "enabled", true) | |
| return T{989240901383, "Operations[<count>]",count = available} | |
| end | |
| TFormat.ActionName_OperationsList = function(context) | |
| local operationsInSector = GetOperationsInSector(context.Id) | |
| local names = {} | |
| for _, operation in ipairs(operationsInSector) do | |
| if operation.enabled then | |
| names[#names+1] = operation.operation.display_name | |
| end | |
| end | |
| if #names>0 then | |
| table.insert(names, 1, Untranslated("\n")) | |
| end | |
| return table.concat(names, "\n") | |
| end | |
| function GetSatelliteViewInterface() | |
| return g_SatelliteUI | |
| end | |
| local openiningSatView | |
| function OpenSatelliteView(campaign, context, loading_screen, wait) | |
| if g_FirstNetStart then return end | |
| if openiningSatView then return end | |
| --print("----------------OpenSatelliteView", GetStack()) | |
| openiningSatView = true | |
| if netInGame and not netGameInfo.started then | |
| _OpenSatelliteView(campaign, context, loading_screen) | |
| elseif GetMap() == "" or not mapdata.GameLogic then | |
| --if not on map, we can't sync anything.. | |
| if not netInGame or NetIsHost() then | |
| NetEchoEvent("OpenSatelliteViewAsync", campaign, context, loading_screen) | |
| end | |
| else | |
| FireNetSyncEventOnHost("OpenSatelliteView", campaign, context, loading_screen) | |
| end | |
| if wait then | |
| WaitMsg("OpenSatelliteView", 1000) | |
| end | |
| end | |
| function NetEvents.OpenSatelliteViewAsync(...) | |
| CreateGameTimeThread(_OpenSatelliteView, ...) | |
| end | |
| function NetSyncEvents.OpenSatelliteView(...) | |
| CreateGameTimeThread(_OpenSatelliteView, ...) | |
| end | |
| function NetSyncEvents.OpenPDASatellite(context) | |
| CreateGameTimeThread(function() | |
| local dlg = GetDialog("PDADialogSatellite") | |
| if not dlg then | |
| OpenDialog("PDADialogSatellite", GetInGameInterface(), context) | |
| end | |
| end) | |
| end | |
| if FirstLoad then | |
| g_OpenSatelliteViewThread = false | |
| end | |
| function _OpenSatelliteView(campaign, context, loading_screen) | |
| --this part is sync cuz coming from sync ev | |
| if g_Combat then | |
| if g_Combat:ShouldEndCombat() then | |
| g_Combat:EndCombatCheck() | |
| while g_Combat do | |
| WaitMsg("CombatEnd", 20) | |
| end | |
| end | |
| end | |
| -- Cancel any aiming mode etc. | |
| CreateRealTimeThread(RestoreDefaultModeSimple) | |
| if not CanYield() or IsRealTimeThread() then | |
| CreateGameTimeThread(SkipNonBlockingSetpieces) | |
| else | |
| SkipNonBlockingSetpieces() --waking up setpieces' threads from rtt is async; | |
| end | |
| assert(not IsValidThread(g_OpenSatelliteViewThread)) | |
| g_OpenSatelliteViewThread = CreateRealTimeThread(function() | |
| --still probably sync | |
| sprocall(function() | |
| if not HasGameSession() then | |
| NewGameSession() | |
| end | |
| Msg("PreOpenSatelliteView") | |
| if context then CloseSatelliteView(true) end | |
| local load_map = GetMap() == "" or not mapdata.GameLogic | |
| loading_screen = loading_screen or load_map | |
| if loading_screen then | |
| LoadingScreenOpen("idSatelliteView", "satellite") --this is not sync | |
| end | |
| if load_map then | |
| --this happens when launching new game or when loading a game where player has not entered a sector yet | |
| LoadCampaignInitialMap(campaign) --dunno if this is sync or not | |
| if not AnyPlayerSquads() then | |
| local campaign = campaign or CampaignPresets[Game.Campaign] | |
| campaign:FirstRunInterface() | |
| end | |
| end | |
| ShowInGameInterface(true) | |
| FireNetSyncEventOnHost("OpenPDASatellite", context) --since there is non sync code up to here, only way to guarantee this is through ev | |
| while HasGameSession() and (not gv_SatelliteView or not GetDialog("PDADialogSatellite")) do --sometimes, when game loads super slowly, this is already true | |
| --gv_SatelliteView is sometimes bogus because it is a game var and it could be persisted true without the ui being up | |
| WaitMsg("InitSatelliteView", 2000) | |
| end | |
| local dlg = GetDialog("PDADialog") | |
| if dlg then | |
| dlg:SetVisible(true) | |
| end | |
| end) | |
| if loading_screen then | |
| LoadingScreenClose("idSatelliteView", "satellite") | |
| end | |
| openiningSatView = nil | |
| g_OpenSatelliteViewThread = false | |
| end) | |
| end | |
| function CloseSatelliteView(force) | |
| if not gv_SatelliteView then return end | |
| FireNetSyncEventOnHost("CloseSatelliteView", force) | |
| end | |
| function NetSyncEvents.CloseSatelliteView(force) | |
| local pda = GetDialog("PDADialogSatellite") | |
| if not pda then return end | |
| pda:Close(force) | |
| end | |
| function CanCloseSatelliteView() | |
| local squads = GetSectorSquadsFromSide(gv_CurrentSectorId, "player1") | |
| if #squads > 0 then | |
| local anyNonTravelling = false | |
| for i, squad in ipairs(squads) do | |
| local travelling = IsSquadTravelling(squad) and not gv_Sectors[squad.CurrentSector].conflict | |
| if not travelling then | |
| anyNonTravelling = true | |
| end | |
| end | |
| if not anyNonTravelling then squads = false end | |
| else | |
| squads = false | |
| end | |
| return gv_SatelliteView and squads and not ForceReloadSectorMap | |
| end | |
| GameVar("gv_SatelliteOpenTime", 0) | |
| GameVar("gv_SatelliteOpenWeather", false) | |
| function OnMsg.StartSatelliteGameplay() | |
| gv_SatelliteOpenTime = Game.CampaignTime | |
| gv_SatelliteOpenWeather = GetCurrentSectorWeather() or false | |
| end | |
| function OnMsg.SatelliteTick() | |
| if ForceReloadSectorMap then | |
| return | |
| end | |
| if (Game.CampaignTime - gv_SatelliteOpenTime >= const.Satellite.ReloadMapAfterSatelliteTime) or -- a lot of time passed in satellite | |
| (gv_SatelliteOpenWeather ~= GetCurrentSectorWeather()) or -- weather changed | |
| (CalculateTimeOfDay(Game.CampaignTime) ~= CalculateTimeOfDay(gv_SatelliteOpenTime)) -- time of day changed | |
| then | |
| ForceReloadSectorMap = true | |
| end | |
| end | |
| if FirstLoad then | |
| g_SatelliteThread = false | |
| PrevSectorOnMouse = false | |
| g_Cabinet = false | |
| g_CitySectors = false | |
| time_thread_waiting_resume = false | |
| end | |
| total_pause_delta = 0 | |
| function OnMsg.CampaignSpeedChanged() | |
| if IsValidThread(g_SatelliteThread) and time_thread_waiting_resume then | |
| Wakeup(g_SatelliteThread) | |
| time_thread_waiting_resume = false | |
| end | |
| end | |
| local div = 1000 | |
| local ticks_batch = 5 --reduce number of msgs getting pumped over the net; | |
| dbgCampaignFactor = false | |
| function SatelliteTimeThread() | |
| local function ShouldRun() | |
| return not IsCampaignPaused() and not GameState.entering_sector | |
| end | |
| --local time_func = GetPreciseTicks | |
| local time_func = RealTime | |
| while true do | |
| local campaign_time = Game.CampaignTime | |
| local sleep_accum = 0 --additional sleep time from fractions | |
| local dt_accum = 0 --additional dt that hasn't ticked | |
| local rt_ts = time_func() | |
| local dbgStart = time_func() | |
| local dbgTikcsFired = 0 | |
| while ShouldRun() do | |
| --ok, so, here is how this works | |
| --sleep for about the time it needs to make one tick | |
| --but, do the ticks based on rt elapsed to accomodate time lost in the waitmsg further down | |
| --do as many ticks as rt has passed | |
| --if dt before sleeping is enough to tick it doesn't sleep | |
| --if time to sleep is not enough for one tick, bump it | |
| --time to sleep lost due to rounding to ms is carried over --this is probably not needed... | |
| --insufficent dt for a tick is carried over | |
| local campaignFactor = dbgCampaignFactor or Game.CampaignTimeFactor --this many ticks per sec | |
| local sleepPerTick = MulDivRound(div, div, campaignFactor) | |
| local dt_so_far = (time_func() - rt_ts + (dt_accum / div)) * div | |
| local tToSleep = (sleepPerTick * ticks_batch + sleep_accum) - dt_so_far | |
| if tToSleep > 0 then | |
| Sleep(tToSleep / div) | |
| sleep_accum = tToSleep % div | |
| if not ShouldRun() then | |
| --pause state might have changed | |
| break | |
| end | |
| end | |
| local now = time_func() | |
| local dt = now - rt_ts + (dt_accum / div) | |
| rt_ts = now | |
| local ticks = ((dt * div) / sleepPerTick) | |
| dt_accum = dt_accum % div + dt * div - (ticks * sleepPerTick) | |
| if ticks > 0 then | |
| dbgTikcsFired = dbgTikcsFired + ticks | |
| local next_t = campaign_time + const.Scale.min * ticks | |
| NetSyncEvent("SatelliteCampaignTimeAdvance", next_t, campaign_time, ticks) | |
| campaign_time = next_t | |
| end | |
| local dbgElapsed = time_func() - dbgStart | |
| --print("AHEAD BY", (campaign_time - Game.CampaignTime) / const.Scale.min ) | |
| --print("ELAPSED", dbgElapsed, "TICKS", dbgTikcsFired, "DTACCUM", dt_accum, "sleepPerTick", sleepPerTick, "TICKS this pass", ticks) | |
| --print("TF", campaignFactor, "TICKS PER SEC", dbgElapsed > 0 and MulDivRound(dbgTikcsFired, div, dbgElapsed) or "N/A") | |
| while #SyncEventsQueue > 0 do --wait for ev dispatcher so not to overwhelm it | |
| WaitMsg("SyncEventsProcessed", 50) | |
| end | |
| end | |
| time_thread_waiting_resume = true | |
| WaitWakeup() | |
| end | |
| end | |
| function OnMsg.StartSatelliteGameplay() | |
| DeleteThread(g_SatelliteThread) | |
| if netInGame and not NetIsHost() then return end | |
| g_SatelliteThread = CreateMapRealTimeThread(SatelliteTimeThread) | |
| end | |
| local ticks_in_day = 24 * 60 * const.Scale.min / const.Satellite.SectorsTick | |
| local function lFireCampaignTimeSyncMessages(time, old_time) | |
| local tick = time / const.Satellite.Tick | |
| local sectors_tick = time / const.Satellite.SectorsTick | |
| if tick ~= old_time / const.Satellite.Tick then | |
| Msg("SatelliteTick", tick) | |
| local list = GetUnitDataList() | |
| ListCallReactions(list, "OnSatelliteTick") | |
| if time % const.Scale.h <= 0 then | |
| ListCallReactions(list, "OnNewHour") | |
| end | |
| if time % const.Scale.day < const.Scale.min then | |
| ListCallReactions(list, "OnNewDay") | |
| end | |
| end | |
| if sectors_tick ~= old_time / const.Satellite.SectorsTick then | |
| Msg("SectorsTick", sectors_tick % ticks_in_day, ticks_in_day) | |
| end | |
| if time % const.Scale.h <= 0 then | |
| Msg("NewHour") | |
| end | |
| if time % const.Scale.day < const.Scale.min then | |
| Msg("NewDay") | |
| end | |
| end | |
| --[[function OnMsg.NewHour() | |
| local hash = NetGetHashValue() | |
| NetUpdateHash("NetHashSatelliteUpdate", hash, GetCampHumanTime()) | |
| NetResetHashValue() | |
| end | |
| ]] | |
| function GetCampHumanTime() | |
| return (Game.CampaignTime % const.Scale.day) / const.Scale.h, ":", | |
| (Game.CampaignTime % const.Scale.day) % const.Scale.h / const.Scale.min | |
| end | |
| if FirstLoad then | |
| g_TimeAdvanceThread = false | |
| end | |
| function NetSyncEvents.SatelliteCampaignTimeAdvance(time, old_time, step) | |
| if IsCampaignPaused() then | |
| return | |
| end | |
| DeleteThread(g_TimeAdvanceThread) | |
| g_TimeAdvanceThread = CreateMapRealTimeThread(function() | |
| local lastGameTime = GameTime() | |
| WaitAllOtherThreads() | |
| assert(Game) | |
| if not Game then return end | |
| while Game.CampaignTime < time and not IsCampaignPaused() do | |
| local ot = Game.CampaignTime | |
| Game.CampaignTime = Game.CampaignTime + const.Scale.min | |
| SetUILCustomTime(Game.CampaignTime) | |
| Game.DailyIncome = GetDailyIncome() | |
| lFireCampaignTimeSyncMessages(Game.CampaignTime, ot) | |
| ObjModified(Game) | |
| Msg("CampaignTimeAdvanced", Game.CampaignTime, ot) | |
| WaitAllOtherThreads() --wait for threads woken up by this thread to exec | |
| -- Detection for a bug | |
| local gameTimeNow = GameTime() | |
| if lastGameTime ~= gameTimeNow then | |
| assert(not "Game time is running in the satellite view :O") | |
| end | |
| lastGameTime = gameTimeNow | |
| end | |
| g_TimeAdvanceThread = false | |
| end) | |
| end | |
| function OnMsg.OpenSatelliteView() | |
| SetUILCustomTime(Game.CampaignTime) | |
| end | |
| function GetAmountPerTick(amount, tick, ticks) | |
| return amount * (tick + 1) / ticks - amount * tick / ticks | |
| end | |
| -- mines | |
| function MineEnable(sector_id, enabled) | |
| gv_Sectors[sector_id].mine_enabled = enabled | |
| end | |
| function GetSectorDepletionTime(sector) | |
| local baseVal = sector.DepletionTime | |
| local percentAccum = 100 | |
| for i, m in ipairs(sector.depletion_mods) do | |
| percentAccum = percentAccum + (m - 100) | |
| end | |
| return MulDivRound(baseVal, percentAccum, 100) | |
| end | |
| function GetSectorDailyIncome(sector) | |
| local baseVal = sector.DailyIncome | |
| local baseValDiffPerc = PercentModifyByDifficulty(GameDifficulties[Game.game_difficulty]:ResolveValue("sectorDailyIncomeBonus")) | |
| baseVal = MulDivRound(baseVal, baseValDiffPerc, 100) | |
| local percentAccum = 100 | |
| for i, m in ipairs(sector.income_mods) do | |
| percentAccum = percentAccum + (m - 100) | |
| end | |
| return MulDivRound(baseVal, percentAccum, 100) | |
| end | |
| function TFormat.MinePercentAtDepleted() | |
| local difficultyPreset = GameDifficulties[GetGameDifficulty()] | |
| local difficultyPercent = difficultyPreset and difficultyPreset:ResolveValue("DepletedMineIncomePerc") | |
| return difficultyPercent or 0 | |
| end | |
| function GetMineIncome(sector_id, showEvenIfUnowned) | |
| local sector = gv_Sectors[sector_id] | |
| -- No income for this sector | |
| if not sector.Mine or not sector.mine_enabled then | |
| return | |
| end | |
| local city_loyalty = GetCityLoyalty(sector.City) or 100 | |
| if sector.Side ~= "player1" then | |
| if showEvenIfUnowned then | |
| city_loyalty = 50 | |
| else | |
| return | |
| end | |
| end | |
| local sectorDepletionTime = GetSectorDepletionTime(sector) | |
| local perc = 100 | |
| if sector.Depletion and sector.mine_work_days and sector.mine_work_days > sectorDepletionTime then | |
| local daysSinceStartedDepleting = sector.mine_work_days - sectorDepletionTime | |
| perc = Lerp(100, 0, daysSinceStartedDepleting, const.Satellite.MineDepletingDays) | |
| perc = Max(0, perc) | |
| end | |
| local difficultyPreset = GameDifficulties[GetGameDifficulty()] | |
| local difficultyPercent = difficultyPreset and difficultyPreset:ResolveValue("DepletedMineIncomePerc") | |
| local incomeAtDepletion = difficultyPercent or 0 | |
| perc = Max(perc, incomeAtDepletion) | |
| if perc == 0 then | |
| return | |
| end | |
| local income = GetSectorDailyIncome(sector) | |
| income = perc * income / 100 | |
| return income * (50 + city_loyalty / 2 ) / 100 | |
| end | |
| function GetMineDepletionDaysLeft(sector) | |
| if not sector.Mine then return end | |
| local daysMineWorked = sector.mine_work_days or 0 | |
| local daysStartDepleting = GetSectorDepletionTime(sector) | |
| local depletionDays = const.Satellite.MineDepletingDays | |
| return (daysStartDepleting + depletionDays) - daysMineWorked, daysMineWorked > daysStartDepleting | |
| end | |
| function GetDaysLeftUntilDepletionStarts(sector) | |
| if not sector.Mine then return end | |
| local daysMineWorked = sector.mine_work_days or 0 | |
| local daysStartDepleting = GetSectorDepletionTime(sector) | |
| return daysStartDepleting - daysMineWorked | |
| end | |
| function OnMsg.SectorsTick(tick, ticks_per_day) | |
| for id, sector in sorted_pairs(gv_Sectors) do | |
| local income = GetMineIncome(id) | |
| if income then | |
| income = GetAmountPerTick(income, tick, ticks_per_day) | |
| AddMoney(income, "income", "noCombatLog") | |
| if tick + 1 == ticks_per_day then | |
| sector.mine_work_days = (sector.mine_work_days or 0) + 1 | |
| local sectorDepletionTime = GetSectorDepletionTime(sector) | |
| if not sector.mine_depleted and sector.Depletion and sector.mine_work_days >= sectorDepletionTime + const.Satellite.MineDepletingDays then | |
| sector.mine_depleted = true | |
| CombatLog("important", T{268514931670, "<SectorName(sector)> is depleted.", sector = sector}) | |
| if g_SatelliteUI then g_SatelliteUI:UpdateSectorVisuals(id) end | |
| end | |
| end | |
| end | |
| -- If an enemy had a waiting conflict for this sector that it needs to stop waiting due | |
| -- to the squad dying or whatever else | |
| local conflict = sector and sector.conflict | |
| if conflict and conflict.waiting and not conflict.player_attacking and not EnemyWantsToWait(id) then | |
| EnterConflict(sector) | |
| end | |
| ExecuteSectorEvents("SE_OnTick", id) | |
| end | |
| end | |
| -- loyalty | |
| function CityModifyLoyalty(city_id, add, msg_reason) | |
| local city = gv_Cities[city_id] | |
| if not city or add == 0 then return end | |
| city.Loyalty = Clamp(city.Loyalty + add, 0, 100) | |
| Msg("LoyaltyChanged", city_id, add) | |
| local msg = false | |
| if add > 0 then | |
| msg = T{562269812751, "Gained <em><num> Loyalty</em> with <em><city></em> ", city = city.DisplayName, num = add} | |
| else | |
| msg = T{837740133104, "Lost <em><num> Loyalty</em> with <em><city></em> ", city = city.DisplayName, num = abs(add)} | |
| end | |
| if msg_reason and msg_reason~="" then | |
| CombatLog("important", T{833235545397, "<msg>(<reason>)", msg = msg, reason = msg_reason} ) | |
| else | |
| CombatLog("short", msg) | |
| end | |
| ObjModified(city) | |
| return add > 0 and "gain" or "loss" | |
| end | |
| function NetSyncEvents.CheatCityModifyLoyalty(city_id, add, msg_reason) | |
| CityModifyLoyalty(city_id, add, msg_reason) | |
| end | |
| function GetCityLoyalty(city_id) | |
| local city = gv_Cities and gv_Cities[city_id] | |
| if not city then return 100 end | |
| return city.Loyalty | |
| end | |
| TFormat.GetCityLoyalty = function(context_obj, city_id) | |
| return GetCityLoyalty(city_id) | |
| end | |
| -- Returns the number of cities controlled by the player. | |
| -- if countSectors is true sectors containing cities are counted instead. | |
| function GetPlayerCityCount(countSectors) | |
| local cityCount = 0 | |
| if countSectors then | |
| for cityName, sectorCount in pairs(gv_PlayerCityCounts and gv_PlayerCityCounts.cities) do | |
| cityCount = cityCount + sectorCount | |
| end | |
| else | |
| cityCount = gv_PlayerCityCounts and gv_PlayerCityCounts.count or 0 | |
| end | |
| return cityCount | |
| end | |
| --militia | |
| function GetSectorMilitiaCount(sector_id) | |
| local squad_id = gv_Sectors[sector_id] and gv_Sectors[sector_id].militia_squad_id | |
| return squad_id and gv_Squads[squad_id] and #(gv_Squads[squad_id].units or "") or 0 | |
| end | |
| function CreateMilitiaUnitData(class, sector, militia_squad) | |
| local session_id = GenerateUniqueUnitDataId("Militia", sector.Id, class) | |
| local unit_data = CreateUnitData(class, session_id, InteractionRand(nil, "Satellite")) | |
| unit_data.militia = true | |
| unit_data.Squad = militia_squad.UniqueId | |
| militia_squad.units = militia_squad.units or {} | |
| table.insert(militia_squad.units, session_id) | |
| end | |
| function DeleteMilitiaUnitData(id, militia_squad) | |
| gv_UnitData[id] = nil | |
| if g_Units[id] then | |
| DoneObject(g_Units[id]) | |
| g_Units[id] = nil | |
| end | |
| table.remove_entry(militia_squad.units, id) | |
| end | |
| MilitiaUpgradePath = { | |
| "MilitiaRookie", | |
| "MilitiaVeteran", | |
| "MilitiaElite", | |
| } | |
| MilitiaIcons = { | |
| false, | |
| "UI/PDA/MercPortrait/T_ClassIcon_Veteran_Small", | |
| "UI/PDA/MercPortrait/T_ClassIcon_Elite_Small", | |
| } | |
| function GetLeastExpMilitia(units) | |
| local leastExperienced = false | |
| local leastExperiencedIdx = false | |
| for _, u in ipairs(units) do | |
| local ud = gv_UnitData[u] | |
| local class = ud.class | |
| local idx = table.find(MilitiaUpgradePath, class) | |
| if not leastExperiencedIdx or idx < leastExperiencedIdx then | |
| leastExperienced = ud | |
| leastExperiencedIdx = idx | |
| end | |
| end | |
| return leastExperienced | |
| end | |
| function SpawnMilitia(trainAmount, sector, bFromOperation) | |
| assert(MilitiaUpgradePath and #MilitiaUpgradePath > 0) | |
| local militia_squad_id = sector.militia_squad_id or | |
| CreateNewSatelliteSquad({ | |
| Side = "ally", | |
| CurrentSector = sector.Id, | |
| militia = true, | |
| Name = T(121560205347, "MILITIA") | |
| }) | |
| sector.militia_squad_id = militia_squad_id | |
| local militia_squad = gv_Squads[militia_squad_id] | |
| local count = {MilitiaRookie = 0,MilitiaVeteran = 0} | |
| for i,unit_id in ipairs(militia_squad and militia_squad.units) do | |
| local class = gv_UnitData[unit_id].class | |
| if class == "MilitiaRookie" then count.MilitiaRookie = count.MilitiaRookie + 1 end | |
| if class == "MilitiaVeteran" then count.MilitiaVeteran = count.MilitiaVeteran + 1 end | |
| end | |
| local count_trained = 0 | |
| for i = 1, trainAmount do | |
| local squadUnits = militia_squad.units or empty_table | |
| local leastExpMember = GetLeastExpMilitia(militia_squad.units) | |
| if #squadUnits < sector.MaxMilitia then | |
| CreateMilitiaUnitData(MilitiaUpgradePath[1], sector, militia_squad) | |
| count_trained = count_trained + 1 | |
| elseif leastExpMember then -- level up | |
| if bFromOperation and count.MilitiaRookie<=0 then | |
| break | |
| end | |
| local leastExperiencedTemplate = bFromOperation and "MilitiaRookie" or leastExpMember.class | |
| local leastExpIdx = table.find(MilitiaUpgradePath, leastExperiencedTemplate) | |
| if not leastExpIdx then leastExpIdx = 0 end | |
| leastExpIdx = leastExpIdx + 1 | |
| local upgradedClass = MilitiaUpgradePath[leastExpIdx] | |
| -- Cannot be upgraded further | |
| if leastExpIdx > #MilitiaUpgradePath or not upgradedClass then | |
| break | |
| end | |
| DeleteMilitiaUnitData(leastExpMember.session_id, militia_squad) | |
| CreateMilitiaUnitData(upgradedClass, sector, militia_squad) | |
| count_trained = count_trained + 1 | |
| count.MilitiaRookie = count.MilitiaRookie - 1 | |
| count.MilitiaVeteran = count.MilitiaVeteran + 1 | |
| end | |
| end | |
| return militia_squad, count_trained | |
| end | |
| if FirstLoad then | |
| g_MilitiaTrainingCompleteCounter = 0 | |
| g_MilitiaTrainingCompletePopups = {} | |
| end | |
| function OnMsg.EnterSector() | |
| g_MilitiaTrainingCompleteCounter = 0 | |
| assert(not next(g_MilitiaTrainingCompletePopups)) | |
| end | |
| function CompleteCurrentMilitiaTraining(sector, mercs) | |
| NetUpdateHash("CompleteCurrentMilitiaTraining") | |
| local eventId = g_MilitiaTrainingCompleteCounter | |
| g_MilitiaTrainingCompleteCounter = g_MilitiaTrainingCompleteCounter + 1 | |
| local start_time = Game.CampaignTime | |
| CreateMapRealTimeThread(function() | |
| local militia_squad, count_trained = SpawnMilitia(const.Satellite.MilitiaUnitsPerTraining, sector, "operation") | |
| sector.militia_training = false | |
| local militia_types ={MilitiaRookie = 0, MilitiaElite=0, MilitiaVeteran=0} | |
| for _, unit_id in ipairs(militia_squad.units) do | |
| local unit = gv_UnitData[unit_id] | |
| militia_types[unit.class] = militia_types[unit.class] + 1 | |
| end | |
| local popupHost = GetDialog("PDADialogSatellite") | |
| popupHost = popupHost and popupHost:ResolveId("idDisplayPopupHost") | |
| if militia_types.MilitiaVeteran >= (sector.MaxMilitia - militia_types.MilitiaElite) then | |
| --max trainig reached | |
| local dlg = CreateMessageBox( | |
| popupHost, | |
| T(295710973806, "Militia Training"), | |
| T{522643975325, "Militia training is finished - trained <militia_trained> defenders.<newline><GameColorD>(<sectorName>)</GameColorD>", | |
| sectorName = GetSectorName(sector), | |
| militia_trained = count_trained}.."\n\n"..T(306458255966, "Militia can’t be trained further. Victories in combat can advance militia soldiers to Elite levels.") | |
| ) | |
| dlg:Wait() | |
| else | |
| -- train one more time | |
| local cost, costTexts, names, errors = GetOperationCosts(mercs, "MilitiaTraining", "Trainer","refund") | |
| local buyAgainText = T(460261217340, "Do you want to train militia again?") | |
| local costText = table.concat(costTexts, ", ") | |
| local dlg = CreateQuestionBox( | |
| popupHost, | |
| T(295710973806, "Militia Training"), | |
| T{522643975325, "Militia training is finished - trained <militia_trained> defenders.<newline><GameColorD>(<sectorName>)</GameColorD>", | |
| sectorName = GetSectorName(sector), | |
| militia_trained = count_trained}, | |
| T(689884995409, "Yes"), | |
| T(782927325160, "No"), | |
| { sector = sector, mercs = mercs, textLower = buyAgainText, costText = costText }, | |
| function() return not next(errors) and (militia_types.MilitiaVeteran < (sector.MaxMilitia - militia_types.MilitiaElite)) and "enabled" or "disabled" end, | |
| nil, | |
| "ZuluChoiceDialog_MilitiaTraining") | |
| assert(g_MilitiaTrainingCompletePopups[eventId] == nil) | |
| g_MilitiaTrainingCompletePopups[eventId] = dlg | |
| NetSyncEvent("ProcessMilitiaTrainingPopupResults", dlg:Wait(), eventId, sector.Id, UnitDataToSessionIds(mercs), cost, start_time) | |
| g_MilitiaTrainingCompletePopups[eventId] = nil | |
| end | |
| end) | |
| end | |
| function UnitDataToSessionIds(arr) | |
| local ret = {} | |
| for i, m in ipairs(arr) do | |
| ret[i] = m.session_id | |
| end | |
| return ret | |
| end | |
| function SessionIdsArrToUnitData(arr) | |
| local unit_data = gv_UnitData | |
| local ret = {} | |
| for i, id in ipairs(arr) do | |
| ret[i] = unit_data[id] | |
| end | |
| return ret | |
| end | |
| function NetSyncEvents.ProcessMilitiaTrainingPopupResults(result, event_id, sector_id, mercs, cost, start_time) | |
| if result == "ok" then | |
| local sector = gv_Sectors[sector_id] | |
| if sector.started_operations["MilitiaTraining"] ~= start_time then --other player already started it | |
| for i, session_id in ipairs(mercs) do | |
| NetSyncEvents.MercSetOperation(session_id, "MilitiaTraining", "Trainer", i == 1 and cost, i, false) | |
| end | |
| NetSyncEvents.LogOperationStart("MilitiaTraining", sector.Id, "log") | |
| NetSyncEvents.StartOperation(sector.Id, "MilitiaTraining", start_time, sector.training_stat) | |
| end | |
| end | |
| if g_MilitiaTrainingCompletePopups[event_id] then | |
| g_MilitiaTrainingCompletePopups[event_id]:Close() | |
| g_MilitiaTrainingCompletePopups[event_id] = nil | |
| end | |
| end | |
| function SavegameSessionDataFixups.MilitiaChangeData(data, metadata, lua_revision) | |
| if lua_revision<283940 then | |
| local l_gv_unit_data = GetGameVarFromSession(data, "gv_UnitData") | |
| -- units | |
| for k, unit in pairs(l_gv_unit_data) do | |
| if unit.class == "MilitiaVeteran" then | |
| unit.class = "MilitiaElite" | |
| unit.Name = T(486398616031, "Militia Elite") | |
| elseif unit.class == "MilitiaRegular" then | |
| unit.class = "MilitiaVeteran" | |
| unit.Name = T(237861181220, "Militia Veteran") | |
| end | |
| end | |
| end | |
| end | |
| function SavegameSectorDataFixups.MilitiaChangeData(sector_data, lua_revision) | |
| -- units | |
| if lua_revision<283940 then | |
| -- load dynamic data | |
| local dynamic_data = sector_data.dynamic_data | |
| if dynamic_data and #dynamic_data > 0 then | |
| for _, ddata_table in ipairs(dynamic_data) do | |
| local ddata = ddata_table[2] | |
| if rawget(ddata, "class") then | |
| if ddata.class == "MilitiaVeteran" then | |
| ddata.class = "MilitiaElite" | |
| elseif ddata.class == "MilitiaRegular" then | |
| ddata.class = "MilitiaVeteran" | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end | |
| -- fixup of savegames with conflict with no mercs due to changes to mercs squads available for conflict (traveling squads are filtered from UI) | |
| function OnMsg.ZuluGameLoaded(filename, lua_revision) | |
| if lua_revision and lua_revision>346296 then return end | |
| for id, sector_id in pairs(g_ConflictSectors) do | |
| local playersqs = GetSquadsInSector(sector_id, "excludeTravelling", "includeMilitia", "excludeArriving", "excludeRetreating") | |
| if not next(playersqs) then | |
| ResolveConflict(gv_Sectors[sector_id], "no voice") | |
| end | |
| end | |
| end | |
| -- Prior to this version arrival squads weren't saved as such. | |
| -- Try to guess based on their name. | |
| function SavegameSessionDataFixups.ArrivalSquads(data, meta) | |
| if meta and meta.lua_revision > 289560 then return end | |
| local arrivingSquadName = _InternalTranslate(T(546629671844, "ARRIVING")) | |
| for id, squad in pairs(data.gvars.gv_Squads) do | |
| if IsT(squad.Name) and _InternalTranslate(squad.Name) == arrivingSquadName then | |
| squad.arrival_squad = true | |
| end | |
| end | |
| end | |
| function SavegameSessionDataFixups.EnforceSquadNameTranslations(data, meta) | |
| local function TryToMatchSquadName(squad_name) | |
| local result | |
| ForEachPreset("SquadName", function(preset, ...) | |
| if preset.group == "Player_Arriving" or preset.group == "Player" then return end | |
| if squad_name == TDevModeGetEnglishText(preset.Name, not "deep", "no_assert") then result = preset.Name return end | |
| end) | |
| return result | |
| end | |
| local playerCounter = 1 | |
| for i,v in pairs(data.gvars.gv_Squads) do | |
| if v.Side == "player1" or v.Side == "player2" then | |
| if v.arrival_squad then | |
| v.Name = Presets.SquadName.Default.Arriving.Name | |
| else | |
| v.Name = Presets.SquadName.Player[playerCounter].Name | |
| playerCounter = playerCounter + 1 | |
| end | |
| elseif type(v.Name) == "string" then | |
| v.Name = TryToMatchSquadName(v.Name) or Presets.SquadName.Default.Squad.Name | |
| elseif v.militia then | |
| -- do nothing | |
| else | |
| assert(false, "Can't identify squad type to fix up save game.") | |
| end | |
| end | |
| end | |
| function SavegameSessionDataFixups.FixMercCreatedVariable(data, meta) | |
| if not (data.gvars.g_ImpTest and data.gvars.g_ImpTest.final) then return end | |
| if data.gvars.g_ImpTest.final.created then return end | |
| local merc_id = data.gvars.g_ImpTest.final.merc_template.id | |
| for _, squad in pairs(data.gvars.gv_Squads) do | |
| if squad.Side == "player1" or squad.Side == "player2" then | |
| for _, unit_id in ipairs(squad.units) do | |
| if unit_id == merc_id then | |
| data.gvars.g_ImpTest.final.created = true | |
| return | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function SavegameSessionDataFixups.testoo(data, meta) | |
| if meta and meta.lua_revision > 302378 then return end | |
| local arrivingSquadName = _InternalTranslate(T(546629671844, "ARRIVING")) | |
| for id, squad in pairs(data.gvars.gv_Squads) do | |
| if IsT(squad.Name) and _InternalTranslate(squad.Name) == arrivingSquadName then | |
| squad.Name = arrivingSquadName | |
| end | |
| end | |
| end | |
| -- Assign militia banters | |
| function OnMsg.EnterSector() | |
| for i, u in ipairs(g_Units) do | |
| if not u.militia then goto continue end | |
| local class = u.unitdatadef_id | |
| local banterTag = false | |
| if class == "MilitiaRookie" then | |
| banterTag = "Rookie" | |
| elseif class == "MilitiaElite" then | |
| banterTag = "Elite" | |
| elseif class == "MilitiaVeteran" then | |
| banterTag = "Veteran" | |
| else | |
| banterTag = "" | |
| assert(false, "Unknown militia class - " .. class) | |
| end | |
| local list = {} | |
| for i, b in ipairs(Presets.BanterDef.Banters_Militia) do | |
| if string.match(b.id, banterTag) then | |
| list[#list + 1] = b.id | |
| end | |
| end | |
| u.banters = list | |
| if banterTag ~= "" and #list == 0 then | |
| assert(false, "didn't find any militia banters for militia unit of class " .. class) | |
| end | |
| ::continue:: | |
| end | |
| end | |
| -- sides | |
| function GetSectorPOITypes() | |
| return { "all", "Mine", "Guardpost", "Port" } | |
| end | |
| -- maintain counts of player-side sectors with POIs or belonging to cities, to avoid iterating when querying these numbers in quest conditions | |
| GameVar("gv_PlayerSectorCounts", { all = 0, Mine = 0, Guardpost = 0, Port = 0 }) | |
| GameVar("gv_PlayerCityCounts", function() return { count = 0, cities = {} } end) -- cities is [city name] = <number of sectors of that city owned by the player> | |
| function SatelliteSectorSetSide(sector_id, side, force) | |
| local sector = gv_Sectors[sector_id] | |
| if not force and sector.StickySide or sector.Side == side then return end | |
| local old_side = sector.Side | |
| sector.Side = side | |
| local sector_buildings = {} | |
| for _, poi in ipairs(POIDescriptions) do | |
| if sector[poi.id] then | |
| sector_buildings[#sector_buildings + 1] = poi.display_name | |
| end | |
| end | |
| local sector_building_text | |
| if sector_buildings and #sector_buildings > 0 then | |
| sector_building_text = table.concat(sector_buildings, ", ") | |
| end | |
| if side == "player1" then | |
| if sector.City ~= "none" then | |
| if sector_building_text then | |
| CombatLog("important", T{156954263652, "Established control over <em><SectorName(sector)> (<sector_building>)</em> in <em><SettlementName></em>", sector = sector, SettlementName = gv_Cities[sector.City].DisplayName,sector_building = sector_building_text}) | |
| else | |
| CombatLog("important", T{695564592147, "Established control over <em><SectorName(sector)></em> in <em><SettlementName></em>", sector = sector, SettlementName = gv_Cities[sector.City].DisplayName}) | |
| end | |
| else | |
| if sector_building_text then | |
| CombatLog("important", T{927281946743, "Established control over <em><SectorName(sector)> (<sector_building>)</em>", sector = sector,sector_building = sector_building_text}) | |
| else | |
| CombatLog("important", T{656688610359, "Established control over <em><SectorName(sector)></em>", sector = sector}) | |
| end | |
| end | |
| gv_LastSectorTakenByPlayer = sector_id | |
| elseif old_side == "player1" and side ~= "neutral" and side ~= "ally" then | |
| if sector.City ~= "none" then | |
| if sector_building_text then | |
| CombatLog("important", T{258379206579, "Lost control of <em><SectorName(sector)> (<sector_building>)</em> in <em><SettlementName></em>", sector = sector, SettlementName = gv_Cities[sector.City].DisplayName,sector_building = sector_building_text}) | |
| else | |
| CombatLog("important", T{418999371512, "Lost control of <em><SectorName(sector)></em> in <em><SettlementName></em>", sector = sector, SettlementName = gv_Cities[sector.City].DisplayName}) | |
| end | |
| else | |
| if sector_building_text then | |
| CombatLog("important", T{440720361823, "Lost control of <em><SectorName(sector)> (<sector_building>)</em>", sector = sector, sector_building = sector_building_text}) | |
| else | |
| CombatLog("important", T{371968542080, "Lost control of <em><SectorName(sector)></em>", sector = sector}) | |
| end | |
| end | |
| end | |
| ExecuteSectorEvents("SE_OnSideChange", sector_id) | |
| if side == "player1" or side == "player2" then | |
| if old_side ~= "player1" and old_side ~= "player2" then | |
| gv_PlayerSectorCounts.all = gv_PlayerSectorCounts.all + 1 | |
| for _, poi in ipairs(GetSectorPOITypes()) do | |
| if sector[poi] then | |
| gv_PlayerSectorCounts[poi] = (gv_PlayerSectorCounts[poi] or 0) + 1 | |
| end | |
| end | |
| local sector_city = sector.City | |
| if sector_city and sector_city ~= "none" then | |
| gv_PlayerCityCounts.cities[sector_city] = (gv_PlayerCityCounts.cities[sector_city] or 0) + 1 | |
| gv_PlayerCityCounts.count = table.count(gv_PlayerCityCounts.cities) | |
| end | |
| end | |
| ExecuteSectorEvents("SE_PlayerControl", sector_id) | |
| elseif old_side == "player1" or old_side == "player2" then | |
| assert(gv_PlayerSectorCounts.all) | |
| gv_PlayerSectorCounts.all = gv_PlayerSectorCounts.all - 1 | |
| for _, poi in ipairs(GetSectorPOITypes()) do | |
| if sector[poi] then | |
| assert(gv_PlayerSectorCounts[poi] and gv_PlayerSectorCounts[poi] > 0) | |
| gv_PlayerSectorCounts[poi] = gv_PlayerSectorCounts[poi] - 1 | |
| end | |
| end | |
| local sector_city = sector.City | |
| if sector_city and sector_city ~= "none" then | |
| assert(gv_PlayerCityCounts.cities[sector_city] > 0) | |
| gv_PlayerCityCounts.cities[sector_city] = gv_PlayerCityCounts.cities[sector_city] - 1 | |
| if gv_PlayerCityCounts.cities[sector_city] == 0 then | |
| gv_PlayerCityCounts.cities[sector_city] = nil | |
| gv_PlayerCityCounts.count = table.count(gv_PlayerCityCounts.cities) | |
| end | |
| end | |
| end | |
| if old_side ~= "player1" and side == "player1" then | |
| sector.last_own_campaign_time = Game.CampaignTime | |
| end | |
| ObjModified(sector) | |
| Msg("SectorSideChanged", sector_id, old_side, side) | |
| return true | |
| end | |
| function SavegameSessionDataFixups.PlayerSectorCounts(data) | |
| if not data.gvars.gv_PlayerSectorCounts then | |
| local counts = { all = 0 } | |
| local city_counts = { count = 0, cities = {} } | |
| local pois = GetSectorPOITypes() | |
| for _, sector in pairs(data.gvars.gv_Sectors) do | |
| if sector.Side == "player1" or sector.Side == "player2" then | |
| counts.all = counts.all + 1 | |
| for _, poi in ipairs(pois) do | |
| if sector[poi] then | |
| counts[poi] = (counts[poi] or 0) + 1 | |
| end | |
| end | |
| if sector.City and sector.City ~= "none" then | |
| city_counts.cities[sector.City] = (city_counts.cities[sector.City] or 0) + 1 | |
| end | |
| end | |
| end | |
| data.gvars.gv_PlayerSectorCounts = counts | |
| city_counts.count = table.count(city_counts.cities) | |
| data.gvars.gv_PlayerCityCounts = city_counts | |
| end | |
| end | |
| function SavegameSessionDataFixups.WaterTravelLeftover(data) | |
| local l_gv_squads = GetGameVarFromSession(data, "gv_Squads") | |
| local l_gv_sectors = GetGameVarFromSession(data, "gv_Sectors") | |
| for k, squad in pairs(l_gv_squads) do | |
| local sectorId = squad.CurrentSector | |
| local isWater = l_gv_sectors[sectorId] | |
| isWater = isWater and isWater.Passability == "Water" | |
| if squad.water_route and not isWater then | |
| squad.water_route = false | |
| end | |
| end | |
| end | |
| function SavegameSessionDataFixups.ReplaceSectorsWithIDs(data) | |
| local quests = GetGameVarFromSession(data, "gv_Quests") | |
| for merc_id, merc_data in pairs(quests.MercStateTracker) do | |
| if type(merc_data) == "table" then | |
| for _, entry in ipairs(merc_data.EmploymentHistory or empty_table) do | |
| if entry.context and type(entry.context.sector) == "table" then | |
| entry.context.sector = entry.context.sector.Id | |
| end | |
| end | |
| end | |
| end | |
| end | |
| function SectorSetStickySide(sector_id, sticky) | |
| gv_Sectors[sector_id].StickySide = sticky | |
| end | |
| -- general | |
| function OnMsg.PreLoadNetGame() | |
| CloseSatelliteView(true) | |
| CloseDialog("ModifyWeaponDlg", true) | |
| end | |
| function OnMsg.ChangeMap() | |
| CloseSatelliteView(true) | |
| CloseDialog("ModifyWeaponDlg", true) | |
| end | |
| function OnMsg.NewGameSessionStart() | |
| CloseSatelliteView(true) | |
| end | |
| if Platform.developer then | |
| local function ShowSectorMapVMEs() | |
| local map = GetMapName() | |
| local map_sectors = {} | |
| local entrance_marker_err_sector_ids = {} | |
| local entrance_marker_dirs = {} | |
| local neighbors_with_map = {} | |
| for c_id, c in pairs(CampaignPresets) do | |
| for s_id, s in pairs(c.Sectors or empty_table) do | |
| if s.Map == map and not s.GroundSector then | |
| map_sectors[c_id] = map_sectors[c_id] or {} | |
| table.insert(map_sectors[c_id], s) | |
| neighbors_with_map[s] = {} | |
| for _, dir in ipairs(const.WorldDirections) do | |
| local n_id = GetNeighborSector(s.Id, dir, c) | |
| local idx = table.find(c.Sectors, "Id", n_id) | |
| local n = c.Sectors[idx] | |
| if n and n.Map then | |
| neighbors_with_map[s][dir] = true | |
| end | |
| end | |
| end | |
| end | |
| end | |
| if next(map_sectors) then | |
| for c_id, sectors in pairs(map_sectors) do | |
| for _, sector in ipairs(sectors) do | |
| -- check entrance markers | |
| --[[ for _, dir in ipairs(const.WorldDirections) do | |
| if sector.Passability ~= "Blocked" and (not sector.BlockTravel or not sector.BlockTravel[dir]) then | |
| if #MapGetMarkers("Entrance", dir) == 0 and neighbors_with_map[sector][dir] then | |
| entrance_marker_err_sector_ids[c_id] = entrance_marker_err_sector_ids[c_id] or {} | |
| table.insert_unique(entrance_marker_err_sector_ids[c_id], sector.Id) | |
| table.insert(entrance_marker_dirs, dir) | |
| end | |
| end | |
| end]] | |
| --check intel | |
| if mapdata.ScriptingStatus == "Ready" then | |
| local intel_marker = MapGetFirst("map", "IntelMarker") | |
| if not intel_marker and sector.Intel then | |
| StoreErrorSource(point30, "No intel markers on a sector with intel - " .. sector.Id) | |
| end | |
| if intel_marker and not sector.Intel then | |
| StoreErrorSource(intel_marker, "Intel marker(s) on a sector without intel - " .. sector.Id) | |
| end | |
| end | |
| --check defender markers with both allowed and forbidden entries in ArchetypesTriState | |
| local def_markers = MapGetMarkers("DefenderPriority", false, function(m) return m.ArchetypesTriState end) | |
| if def_markers then | |
| for _, def_marker in ipairs(def_markers) do | |
| local errorMsg = def_marker:GetError() | |
| if errorMsg then | |
| StoreErrorSource(def_marker, errorMsg) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| end | |
| --[[ if next(entrance_marker_err_sector_ids) then | |
| local lines = {} | |
| for c_id, sectors in pairs(entrance_marker_err_sector_ids) do | |
| lines[#lines + 1] = string.format("%s: %s", c_id, table.concat(sectors, ", ")) | |
| end | |
| StoreWarningSource(point(terrain.GetMapSize()/2, terrain.GetMapSize()/2), string.format("This map corresponds to sectors: %s. Missing Entrance markers on map: %s", | |
| table.concat(lines, "; "), table.concat(entrance_marker_dirs, ", "))) | |
| end]] | |
| end | |
| OnMsg.PostSaveMap = ShowSectorMapVMEs | |
| OnMsg.NewMapLoaded = ShowSectorMapVMEs | |
| end --Platform.developer | |
| -- satellite <-> sector transition | |
| function SpawnSquadUnits(session_ids, positions, marker_angle, defender_marker, entrance_marker) | |
| assert(#session_ids == #positions, "Not enough spawn positions were found") | |
| for i, session_id in ipairs(session_ids) do | |
| local unit_data = gv_UnitData[session_id] | |
| unit_data.already_spawned_on_map = true | |
| local angle = type(marker_angle) == "table" and marker_angle[i] or marker_angle | |
| local groups, routine, routine_area, name | |
| local marker = IsValid(defender_marker) and defender_marker or type(defender_marker) == "table" and defender_marker[i] | |
| local isEnemy = false | |
| if IsEnemySquad(unit_data.Squad) then -- add enemy squad units to EnemySquad group and to their defender marker groups | |
| isEnemy = true | |
| groups = {"EnemySquad"} | |
| if marker and marker.Groups then | |
| table.iappend(groups, marker.Groups) | |
| end | |
| end | |
| if IsGridMarkerWithDefenderRole(marker) then | |
| routine = marker.Routine | |
| routine_area = marker.RoutineArea | |
| name = marker.Name | |
| end | |
| if positions[i] then | |
| local class = unit_data.class | |
| local unit = SpawnUnit(class, session_id, positions[i], angle, groups, nil, entrance_marker) | |
| if routine ~= nil then unit.routine = routine end | |
| if routine_area~= nil then unit.routine_area = routine_area end | |
| if isEnemy and name and name ~= "" then unit.Name = name end | |
| unit.routine_spawner = marker | |
| end | |
| end | |
| end | |
| local function InsertMarkerInfo(markers_info, marker_type, key, session_id) | |
| markers_info[marker_type][key] = markers_info[marker_type][key] or {} | |
| table.insert(markers_info[marker_type][key], session_id) | |
| end | |
| function FillMarkerInfoExplore(markers_info, squads_to_spawn) | |
| for squad_id, session_ids in sorted_pairs(squads_to_spawn) do | |
| local squad = gv_Squads[squad_id] | |
| assert(squad.Side == "player1" or squad.Side == "player2" or squad.Side == "ally", | |
| string.format("Trying to explore a sector with a non-player squad in it (where should it be placed). Squad: %s, Side: %s", squad.Name, squad.Side)) | |
| for _, session_id in ipairs(session_ids) do | |
| local unit_data = gv_UnitData[session_id] | |
| if squad.Side == "ally" and squad.militia then | |
| InsertMarkerInfo(markers_info, "defend_priority", squad_id, session_id) | |
| elseif not unit_data.arrival_dir then | |
| InsertMarkerInfo(markers_info, "defend", squad_id, session_id) | |
| else | |
| InsertMarkerInfo(markers_info, "entrance", unit_data.arrival_dir, session_id) | |
| end | |
| end | |
| end | |
| end | |
| -- Each defender priority marker can house up to one unit. | |
| function SpawnOnDefenderPriorityMarkerPositions(session_ids) | |
| if #session_ids <= 0 then | |
| return session_ids | |
| end | |
| local def_markers = MapGetMarkers("DefenderPriority", false, function(m) | |
| if not m:IsMarkerEnabled() then | |
| return false | |
| end | |
| local passSlab = SnapToPassSlab(m) | |
| if not passSlab or IsOccupiedExploration(nil, passSlab:xyz()) then | |
| return false | |
| end | |
| return true | |
| end) | |
| local remaining_sids = table.copy(session_ids) | |
| local unitToMarker = {} | |
| local occupied_priority_markers = {} | |
| -- Try to match units to markers which prefer those units in particular. | |
| for _, marker in ipairs(def_markers) do | |
| if not marker.UnitDef then goto continue end | |
| for _, sid in ipairs(remaining_sids) do | |
| local unitData = gv_UnitData[sid] | |
| if unitData.class == marker.UnitDef then | |
| unitToMarker[sid] = marker | |
| occupied_priority_markers[marker] = true | |
| table.remove_value(remaining_sids, sid) | |
| break | |
| end | |
| end | |
| ::continue:: | |
| end | |
| -- Find eligible markers for each enemy role. | |
| local roleToMarker = {} | |
| local enemyRoles = Presets.EnemyRole.Default | |
| for i, rolePreset in ipairs(enemyRoles) do | |
| local role = rolePreset.id | |
| local thisRoleMarkers = {} | |
| for _, def_marker in ipairs(def_markers) do | |
| local overwriteBeastOnMarker = false | |
| local shouldSpawn = false | |
| if def_marker.Archetypes and next(def_marker.Archetypes) then -- Old property overrides new one. | |
| shouldSpawn = not not table.find(def_marker.Archetypes, role) | |
| overwriteBeastOnMarker = shouldSpawn | |
| elseif def_marker.ArchetypesTriState then | |
| local onlyDisabledOthers = def_marker.ArchetypesTriState[role] == nil and table.values(def_marker.ArchetypesTriState)[1] == false | |
| local enabledMe = def_marker.ArchetypesTriState[role] | |
| shouldSpawn = onlyDisabledOthers or enabledMe | |
| overwriteBeastOnMarker = enabledMe | |
| end | |
| -- Dont spawn beasts on high up markers unless they are specifically set. | |
| if role == "Beast" and not overwriteBeastOnMarker then | |
| local floor = GetFloorOfPos(def_marker:GetPosXYZ()) | |
| if floor >= 1 then -- zero indexed | |
| shouldSpawn = false | |
| end | |
| end | |
| if shouldSpawn then | |
| table.insert(thisRoleMarkers, def_marker) | |
| end | |
| end | |
| roleToMarker[role] = thisRoleMarkers | |
| end | |
| -- Try to match units to markers which prefer these units' roles. | |
| for sIdx, sid in ipairs(remaining_sids) do | |
| local ud = gv_UnitData[sid] | |
| local unitRole = ud.role | |
| if not unitRole then goto continue end | |
| local markers = roleToMarker[unitRole] | |
| if #markers == 0 then goto continue end | |
| for i, m in ipairs(markers) do | |
| if not occupied_priority_markers[m] then | |
| unitToMarker[sid] = m | |
| occupied_priority_markers[m] = true | |
| remaining_sids[sIdx] = nil | |
| break | |
| end | |
| end | |
| ::continue:: | |
| end | |
| table.compact(remaining_sids) | |
| -- Fill the rest of the markers with the rest of the units. | |
| for _, marker in ipairs(def_markers) do | |
| for _, sid in ipairs(remaining_sids) do | |
| if not occupied_priority_markers[marker] then | |
| unitToMarker[sid] = marker | |
| occupied_priority_markers[marker] = true | |
| table.remove_value(remaining_sids, sid) | |
| break | |
| end | |
| end | |
| end | |
| --[[ print("Units Had To Deploy:", #session_ids) | |
| print("Markers Available:", #def_markers) | |
| print("Left for Defender Markers:", #remaining_sids)]] | |
| local result_sids, spawn_positions, spawn_angles, spawn_markers = {}, {}, {}, {} | |
| for _, sid in ipairs(session_ids) do | |
| local marker = unitToMarker[sid] | |
| if marker then | |
| result_sids[#result_sids + 1] = sid | |
| spawn_positions[#spawn_positions + 1] = SnapToPassSlab(marker) | |
| spawn_angles[#spawn_angles + 1] = marker:GetAngle() | |
| spawn_markers[#spawn_markers + 1] = marker | |
| end | |
| end | |
| SpawnSquadUnits(result_sids, spawn_positions, spawn_angles, spawn_markers) | |
| return remaining_sids | |
| end | |
| function FillDefenderMarkerPositions(count, spawn_positions, spawn_angles, spawn_markers) | |
| if count <= 0 then return end | |
| local markers = MapGetMarkers("Defender", false, function(m)return m:IsMarkerEnabled() end) | |
| if not markers or #markers == 0 then | |
| StoreErrorSource(false, "No enabled Defender markers found on map") | |
| markers = MapGetMarkers("Entrance") | |
| end | |
| local _, positions, _, meta = GetRandomSpreadSpawnMarkerPositions(markers, count) | |
| for i, pos in ipairs(positions) do | |
| local positionMeta = meta[i] | |
| spawn_positions[#spawn_positions+1] = pos | |
| spawn_angles[#spawn_angles+1] = positionMeta[2] | |
| spawn_markers[#spawn_markers+1] = positionMeta[3] | |
| end | |
| end | |
| MapVar("g_GroupedSquadUnits", {}) | |
| local cardinalToAngle = { | |
| ["North"] = 0, | |
| ["East"] = 90, | |
| ["South"] = 180, | |
| ["West"] = 270 | |
| } | |
| DefineClass.AutoGeneratedEntranceMarker = { | |
| __parents = { "GameDynamicSpawnObject", "SyncObject", "GridMarker" }, | |
| exit_zone_interactable = false, | |
| underground = false | |
| } | |
| -- load | |
| function AutoGeneratedEntranceMarker:SetDynamicData(data) | |
| if data.exit_zone_handle then | |
| local handle = data.exit_zone_handle | |
| local exitZone = HandleToObject[handle] | |
| if IsKindOf(exitZone, "ExitZoneInteractable") then | |
| local underground = data.underground | |
| if underground then | |
| GenerateUndergroundMarker(exitZone, self) | |
| else | |
| GenerateEntranceMarker(exitZone, self) | |
| end | |
| end | |
| end | |
| end | |
| -- save | |
| function AutoGeneratedEntranceMarker:GetDynamicData(data) | |
| if self.exit_zone_interactable then | |
| data.exit_zone_handle = self.exit_zone_interactable:GetHandle() | |
| data.underground = self.underground | |
| end | |
| end | |
| function GenerateUndergroundMarker(exitZoneInteractable, placedMarker) | |
| local direction = exitZoneInteractable.Groups[1] | |
| local markersInThisDirection = MapGetMarkers("Entrance", direction, function(marker) return marker ~= placedMarker end) | |
| if markersInThisDirection and #markersInThisDirection > 0 then | |
| return | |
| end | |
| local fakeMarker = placedMarker or PlaceObject("AutoGeneratedEntranceMarker") | |
| fakeMarker:ClearGameFlags(const.gofPermanent) -- place object spawns non-perma, but this is to clarify intention | |
| fakeMarker:SetType("Entrance") | |
| fakeMarker:SetGroups(exitZoneInteractable.Groups) | |
| fakeMarker:SetPos(SnapToVoxel(exitZoneInteractable:GetPos())) | |
| fakeMarker:SetAngle(exitZoneInteractable:GetAngle() - 90 * 60) | |
| fakeMarker:SetAreaWidth(5) | |
| fakeMarker:SetAreaHeight(5) | |
| fakeMarker.GroundVisuals = true | |
| fakeMarker.IsMarkerEnabled = function() | |
| return exitZoneInteractable:IsMarkerEnabled() | |
| end | |
| fakeMarker.underground = true | |
| fakeMarker.exit_zone_interactable = exitZoneInteractable | |
| table.insert(g_InteractableAreaMarkers, fakeMarker) | |
| end | |
| local lDeployAlongMapSize = 5 | |
| function GenerateEntranceMarker(exitZoneInteractable, placedMarker) | |
| local direction = exitZoneInteractable.Groups[1] | |
| local entranceMarkerPos = exitZoneInteractable:GetPos() | |
| local markersInThisDirection = MapGetMarkers("Entrance", direction, function(marker) return marker ~= placedMarker end) | |
| if markersInThisDirection and #markersInThisDirection > 0 then | |
| return | |
| end | |
| local fakeMarker = placedMarker or PlaceObject("AutoGeneratedEntranceMarker") | |
| fakeMarker:ClearGameFlags(const.gofPermanent) -- place object spawns non-perma, but this is to clarify intention | |
| fakeMarker:SetType("Entrance") | |
| fakeMarker:SetGroups({ direction }) | |
| fakeMarker.GroundVisuals = true | |
| fakeMarker.underground = false | |
| fakeMarker.exit_zone_interactable = exitZoneInteractable | |
| table.insert(g_InteractableAreaMarkers, fakeMarker) | |
| local mapDir = mapdata.MapOrientation - cardinalToAngle[direction] | |
| local mapCenter = point(terrain.GetMapSize()) / 2 | |
| local bam = GetBorderAreaMarker() | |
| local mapDirectionSide = (GetMapPositionAlongOrientation(mapDir) - mapCenter + bam:GetPos()):SetInvalidZ() | |
| local borderBox = GetBorderAreaLimits() | |
| local bamCenter = borderBox:Center() | |
| mapDirectionSide = ClampPoint(mapDirectionSide, borderBox) | |
| local sizex, sizey = borderBox:sizexyz() | |
| if direction == "East" or direction == "West" then | |
| local offset = sign(entranceMarkerPos:x() - mapDirectionSide:x()) * const.SlabSizeX * lDeployAlongMapSize/2 | |
| mapDirectionSide = point(mapDirectionSide:x() + offset, mapDirectionSide:y()) | |
| mapDirectionSide = SnapToVoxel(mapDirectionSide) | |
| if not GetPassSlab(mapDirectionSide) then | |
| mapDirectionSide = entranceMarkerPos | |
| end | |
| fakeMarker:SetPos(mapDirectionSide) | |
| fakeMarker:SetAreaWidth(lDeployAlongMapSize) | |
| local offsetFromCenter = abs(entranceMarkerPos:y() - bamCenter:y()) | |
| sizey = sizey + (offsetFromCenter * 2) | |
| sizey = DivCeil(sizey, const.SlabSizeY) | |
| if sizey % 2 == 0 then sizey = sizey + 1 end | |
| fakeMarker:SetAreaHeight(sizey) | |
| mapDir = (mapDir - 90) % 360 | |
| fakeMarker:SetAngle(mapDir * 60) | |
| elseif direction == "North" or direction == "South" then | |
| local offset = sign(entranceMarkerPos:y() - mapDirectionSide:y()) * const.SlabSizeY * lDeployAlongMapSize/2 | |
| mapDirectionSide = point(mapDirectionSide:x(), mapDirectionSide:y() + offset) | |
| mapDirectionSide = SnapToVoxel(mapDirectionSide) | |
| if not GetPassSlab(mapDirectionSide) then | |
| mapDirectionSide = entranceMarkerPos | |
| end | |
| fakeMarker:SetPos(mapDirectionSide) | |
| fakeMarker:SetAreaHeight(lDeployAlongMapSize) | |
| local offsetFromCenter = abs(entranceMarkerPos:x() - bamCenter:x()) | |
| sizex = sizex + (offsetFromCenter * 2) | |
| sizex = DivCeil(sizex, const.SlabSizeX) | |
| if sizex % 2 == 0 then sizex = sizex + 1 end | |
| fakeMarker:SetAreaWidth(sizex) | |
| mapDir = (mapDir + 90) % 360 | |
| fakeMarker:SetAngle(mapDir * 60) | |
| end | |
| end | |
| function SpawnSquads(squad_ids, spawn_mode, spawn_markers, force_test_map, enter_sector) | |
| local remove_dead = not not enter_sector | |
| g_GroupedSquadUnits = {} | |
| local squads_to_spawn = {} | |
| local map_combat_units = MapGet("map", "Unit") or empty_table | |
| -- remove map objs for mercs which are no longer with the squad saved on the sector, also all dead bodies on map reenter | |
| for i = #map_combat_units, 1, -1 do | |
| local obj = map_combat_units[i] | |
| local session_id = obj.session_id | |
| local delete_obj | |
| if not obj.spawner or obj.Squad then -- don't remove spawner units, except those who joined a squad | |
| -- unit was on this map once and either died here (which why they have no squad), or their squad isnt here anymore. | |
| local squad = gv_Squads[obj.Squad] | |
| local squad_units = table.find(squad_ids, obj.Squad) and squad and squad.units or empty_table | |
| local missing = (remove_dead or not obj:IsDead()) and not table.find(squad_units, session_id) | |
| -- unit was on this map once and has returned after having been somewhere else, it needs to be respawned (lUpdateSquadCurrentSector) | |
| local should_respawn = gv_UnitData[session_id] and not gv_UnitData[session_id].already_spawned_on_map | |
| -- Enemies will always redeploy (211594) to simulate time passing, | |
| -- if they are part of a squad. | |
| if enter_sector and squad and squad.Side == "enemy1" then | |
| should_respawn = true | |
| end | |
| delete_obj = missing or should_respawn | |
| elseif remove_dead and obj.spawner and obj:IsDead() and not obj:IsPersistentDead() then | |
| if obj:HasPassedTimeAfterDeath(const.Satellite.RemoveDeadBodiesAfter) then | |
| delete_obj = true | |
| end | |
| end | |
| if delete_obj then | |
| -- drop loot here | |
| if obj:IsDead() then | |
| -- Unit:DropLoot() should have been already called at this point from somewhere else. | |
| local sector = gv_Sectors[gv_CurrentSectorId] | |
| local diedHere = next(sector.dead_units) and table.find(sector.dead_units, session_id) | |
| if diedHere then | |
| table.remove_value(sector.dead_units, obj.session_id) | |
| obj:DropAllItemsInAContainer() | |
| end | |
| obj:Despawn()-- also remove UnitData and do not save it in savegame | |
| else | |
| DoneObject(obj) | |
| end | |
| table.remove(map_combat_units, i) | |
| end | |
| end | |
| -- add map objs for mercs which were not with the squad saved on the sector | |
| for _, squad_id in ipairs(squad_ids) do | |
| local squad = gv_Squads[squad_id] | |
| assert(squad) -- false when squad exists in SquadArray but not in gv_Squads | |
| for _, session_id in ipairs(squad and squad.units) do | |
| if not table.find_value(map_combat_units, "session_id", session_id) and not gv_UnitData[session_id].retreat_to_sector then | |
| squads_to_spawn[squad_id] = squads_to_spawn[squad_id] or {} | |
| table.insert(squads_to_spawn[squad_id], session_id) | |
| end | |
| end | |
| end | |
| if spawn_mode then | |
| local markers_info = {defend_priority = {}, defend = {}, entrance = {}} | |
| local sorted_group_on_markers = {} | |
| if spawn_mode == "explore" then | |
| FillMarkerInfoExplore(markers_info, squads_to_spawn) | |
| else | |
| for squad_id, session_ids in sorted_pairs(squads_to_spawn) do | |
| local squad = gv_Squads[squad_id] | |
| if spawn_markers and spawn_markers[squad_id] then -- if spawn_markers is specified for this squad, use that instead of the standard logic | |
| local marker_type, marker_group = table.unpack(spawn_markers[squad_id]) | |
| local markers = MapGetMarkers(marker_type, marker_group, function(m) return m:IsMarkerEnabled() end) | |
| local _, positions, marker_angle = GetRandomSpawnMarkerPositions(markers, #session_ids) | |
| SpawnSquadUnits(session_ids, positions, marker_angle) | |
| elseif (squad.Side == "enemy1" or squad.Side == "enemy2") and spawn_mode == "defend" then -- enemies attacking, group them in small batches of 2-4 units | |
| local markers = GetAvailableEntranceMarkers(gv_UnitData[session_ids[1]].arrival_dir) | |
| local idx = 0 | |
| while idx < #session_ids do | |
| local marker = table.interaction_rand(markers, "SpawnEnemies") | |
| local count = Min(InteractionRandRange(2, 4), #session_ids - idx) | |
| if idx + count == #session_ids - 1 then -- don't leave one unit alone for the last group | |
| count = count + 1 | |
| end | |
| local group_on_marker = {marker} | |
| table.insert(sorted_group_on_markers, group_on_marker) | |
| for i = idx + 1, idx + count do | |
| InsertMarkerInfo(markers_info, "entrance", #sorted_group_on_markers, session_ids[i]) | |
| idx = idx + 1 | |
| end | |
| end | |
| else | |
| for _, session_id in ipairs(session_ids) do | |
| local unit_data = gv_UnitData[session_id] | |
| if force_test_map then | |
| unit_data.arrival_dir = "North" | |
| end | |
| -- militia and enemy mercs use priority defender markers first, villain have special defender priority markers | |
| if (spawn_mode == "defend" and (squad.Side == "player1" or squad.Side == "ally") and squad.militia) or (spawn_mode == "attack" and squad.Side ~= "player1") then | |
| InsertMarkerInfo(markers_info, "defend_priority", squad_id, session_id) | |
| elseif spawn_mode == "defend" and squad.Side == "player1" then | |
| InsertMarkerInfo(markers_info, "defend", squad_id, session_id) | |
| else | |
| InsertMarkerInfo(markers_info, "entrance", unit_data.arrival_dir, session_id) | |
| end | |
| end | |
| end | |
| end | |
| end | |
| local occupied_priority_markers = {} | |
| for squad_id, session_ids in sorted_pairs(markers_info.defend_priority) do | |
| local spawn_positions, spawn_angles, spawn_markers = {}, {}, {} | |
| local remaining = SpawnOnDefenderPriorityMarkerPositions(session_ids) | |
| -- Add remaning units to the normal defender marker deploy | |
| if #remaining > 0 then | |
| local squadUnits = markers_info.defend[squad_id] | |
| if not squadUnits then | |
| markers_info.defend[squad_id] = remaining | |
| else | |
| local list = markers_info.defend[squad_id] | |
| for i, session_id in ipairs(remaining) do | |
| list[#list + 1] = session_id | |
| end | |
| end | |
| end | |
| end | |
| -- Deploy all enemies together as a single squad to better spread them out. | |
| local allSquadsFlattened = {} | |
| for squad_id, session_ids in sorted_pairs(markers_info.defend) do | |
| table.iappend(allSquadsFlattened, session_ids) | |
| end | |
| if true then | |
| local spawn_positions, spawn_angles, spawn_markers = {}, {}, {} | |
| FillDefenderMarkerPositions(#allSquadsFlattened, spawn_positions, spawn_angles, spawn_markers) | |
| SpawnSquadUnits(allSquadsFlattened, spawn_positions, spawn_angles, spawn_markers) | |
| end | |
| --[[ for squad_id, session_ids in sorted_pairs(markers_info.defend) do | |
| local spawn_positions, spawn_angles, spawn_markers = {}, {}, {} | |
| FillDefenderMarkerPositions(#session_ids, spawn_positions, spawn_angles, spawn_markers) | |
| SpawnSquadUnits(session_ids, spawn_positions, spawn_angles, spawn_markers) | |
| end]] | |
| local positions_per_marker = {} | |
| for key, session_ids in sorted_pairs(markers_info.entrance) do | |
| local markers | |
| local marker, positions, marker_angle | |
| if type(key) == "string" then | |
| markers = MapGetMarkers("Entrance", key, function(marker) return SnapToPassSlab(marker) end) | |
| if not markers or #markers == 0 then | |
| StoreErrorSource(session_ids, string.format("No enabled Entrance markers found on map '%s' with key '%s' - trying random entrance marker instead!", GetMapName(), key)) | |
| markers = MapGetMarkers("Entrance") | |
| if not markers or #markers == 0 then | |
| StoreErrorSource(session_ids, string.format("No enabled Entrance markers found on map '%s'!", GetMapName())) | |
| return | |
| end | |
| end | |
| --temp 0210007 | |
| NetUpdateHash("GetRandomSpawnMarkerPositions1", #session_ids, table.unpack(markers)) | |
| marker, positions, marker_angle = GetRandomSpawnMarkerPositions(markers, #session_ids, "around_center") | |
| NetUpdateHash("GetRandomSpawnMarkerPositions2", table.unpack(positions)) | |
| elseif type(key) == "number" or type(key) == "boolean" then | |
| if not key then | |
| markers = MapGetMarkers("Entrance") | |
| else | |
| markers = sorted_group_on_markers[key] | |
| end | |
| if not markers or #markers == 0 then | |
| StoreErrorSource(session_ids, string.format("No enabled Entrance markers found on map '%s'!", GetMapName())) | |
| markers = MapGetMarkers("Entrance") | |
| end | |
| marker, positions, marker_angle = GetRandomPositionsFromSpawnMarkersMaxDistApart(markers, #session_ids, positions_per_marker) | |
| -- add grouped units in global var | |
| g_GroupedSquadUnits[#g_GroupedSquadUnits + 1] = session_ids | |
| else | |
| assert(false, "sorted_pairs with weirdo key types is an async op!") | |
| end | |
| SpawnSquadUnits(session_ids, positions, marker_angle, nil, marker) | |
| end | |
| elseif spawn_markers then | |
| for squad_id, session_ids in sorted_pairs(squads_to_spawn) do | |
| local marker_type, marker_group = table.unpack(spawn_markers[squad_id] or empty_table) | |
| local markers = MapGetMarkers(marker_type, marker_group, function(m) return m:IsMarkerEnabled() end) | |
| local _, positions, marker_angle = GetRandomSpawnMarkerPositions(markers, #session_ids) | |
| SpawnSquadUnits(session_ids, positions, marker_angle) | |
| end | |
| end | |
| end | |
| MapVar("g_GoingAboveground", false) | |
| local function shouldSpawnSquad(squad, sector, ignore_travel) | |
| return squad.CurrentSector == sector.Id and (not IsSquadTravelling(squad) or ignore_travel) and not squad.arrival_squad | |
| end | |
| LoadSectorThread = false | |
| local enterSectorThread = false | |
| local doneEnterSector = false | |
| function EnterSector(sector_id, spawn_mode, spawn_markers, save_sector, force_test_map, game_start) | |
| if IsValidThread(enterSectorThread) then | |
| assert(false) --two concurrent enter sector threads! | |
| return | |
| end | |
| enterSectorThread = CurrentThread() | |
| doneEnterSector = false | |
| NetGossip("EnterSector", sector_id, spawn_mode, GetCurrentPlaytime(), game_start) | |
| if netInGame and GetMapName() ~= "" and not GetGamePause() and not IsGameReplayRunning() then | |
| --entering_sector is treated as sync, so try to make it such when possible | |
| FireNetSyncEventOnHost("EnteringSectorSync") | |
| local state = {entering_sector = true} | |
| local timeout = 5000 | |
| local start_ts = GetPreciseTicks() | |
| while not MatchGameState(state) and GetPreciseTicks() - start_ts < timeout do | |
| WaitMsg("GameStateChanged", timeout) | |
| end | |
| end | |
| ChangeGameState { entering_sector = true } | |
| -- If there is a conflict auto save running, wait for it to finish cuz crashes can occur otherwise. | |
| while IsAutosaveScheduled() do | |
| Sleep(1) | |
| end | |
| spawn_mode = force_test_map and "attack" or spawn_mode | |
| local load_game = not spawn_mode and not spawn_markers | |
| if not load_game and not force_test_map and gv_Sectors[sector_id] then | |
| ExecuteSectorEvents("SE_PreChangeMap", sector_id, "wait") | |
| end | |
| local sat_view_loading_screen = GameState.loading_savegame and gv_SatelliteView | |
| local id = sat_view_loading_screen and "idSatelliteView" or "idEnterSector" | |
| SectorLoadingScreenOpen(id, "enter sector", not sat_view_loading_screen and sector_id) | |
| local sector = gv_Sectors[sector_id] | |
| local ambient_timeouted | |
| if sector and not GameState.loading_savegame then | |
| ambient_timeouted = sector.last_enter_campaign_time and | |
| (Game.CampaignTime - sector.last_enter_campaign_time > const.AmbientLife.SatelliteTimeout) | |
| sector.last_enter_campaign_time = Game.CampaignTime | |
| -- When entering a sector you can no longer wait. | |
| if sector.conflict then | |
| sector.conflict.waiting = false | |
| SetCampaignSpeed(0, "SatelliteConflict") | |
| end | |
| CheckSectorRadioStations(sector) | |
| end | |
| SkipAnySetpieces() | |
| ChangeGameState("setpiece_playing", false) --we've tried skipping setpieces gracefully, but if a setpiece hadn't started yet it may leave this on once change map kills its threads | |
| -- Wait for net sync events to end since the map change will delete them. | |
| NetSyncEventFence("EnterSector") | |
| NetGameSend("rfnClearHash") --this clears passed caches stored server side so new hashes @ the same gametime doesn't cause desyncs | |
| NetStartBufferEvents("EnterSector") | |
| -- Leaving the map? Delete ephemeral units and fast forward commands | |
| if not GameState.loading_savegame then | |
| --deleting sync objs in rtt, awesome. | |
| --it's kind of sort of sync after the fence, but not really | |
| local units = table.copy(g_Units) | |
| for i, u in ipairs(units) do | |
| if u.ephemeral then | |
| u:Despawn() | |
| elseif not IsMerc(u) then | |
| u:FastForwardCommand() | |
| end | |
| end | |
| end | |
| if save_sector then | |
| GatherSectorDynamicData() -- create sector_data for the previous sector | |
| end | |
| NetSyncEvents.CloseSatelliteView(true) --this is presumably sync enough after fence | |
| OnSatViewClosed() | |
| gv_CurrentSectorId = sector_id | |
| g_GoingAboveground = false | |
| for _, squad in ipairs(g_SquadsArray) do | |
| if shouldSpawnSquad(squad, sector) and IsSectorUnderground(squad.PreviousSector) and not IsSectorUnderground(sector.Id) then | |
| g_GoingAboveground = true | |
| break | |
| end | |
| end | |
| -- Try to find sync handle generation during map init. | |
| -- These can break saves if they take the handle of an object that will be | |
| -- spawned by dynamic data. | |
| if Platform.developer then | |
| local postMapLoaded = false | |
| CreateRealTimeThread(function() | |
| WaitMsg("PostNewMapLoaded") | |
| postMapLoaded = true | |
| end) | |
| local genSyncHandleOld = GenerateSyncHandle | |
| GenerateSyncHandle = function(...) | |
| if postMapLoaded then | |
| GenerateSyncHandle = genSyncHandleOld | |
| else | |
| assert(false) -- Generating sync handle before map has loaded! | |
| end | |
| return genSyncHandleOld(...) | |
| end | |
| end | |
| ChangeGameState{loading_savegame = true} | |
| ChangeMap( | |
| force_test_map or | |
| sector.Map or | |
| (Platform.goldmaster and "EmptyMap" or "__CombatTest") | |
| ) | |
| ChangeGameState{loading_savegame = false, entered_sector = true} | |
| -- Load saved data | |
| local luaRevisionLoaded = ApplyDynamicData() | |
| -- Auto create entrance markers for directions in which entrances are not placed. | |
| for _, direction in sorted_pairs(const.WorldDirections) do | |
| local exitZoneInt = MapGetMarkers("ExitZoneInteractable", direction) | |
| exitZoneInt = exitZoneInt and exitZoneInt[1] | |
| local neighbour = exitZoneInt and GetNeighborSector(sector_id, direction) | |
| local validDirection = neighbour and not SectorTravelBlocked(neighbour, sector_id, false, "land_water_river") | |
| if force_test_map or validDirection then | |
| GenerateEntranceMarker(exitZoneInt) | |
| end | |
| end | |
| -- Auto create any above/underground entrances from exit zone interactables. | |
| local underground = MapGetMarkers("ExitZoneInteractable", false, ExitZoneInteractable.IsUndergroundExit) | |
| for i, exitZoneInt in ipairs(underground) do | |
| GenerateUndergroundMarker(exitZoneInt) | |
| end | |
| -- Reset retreating status of mercs if re-entering sector. | |
| if not load_game then | |
| local udHere = GetPlayerMercsInSector() | |
| for i, uId in ipairs(udHere) do | |
| local ud = gv_UnitData[uId] | |
| if ud and ud.retreat_to_sector then | |
| CancelUnitRetreat(ud) | |
| end | |
| end | |
| end | |
| if not load_game then -- do not clear var if savegame was made during deployment | |
| gv_Deployment = false | |
| end | |
| local deployment = not force_test_map and not SkipDeployment(spawn_mode) | |
| if deployment then | |
| SetDeploymentMode(spawn_mode or gv_Deployment) | |
| end | |
| if not load_game then | |
| local squad_ids = {} | |
| if force_test_map then | |
| table.insert(squad_ids, 1) | |
| else | |
| for _, squad in ipairs(g_SquadsArray) do | |
| if shouldSpawnSquad(squad, sector) then | |
| table.insert(squad_ids, squad.UniqueId) | |
| end | |
| end | |
| end | |
| if #squad_ids == 0 and IsSectorUnderground(sector_id) then | |
| for _, squad in ipairs(g_SquadsArray) do | |
| if shouldSpawnSquad(squad, sector, "ignore travel") then | |
| table.insert(squad_ids, squad.UniqueId) | |
| end | |
| end | |
| end | |
| UpdateSpawnersLocal() | |
| SpawnSquads(squad_ids, spawn_mode, spawn_markers, force_test_map, "enter_sector") | |
| end | |
| SetupTeamsFromMap() | |
| UpdateSpawnersLocal() | |
| Msg("EnterSector", game_start, load_game, luaRevisionLoaded) | |
| ListCallReactions(g_Units, "OnEnterSector", game_start, load_game) | |
| if not load_game or ambient_timeouted then | |
| Msg("AmbientLifeSpawn") | |
| end | |
| if not load_game and gv_CurrentSectorId and gv_Sectors[gv_CurrentSectorId] and gv_Sectors[gv_CurrentSectorId].Map == GetMapName() then | |
| ExecuteSectorEvents("SE_OnEnterMap", gv_CurrentSectorId, "wait") | |
| end | |
| if netInGame then | |
| if load_game then | |
| local rez = NetWaitGameStart() | |
| if rez == "timeout" then | |
| if NetIsHost() then | |
| NetEvent("RemoveClient", 2, "timeout") | |
| else | |
| NetLeaveGame("timeout") | |
| end | |
| end | |
| assert(AdvanceToGameTimeLimit == GameTime()) | |
| AdvanceToGameTimeLimit = GameTime() | |
| end | |
| Resume("net") | |
| end | |
| --apparently, this needs to be before netstopbuffer, idk why, otherwise we get the same events but with messed up thread order | |
| SectorLoadingScreenClose(id, "enter sector", not sat_view_loading_screen and sector_id) | |
| NetStopBufferEvents("EnterSector") | |
| if not load_game and gv_CurrentSectorId and gv_Sectors[gv_CurrentSectorId] and gv_Sectors[gv_CurrentSectorId].Map == GetMapName() then | |
| ExecuteSectorEvents("SE_OnEnterMapVisual", gv_CurrentSectorId, true) | |
| CreateOnEnterMapVisualMsg() | |
| end | |
| NetSyncEventFence() --make sure auto save happens after buffered events are executed in case they do something to the state that needs saving | |
| enterSectorThread = false --deployment test never actually leaves deployment before changing to next sector | |
| SetupDeployOrExploreUI(load_game) | |
| -- do not auto-save if deployment is delayed | |
| if not load_game and (not deployment or gv_DeploymentStarted) then | |
| RequestAutosave{ autosave_id = "sectorEnter", save_state = "SectorEnter", display_name = T{841930548612, "<u(Id)>_SectorEnter", gv_Sectors[gv_CurrentSectorId]}, mode = "delayed" } | |
| end | |
| NetSyncEvent("DoneEnterSector", netUniqueId) | |
| assert(not IsEditorActive() and cameraTac.IsActive(), "Map has non-tac camera activated") | |
| end | |
| function NetSyncEvents.LoadGame_SkipAnySetpieces() | |
| if IsValidThread(LoadSectorThread) and GameState.setpiece_playing then | |
| DeleteThread(LoadSectorThread) | |
| end | |
| CreateGameTimeThread(SkipAnySetpieces) | |
| end | |
| function LoadGame_SkipAnySetpieces() | |
| NetSyncEvent("LoadGame_SkipAnySetpieces") | |
| while GameState.setpiece_playing do | |
| WaitMsg("SetpieceEnded", 100) | |
| end | |
| end | |
| function NetSyncEvents.EnteringSectorSync() | |
| ChangeGameState { entering_sector = true } | |
| end | |
| function NetSyncEvents.DoneEnterSector(id) | |
| doneEnterSector = doneEnterSector or {} | |
| if id then | |
| doneEnterSector[id] = true | |
| end | |
| if not netInGame or table.count(doneEnterSector) >= table.count(netGamePlayers) then | |
| ChangeGameState { entering_sector = false } | |
| end | |
| Msg("DoneEnterSector") | |
| end | |
| function OnMsg.NetPlayerLeft(player, reason) | |
| if GameState.entering_sector and doneEnterSector and not doneEnterSector[player.id] then | |
| NetSyncEvents.DoneEnterSector() | |
| end | |
| end | |
| function LoadSector(sector_id, spawn_mode) | |
| --NetUpdateHash("LoadSector", sector_id, spawn_mode) --this is not sync code, it hasn't desynced till now cuz it gets dropped due to changemap | |
| if IsValidThread(enterSectorThread) then | |
| assert(false) --trying to enter sector more than once! | |
| return | |
| end | |
| local multiplayer = IsCoOpGame() | |
| if multiplayer then | |
| Pause("net") | |
| end | |
| LoadSectorThread = CreateRealTimeThread(function(multiplayer, sector_id, spawn_mode) | |
| if multiplayer then | |
| assert(netInGame) | |
| end | |
| LocalLoadSector(sector_id, spawn_mode, nil, "saveSector") | |
| end, multiplayer, sector_id, spawn_mode) | |
| end | |
| function LocalLoadSector(sector_id, spawn_mode, spawn_markers, save_sector, force_test_map, game_start) | |
| assert(CanYield()) | |
| if not EnterFirstSector(sector_id) then | |
| EnterSector(sector_id, spawn_mode, spawn_markers, save_sector, force_test_map, game_start) | |
| end | |
| end | |
| function EnterFirstSector(sector_id, force) | |
| local campaignPreset = GetCurrentCampaignPreset() | |
| local init_sector = campaignPreset.InitialSector | |
| local thisIsFirstSector = force | |
| if not thisIsFirstSector and sector_id == init_sector then | |
| thisIsFirstSector = not GameState.entered_sector | |
| -- We are in the initial sector but we've been in a sector before. | |
| -- Check if this is the initial_sector conflict, in which case it | |
| -- means we've game overed here. | |
| if not thisIsFirstSector then | |
| local conflictHere = gv_Sectors[sector_id] | |
| conflictHere = conflictHere and conflictHere.conflict | |
| thisIsFirstSector = conflictHere and conflictHere.initial_sector | |
| end | |
| end | |
| if thisIsFirstSector then | |
| local conflict = gv_Sectors[sector_id] | |
| conflict = conflict and conflict.conflict | |
| if conflict then | |
| conflict.initial_sector = true | |
| end | |
| local spawn_markers = {} | |
| local markers = {"Defender", "GameIntro"} | |
| for i, s in ipairs(GetPlayerMercSquads()) do | |
| if s.CurrentSector == sector_id then | |
| spawn_markers[s.UniqueId] = markers | |
| end | |
| end | |
| EnterSector(sector_id, nil, spawn_markers, true, nil, "game_start") | |
| return true | |
| end | |
| end | |
| function OnMsg.StartSatelliteGameplay() | |
| -- Open the satellite conflict screen when the satellite is opened for the first time. | |
| if not GameState.entered_sector then | |
| local initialSector = GetCurrentCampaignPreset().InitialSector | |
| if IsConflictMode(initialSector) then | |
| OpenSatelliteConflictDlg(gv_Sectors[initialSector]) | |
| end | |
| end | |
| end | |
| function OnMsg.ChangeMap() | |
| ChangeGameState{entered_sector = false} | |
| end | |
| function OnMsg.NewGameSessionStart() | |
| ChangeGameState{entered_sector = false} | |
| end | |
| function OnMsg.DoneGame() | |
| ChangeGameState{entered_sector = false, entering_sector = false} | |
| DeleteThread(enterSectorThread) | |
| DeleteThread(doneEnterSector) | |
| DeleteThread(LoadSectorThread) | |
| end | |
| function IsSectorUnderground(id) | |
| return not not (gv_Sectors[id] and gv_Sectors[id].GroundSector) | |
| end | |
| DefineConstInt("Satellite", "CoOpCountdownSeconds", 3, false, "The length of the countdown to enter satellite view in co-op.") | |
| local function lCloseSatelliteCountdowns() | |
| for d, _ in pairs(g_OpenMessageBoxes) do | |
| if d and d.window_state == "open" and d.context.obj == "satellite-countdown" then | |
| d:Close() | |
| end | |
| end | |
| end | |
| function NetSyncEvents.StartSatelliteCountdown(mode, mode_param) | |
| if not CanYield() then | |
| CreateRealTimeThread(NetSyncEvents.StartSatelliteCountdown, mode, mode_param) | |
| return | |
| end | |
| lCloseSatelliteCountdowns() | |
| local dialog = CreateMessageBox(terminal.desktop, "", "", T(739643427177, "Cancel"), "satellite-countdown") | |
| local reason = "satellite-countdown" | |
| Pause(reason) | |
| PauseCampaignTime(reason) | |
| dialog.OnDelete = function() | |
| Resume(reason) | |
| ResumeCampaignTime(reason) | |
| end | |
| local countdown_seconds = const.Satellite.CoOpCountdownSeconds | |
| dialog:CreateThread("countdown", function() | |
| local idText = dialog.idMain.idText | |
| local currentCountdown = countdown_seconds | |
| for i = 1, countdown_seconds do | |
| if idText.window_state == "open" then | |
| if mode == "open" then | |
| idText:SetText(T{766863202626, "<center>Entering Sat View in <u(currentCountdown)>", currentCountdown = currentCountdown}) | |
| elseif GameState.entered_sector then | |
| idText:SetText(T{628266992810, "<center>Closing Sat View in <u(currentCountdown)>", currentCountdown = currentCountdown}) | |
| else | |
| idText:SetText(T{552141784654, "<center>Starting game in <u(currentCountdown)>", currentCountdown = currentCountdown}) | |
| end | |
| else | |
| break | |
| end | |
| Sleep(1000) | |
| currentCountdown = currentCountdown - 1 | |
| ObjModified(currentCountdown) | |
| if currentCountdown <= 1 then | |
| CloseBlockingDialogs() | |
| end | |
| end | |
| dialog:Close() | |
| if mode == "open" then | |
| OpenSatelliteView() | |
| elseif mode == "close" and mode_param then | |
| UIEnterSectorInternal(table.unpack(mode_param)) | |
| elseif mode == "close" then | |
| CloseDialog("PDADialogSatellite") | |
| else | |
| local pda = GetDialog("PDADialogSatellite") | |
| if pda then pda:SetMode(mode, mode_param, "skip_can_close") end | |
| end | |
| end) | |
| local res = dialog:Wait() | |
| if res == "ok" then | |
| NetSyncEvent("CancelSatelliteCountdown", mode, netUniqueId) | |
| end | |
| end | |
| function NetSyncEvents.CancelSatelliteCountdown(mode, player_id) | |
| lCloseSatelliteCountdowns() | |
| if mode == "open" then | |
| if player_id == netUniqueId then | |
| CombatLog("debug", T(Untranslated("Cancelled the transition to Sat View."))) | |
| else | |
| CombatLog("debug", T(Untranslated("<OtherPlayerName()> cancelled the transition to Sat View."))) | |
| end | |
| else | |
| if player_id == netUniqueId then | |
| CombatLog("debug", T(Untranslated("Cancelled the transition from Sat View."))) | |
| else | |
| CombatLog("debug", T(Untranslated("<OtherPlayerName()> cancelled the transition from Sat View."))) | |
| end | |
| end | |
| end | |
| function GetCitySectors(city) | |
| if not g_CitySectors then | |
| g_CitySectors = {} | |
| for id, sector in pairs(gv_Sectors) do | |
| local c = sector.City | |
| if c and c ~= "none" then | |
| g_CitySectors[c] = g_CitySectors[c] or {} | |
| table.insert(g_CitySectors[c], id) | |
| end | |
| end | |
| end | |
| return g_CitySectors[city] | |
| end | |
| if false then | |
| function GameTests.SatelliteView() | |
| CreateRealTimeThread(function() | |
| QuickStartCampaign() | |
| gv_Sectors[gv_CurrentSectorId].ForceConflict = true | |
| ResolveConflict() | |
| Game.CampaignTimeFactor = 20 | |
| Game.Campaign = DefaultCampaign | |
| OpenSatelliteView() | |
| end) | |
| WaitMsg("InitSatelliteView") | |
| CreateNewSatelliteSquad({Side = "player1", CurrentSector = "H11", Name = SquadName:GetNewSquadName("player1")}, { "Caesar", "Cass", "Cliff" }, 7) | |
| for _, s in ipairs(g_SquadsArray) do | |
| local route = GenerateRouteDijkstra(s.CurrentSector, "H10", s.route, s.units, nil, nil, squad.Side) | |
| if route then | |
| SetSatelliteSquadRoute(s, { route }) | |
| end | |
| end | |
| Sleep(const.Scale.h/4) | |
| TestSaveLoadGame() | |
| for _, s in ipairs(g_SquadsArray) do | |
| local route = GenerateRouteDijkstra(s.CurrentSector, "J15", s.route, s.units, nil, nil, squad.Side) | |
| if route then | |
| SetSatelliteSquadRoute(s, { route }) | |
| end | |
| end | |
| Sleep(const.Scale.h/4) | |
| CloseSatelliteView(true) | |
| CloseDialog("InGameInterface") | |
| DoneGame() | |
| end | |
| end | |
| function OnMsg.NewDay() | |
| CreateMapRealTimeThread(function() | |
| RequestAutosave{ autosave_id = "newDay", save_state = "NewDay", display_name = T{829706997018, "Day_<day>_<month()>", day = TFormat.day()}, mode = "delayed" } | |
| end) | |
| local mercs = CountPlayerMercsInSquads(false, "include_imp") | |
| CombatLog("important", T{102190343056, "The date is <day()> <month()> - You have <mercs_amount> Merc(s) and <money(money_value)>",mercs_amount = mercs, money_value = Game.Money}) | |
| ObjModified("day_display") | |
| end | |
| function OnMsg.LoadSessionData() | |
| if gv_SatelliteView and not IsCampaignPaused() then | |
| PauseCampaignTime("UI") | |
| end | |
| end | |
| ---- | |
| function SectorPanelShowAlliedSection(sector) | |
| if not IsSectorRevealed(sector) then return false end | |
| local al, en = GetSquadsInSector(sector.Id, false, false) | |
| local anyAllies = next(al) | |
| local militiaSector = sector.Militia and (sector.Side=="player1" or sector.Side=="player2" or sector.Side=="ally") | |
| return anyAllies or militiaSector | |
| end | |
| function SectorPanelShowEnemySection(sector) | |
| if not IsSectorRevealed(sector) then return false end | |
| local al, en = GetSquadsInSector(sector.Id, false, true) | |
| return next(en) | |
| end | |
| function SavegameSessionDataFixups.FixMinesTaggedAsDepleted(sector_data, lua_revision) | |
| local gvars = sector_data.gvars | |
| local sectors = gvars and gvars.gv_Sectors | |
| for id, sector in pairs(sectors) do | |
| if sector.Mine and sector.mine_depleted and sector.Depletion then | |
| local sectorDepletionTime = GetSectorDepletionTime(sector) | |
| if sector.mine_work_days < sectorDepletionTime + const.Satellite.MineDepletingDays then | |
| sector.mine_depleted = false | |
| end | |
| end | |
| end | |
| end | |
| function OnMsg.BobbyRayShopShipmentArrived(shipment) | |
| CombatLog("important", T{389474969879, "<em>Bobby Ray's</em> shipment arrived in the sector stash of <em><SectorName(sector_id)></em>, accessible through the Sat View.", order_id = Untranslated(shipment.order_id), sector_id = shipment.sector_id}) | |
| end |