Download Lua/GameSession.lua from sirnii/myspace: direct link, hf CLI and curl.
- Browser
- Download file 53 kB
-
https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/GameSession.lua
- Command line
-
hf download hf://spaces/sirnii/myspace/Lua/GameSession.lua
-
curl -L -o GameSession.lua https://huggingface.co/spaces/sirnii/myspace/resolve/main/Lua/GameSession.lua
53 kB
| DefineClass.CampaignObject = { | |
| __parents = { "PropertyObject" }, | |
| } | |
| function CampaignObject:SetId(id) | |
| self.Id = id | |
| local template = self:GetCampaignTemplate() | |
| if template then | |
| setmetatable(self, { __index = template, __toluacode = template.__toluacode }) | |
| end | |
| end | |
| function CampaignObject:GetCampaignTemplate() | |
| if self.template_key and Game and Game.Campaign and CampaignPresets[Game.Campaign] then | |
| local campaign = CampaignPresets[Game.Campaign] | |
| local subobjects = campaign[self.template_key] | |
| return table.find_value(subobjects, "Id", self.Id) | |
| end | |
| end | |
| function CampaignObject:GetDefaultPropertyValue(prop, prop_meta) | |
| prop_meta = prop_meta or self:GetPropertyMetadata(prop) | |
| if prop_meta and prop ~= "Id" then | |
| local template = self:GetCampaignTemplate() | |
| if template then | |
| return template:GetProperty(prop) | |
| end | |
| end | |
| return PropertyObject.GetDefaultPropertyValue(self, prop, prop_meta) | |
| end | |
| function CampaignObject:LoadFirstPriorityProps(obj, prop_data) | |
| -- load id and template_key first, so other props can find their template | |
| local idx = table.find(prop_data, "Id") | |
| if idx then | |
| obj:SetId(prop_data[idx + 1]) | |
| end | |
| idx = table.find(prop_data, "template_key") | |
| if idx then | |
| obj.template_key = prop_data[idx + 1] | |
| end | |
| end | |
| function CampaignObject:__fromluacode(prop_data, arr) | |
| local obj = self:new(arr) | |
| self:LoadFirstPriorityProps(obj, prop_data) | |
| SetObjPropertyList(obj, prop_data) | |
| return obj | |
| end | |
| GameVar("gv_NextSquadUniqueId", 1) | |
| GameVar("gv_CurrentSectorId", false) | |
| GameVar("gv_ActiveCombat", false) | |
| GameVar("gv_SatelliteView", false) | |
| GameVar("gv_Sectors", {}) | |
| GameVar("gv_ConflictSectorIds", {}) | |
| GameVar("gv_Cities", {}) | |
| GameVar("gv_Squads", {}) | |
| GameVar("gv_UnitData", {}) | |
| GameVar("gv_Quests", {}) | |
| GameVar("gv_OldQuestStatus", {}) | |
| GameVar("gv_NextUnitDataUniqueId", 1) | |
| GameVar("gv_SaveCamera", false) | |
| GameVar("gv_DisabledPopups", {}) | |
| GameVar("gv_Cheats", {}) | |
| GameVar("gv_LastSectorTakenByPlayer", false) | |
| GameVar("gv_AITargetModifiers", {}) | |
| PersistableGlobals.NetTimeFactor = false | |
| local persistable_pause_reasons = {["UI"] = true, ["SatelliteConflict"] = true, ["EndGame"] = true} | |
| if FirstLoad then | |
| CampaignPauseReasons = {} | |
| PDAOpenedByPlayer = {} | |
| g_LatestSave = false | |
| g_PersistentUnitData = false | |
| end | |
| function OnMsg.SaveGameStart(game) | |
| LastPlaytime = GetCurrentPlaytime() | |
| PlaytimeCheckpoint = GetPreciseTicks() | |
| Game.playthrough_time = LastPlaytime -- store LastPlaytime in savegame | |
| end | |
| function OnMsg.ZuluGameLoaded(game) | |
| LastPlaytime = Game.playthrough_time -- restore LastPlaytime from savegame | |
| g_PersistentUnitData = {} | |
| for i, ud in sorted_pairs(gv_UnitData) do | |
| if ud.PersistentSessionId then | |
| local unitList = g_PersistentUnitData[ud.PersistentSessionId] or {} | |
| unitList[#unitList + 1] = ud | |
| g_PersistentUnitData[ud.PersistentSessionId] = unitList | |
| end | |
| end | |
| end | |
| function OnMsg.NewGame() | |
| g_PersistentUnitData = {} | |
| end | |
| function OnMsg.UnitDataCreated(ud) | |
| if not ud.PersistentSessionId then return end | |
| if not g_PersistentUnitData then g_PersistentUnitData = {} end | |
| local unitList = g_PersistentUnitData[ud.PersistentSessionId] or {} | |
| unitList[#unitList + 1] = ud | |
| g_PersistentUnitData[ud.PersistentSessionId] = unitList | |
| end | |
| function OnMsg.NewGame(game) | |
| local diffMoneyChangePerc = PercentModifyByDifficulty(GameDifficulties[Game.game_difficulty]:ResolveValue("startMoneyBonus")) | |
| game.Money = game.Money or (MulDivRound(const.Satellite.StartingMoney, diffMoneyChangePerc, 100)) | |
| game.Components = game.Components or 0 | |
| game.DailyIncome = game.DailyIncome or 0 | |
| game.CampaignTime = game.CampaignTime or const.StoryBits.StartDate | |
| SetUILCustomTime(game.CampaignTime) | |
| game.CampaignTimeStart = game.CampaignTime | |
| game.CampaignTimeFactor = game.CampaignTimeFactor or const.Satellite.CampaignTimeNormalSpeed | |
| game.PersistableCampaignPauseReasons = {} | |
| CampaignPauseReasons = {} | |
| end | |
| function OnMsg.PreLoadSessionData() | |
| CampaignPauseReasons = table.copy(Game.PersistableCampaignPauseReasons) | |
| for k, v in pairs(CampaignPauseReasons) do -- patch so that old saves can work, will remove it soon | |
| if not persistable_pause_reasons[k] then | |
| CampaignPauseReasons[k] = nil | |
| end | |
| end | |
| ObjModified(CampaignPauseReasons) | |
| end | |
| -- campaign speed | |
| function PauseCampaignTime(reason) | |
| if Game then | |
| SetCampaignSpeed(0, reason) | |
| end | |
| end | |
| function ResumeCampaignTime(reason) | |
| if Game then | |
| SetCampaignSpeed(Game.CampaignTimeFactor, reason) | |
| end | |
| end | |
| function SetCampaignSpeed(speed, reason) | |
| NetSyncEvent("SetCampaignSpeed", speed, reason) | |
| end | |
| function _SetCampaignSpeed(speed, reason) | |
| if not Game then return end --while host boots up first Game, client has no Game | |
| -- set speed in session data | |
| NetUpdateHash("_SetCampaignSpeed", Game.CampaignTime, speed, reason) | |
| local old_is_paused = IsCampaignPaused() | |
| local old_factor = Game and Game.CampaignTimeFactor | |
| if reason then | |
| if speed == 0 then | |
| CampaignPauseReasons[reason] = true | |
| if persistable_pause_reasons[reason] then | |
| Game.PersistableCampaignPauseReasons[reason] = true | |
| end | |
| else | |
| CampaignPauseReasons[reason] = nil | |
| if persistable_pause_reasons[reason] then | |
| Game.PersistableCampaignPauseReasons[reason] = nil | |
| end | |
| end | |
| end | |
| if speed and speed > 0 then | |
| Game.CampaignTimeFactor = speed | |
| end | |
| ObjModified(Game) | |
| if IsCampaignPaused() ~= old_is_paused or (Game and Game.CampaignTimeFactor ~= old_factor) then | |
| Msg("CampaignSpeedChanged") | |
| end | |
| ObjModified(CampaignPauseReasons) | |
| end | |
| function IsCampaignPaused() | |
| return not not next(CampaignPauseReasons) | |
| end | |
| local host_suffix, guest_suffix = "_mp_coop_host", "_mp_coop_guest" | |
| function GetUICampaignPauseReason(reason) | |
| local is_host = not netInGame or NetIsHost() | |
| return string.format("%s%s", reason, is_host and host_suffix or guest_suffix) | |
| end | |
| function GetPauseUIReasonExists(reason) | |
| local guestExist = string.format("%s%s", reason, guest_suffix) | |
| local hostExist = string.format("%s%s", reason, host_suffix) | |
| guestExist = CampaignPauseReasons[guestExist] | |
| hostExist = CampaignPauseReasons[hostExist] | |
| return guestExist or hostExist, guestExist, hostExist | |
| end | |
| function OnMsg.NetGameLeft() | |
| PDAOpenedByPlayer[2] = nil | |
| end | |
| function OnMsg.NetPlayerLeft(player, reason) | |
| PDAOpenedByPlayer[player.id] = nil | |
| end | |
| function OnMsg.ChangeMap() | |
| PDAOpenedByPlayer = {} | |
| end | |
| function NetSyncEvents.SetPDAOpened(playerId, state) | |
| local inMpGame = netInGame and table.count(netGamePlayers) > 1 | |
| local isHost = not inMpGame or NetIsHost(playerId) | |
| PDAOpenedByPlayer[playerId] = state | |
| local pausedByPDAOpen = PDAOpenedByPlayer[1] and PDAOpenedByPlayer[2] | |
| if not inMpGame then | |
| pausedByPDAOpen = PDAOpenedByPlayer[netUniqueId] | |
| end | |
| _SetCampaignSpeed(pausedByPDAOpen and 0 or false, "PDAOpened") | |
| end | |
| function IsCampaignPausedByOtherPlayer() | |
| if netInGame then | |
| local suffix = NetIsHost() and guest_suffix or host_suffix | |
| for k, v in pairs(CampaignPauseReasons) do | |
| if v and string.match(k, string.format("%s$", suffix)) then | |
| return true | |
| end | |
| end | |
| end | |
| return false | |
| end | |
| function ClearRemotePauseReasons(reasons) | |
| local cleared = {} | |
| local suffix = NetIsHost() and guest_suffix or host_suffix | |
| for k, v in pairs(reasons) do | |
| if v then | |
| if type(k) == "string" and string.match(k, string.format("%s$", suffix)) then | |
| cleared[#cleared + 1] = k | |
| reasons[k] = nil | |
| end | |
| end | |
| end | |
| return cleared | |
| end | |
| function GetAllSuffixReasons(reasons) | |
| local res = {} | |
| for k, v in pairs(reasons) do | |
| if v and type(k) == "string" and (k:ends_with(guest_suffix) or k:ends_with(host_suffix)) then | |
| res[#res + 1] = k | |
| end | |
| end | |
| return res | |
| end | |
| function GetRemoteSuffixReasons(reasons) | |
| local res = {} | |
| for k, v in pairs(reasons) do | |
| if v and type(k) == "string" and k:ends_with(guest_suffix) then | |
| res[#res + 1] = k | |
| end | |
| end | |
| return res | |
| end | |
| function OnMsg.NetPlayerLeft(player, reason) | |
| --clear other player pause layer reasons | |
| ClearRemotePauseReasons(CampaignPauseReasons) | |
| local cleared = GetRemoteSuffixReasons(PauseReasons) | |
| for i, r in ipairs(cleared) do | |
| _SetPauseLayerPause(false, r) | |
| end | |
| end | |
| function ClearAllSuffixReasons(reasons) | |
| local suffixReasons = GetAllSuffixReasons(reasons) | |
| for i, sR in ipairs(suffixReasons) do | |
| reasons[sR] = nil | |
| end | |
| end | |
| function OnMsg.ChangeMapDone() | |
| ClearAllSuffixReasons(CampaignPauseReasons) | |
| ClearAllSuffixReasons(PauseReasons) | |
| end | |
| function OnMsg.NetGameLeft() | |
| ClearAllSuffixReasons(CampaignPauseReasons) | |
| local cleared = GetAllSuffixReasons(PauseReasons) | |
| for i, r in ipairs(cleared) do | |
| _SetPauseLayerPause(false, r) | |
| end | |
| end | |
| function IsCampaignPausedByRemotePlayerOnly() | |
| return DoPauseReasonsContainRemotePlayerOnlyPause(CampaignPauseReasons) | |
| end | |
| function IsGamePausedByRemotePlayerOnly() | |
| return DoPauseReasonsContainRemotePlayerOnlyPause(PauseReasons) | |
| end | |
| function IsCampaignOrGamePausedByRemotePlayerOnly() | |
| local pausedByRemote, reason = IsCampaignPausedByRemotePlayerOnly() | |
| if pausedByRemote then return pausedByRemote, reason end | |
| return IsGamePausedByRemotePlayerOnly() | |
| end | |
| function DoPauseReasonsContainRemotePlayerOnlyPause(reasons) | |
| --checks that game is paused by ui on exactly on remote machine only, if it is also paused locally it returns false | |
| if netInGame then | |
| local his_suffix = NetIsHost() and guest_suffix or host_suffix | |
| local my_suffix = NetIsHost() and host_suffix or guest_suffix | |
| local has_mine = false | |
| local has_his, his_reason = false | |
| for k, v in pairs(reasons) do | |
| if v and type(k) == "string" then | |
| if not has_his and string.match(k, string.format("%s$", his_suffix)) then | |
| has_his = true | |
| his_reason = k | |
| elseif not has_mine and string.match(k, string.format("%s$", my_suffix)) then | |
| has_mine = true | |
| end | |
| end | |
| end | |
| return has_his and not has_mine, his_reason | |
| end | |
| return false | |
| end | |
| function IsCampaignPausedByRemoteBobbyRayOnly() | |
| if not netInGame then return false end | |
| local his_suffix = NetIsHost() and guest_suffix or host_suffix | |
| local my_suffix = NetIsHost() and host_suffix or guest_suffix | |
| for k, v in pairs(CampaignPauseReasons) do | |
| if v and type(k) == "string" then | |
| if not string.match(k, "PDABrowserBobbyRay") and string.match(k, his_suffix) then | |
| return false | |
| end | |
| end | |
| end | |
| return true | |
| end | |
| function NetSyncEvents.SetCampaignSpeed(speed, reason) | |
| _SetCampaignSpeed(speed, reason) | |
| end | |
| function NetEvents.SetLayerPause(pause, reason, keep_sounds) | |
| _SetPauseLayerPause(pause, reason, keep_sounds) | |
| end | |
| function OnMsg.ClassesGenerate(classes) | |
| table.insert(classes.XPauseLayer.properties, { id = "PauseReason", editor = "text", default = false }) | |
| end | |
| function SetPauseLayerPause(pause, layer, keep_sounds) | |
| -- Try to determine a unique id for this layer. | |
| local reason = false | |
| if layer then | |
| if type(layer) == "string" then | |
| reason = layer | |
| elseif #(layer.PauseReason or "") > 0 then | |
| reason = layer.PauseReason | |
| elseif type(layer) == "table" then | |
| reason = layer.class | |
| end | |
| end | |
| reason = reason or "UnknownPause" | |
| reason = GetUICampaignPauseReason(reason) -- Convert to co-op aware | |
| if GetMapName() ~= "" and not IsChangingMap() then | |
| NetEchoEvent("SetLayerPause", pause, reason, keep_sounds) | |
| else | |
| _SetPauseLayerPause(pause, reason, keep_sounds) | |
| end | |
| end | |
| function _SetPauseLayerPause(pause, layer, keep_sounds) | |
| if pause then | |
| Pause(layer, keep_sounds) | |
| else | |
| Resume(layer) | |
| end | |
| local dlg = GetDialog("MPPauseHint") | |
| if dlg then | |
| dlg:OnContextUpdate(dlg.context) | |
| end | |
| end | |
| function OnMsg.SaveGameStart(save) | |
| if save and not save.autosave then | |
| SetPauseLayerPause(true, "SavingGame") | |
| end | |
| end | |
| function OnMsg.SaveGameDone(save) | |
| SetPauseLayerPause(false, "SavingGame") | |
| end | |
| function IsCampaignMap(map_name) | |
| for _, preset in pairs(CampaignPresets) do | |
| for _, sector in pairs(preset.Sectors or empty_table) do | |
| if sector.Map == map_name then | |
| return true | |
| end | |
| end | |
| end | |
| end | |
| local function SingleplayerPauseCampaignTime(pause, reason) | |
| if not Game then return end | |
| -- In multiplayer allow sp unpause only (and it needs to go through an event) | |
| if IsInMultiplayerGame() then | |
| if not pause then | |
| SetCampaignSpeed(pause and 0 or Game.CampaignTimeFactor, reason) | |
| end | |
| return | |
| end | |
| _SetCampaignSpeed(pause and 0 or (Game and Game.CampaignTimeFactor), reason) | |
| end | |
| function OnMsg.BugReportStart() | |
| PauseCampaignTime(GetUICampaignPauseReason("InGameMenu")) | |
| end | |
| function OnMsg.BugReportEnd() | |
| ResumeCampaignTime(GetUICampaignPauseReason("InGameMenu")) | |
| end | |
| function CloseBugReporter() | |
| local template_id = config.BugReporterXTemplateID | |
| local dlg = GetDialog(template_id) | |
| if not dlg then return end | |
| dlg:Close() | |
| end | |
| OnMsg.OpenSatelliteView = CloseBugReporter | |
| OnMsg.CloseSatelliteView = CloseBugReporter | |
| function OnMsg.InGameMenuOpen() | |
| SingleplayerPauseCampaignTime(true, "InGameMenu") | |
| end | |
| function OnMsg.InGameMenuClose() | |
| SingleplayerPauseCampaignTime(false, "InGameMenu") | |
| end | |
| function OnMsg.EnterSector() | |
| OpenDialog("MPPauseHint", GetInGameInterface()) | |
| end | |
| -- session data | |
| function GatherSessionData() | |
| --FunctionProfilerStart() | |
| local time_start = GetPreciseTicks() | |
| PauseInfiniteLoopDetection("GatherSessionData") | |
| Msg("GatherSessionData") | |
| if Platform.developer then | |
| --printf("Msg GatherSessionData in %dms", GetPreciseTicks() - time_start) | |
| end | |
| local time_to_lua_code = GetPreciseTicks() | |
| g_PresetForbidSerialize = true | |
| local save_data = pstr("return "):append(TupleToLuaCodePStr({ | |
| game = Game, | |
| gvars = GetPersistableGameVarValues(), | |
| })) | |
| g_PresetForbidSerialize = false | |
| if Platform.developer then | |
| local time = GetPreciseTicks() - time_to_lua_code | |
| --printf("TupleToLuaCodePStr in %dms", time) | |
| end | |
| Msg("GatherSessionDataEnd") | |
| ResumeInfiniteLoopDetection("GatherSessionData") | |
| --FunctionProfilerStop() | |
| if Platform.developer then | |
| --printf("GatherSesionData in %dms", GetPreciseTicks() - time_start) | |
| end | |
| return save_data | |
| end | |
| function GetGameVarFromSession(session_data, name) | |
| return session_data.gvars[name] | |
| end | |
| function GetSectorDataFromSession(session_data, sector_id) | |
| return session_data.gvars.gv_Sectors[sector_id].sector_data | |
| end | |
| local function GameDataToLuaValue(data, env) | |
| local func, err = load(data, nil, nil, env) | |
| if not func then return err end | |
| local ok, data = procall(func) | |
| if not ok then | |
| return data or "runtime err" | |
| end | |
| return nil, data | |
| end | |
| function GenerateMultiplayerGuestCampaignName() | |
| assert(netGamePlayers and table.count(netGamePlayers) > 1) | |
| local campaign = Game and Game.Campaign or rawget(_G, "DefaultCampaign") or "HotDiamonds" | |
| assert(CampaignPresets[campaign]) | |
| return _InternalTranslate(CampaignPresets[campaign].DisplayName) | |
| end | |
| function LoadGameSessionData(data, metadata) | |
| local wasInMultiplayerGame = IsInMultiplayerGame() | |
| collectgarbage("stop") | |
| SkipAnySetpieces() | |
| ChangeGameState( { loading_savegame = true } ) | |
| if GetMap() == "" then | |
| ResetGameTime() -- NetUpdateHash can be called before loading a new map | |
| else | |
| ChangeMap("") | |
| end | |
| ClearItemIdData() | |
| ZuluNewGame() | |
| local env = LuaValueEnv{ -- place functions that can be used in the save here | |
| PlaceInventoryItem = PlaceInventoryItem, | |
| PlaceUnitData = PlaceUnitData, | |
| PlaceStatusEffect = PlaceCharacterEffect, | |
| PlaceCharacterEffect = PlaceCharacterEffect, | |
| InvalidPos = InvalidPos, | |
| GetMissingSourceFallback = function() | |
| print("Loading SaveGame Environment: Func source missing!") | |
| return function() | |
| print("Loading SaveGame Environment: Missing func!") | |
| end | |
| end, | |
| } | |
| local err, data = GameDataToLuaValue(data, env) | |
| assert(not err, err) | |
| if not err then | |
| FixupSessionData(metadata, data) | |
| for _, name in ipairs(GameVars) do | |
| if data.gvars[name] == nil then | |
| local value = GameVarValues[name] | |
| if type(value) == "function" then | |
| value = value() | |
| end | |
| _G[name] = value or false | |
| else | |
| _G[name] = data.gvars[name] | |
| end | |
| end | |
| Game = data.game | |
| if Platform.console and IsInMultiplayerGame() and not NetIsHost() then | |
| Game.playthrough_name = GenerateMultiplayerGuestCampaignName() | |
| end | |
| Game.Campaign = Game.Campaign or DefaultCampaign | |
| -- !!! backwards compatibility before rev 277827 | |
| if not IsKindOf(Game, "GameSettings") then | |
| Game = GameSettings:new(Game) | |
| end | |
| Game.loaded_from_id = Game.save_id | |
| Game.combat_outcomes = Game.combat_outcomes or {defeats = {}, retreats = {}, victories = {}} | |
| NetGossip("LoadGame", Game.id, Game.loaded_from_id, GetGameSettingsTable(Game), Game.combat_outcomes, GetCurrentPlaytime(), Game and Game.CampaignTime) | |
| -- We don't need to filter texts from an unbought merc, because these are never displayed in the UI as UserTexts (even though they exist in memory); relevant for resyncs after merc creation is started but before it is finished | |
| local custom_merc = data.gvars.g_ImpTest and data.gvars.g_ImpTest.final | |
| -- backward compatibility (old saves will have these as strings) | |
| if custom_merc and custom_merc.created then | |
| if type(custom_merc.name) == "string" then | |
| custom_merc.name = CreateUserText(custom_merc.name, "name") | |
| end | |
| if type(custom_merc.nick) == "string" then | |
| custom_merc.nick = CreateUserText(custom_merc.nick, "name") | |
| end | |
| -- this is required because some savegames have become inconsistent on steam; the g_ImpTest and gv_UnitData UserTexts with the same text but different steam ids; see mantis#219504 | |
| local merc_unit_data = data.gvars.gv_UnitData[custom_merc.merc_template.id] | |
| if merc_unit_data then | |
| merc_unit_data.Name = custom_merc.name | |
| merc_unit_data.Nick = custom_merc.nick | |
| end | |
| local user_texts = { custom_merc.name, custom_merc.nick} | |
| local loading = _InternalTranslate(LoadingUnitName) | |
| SetCustomFilteredUserTexts(user_texts, { loading, loading }) | |
| local errors = AsyncFilterUserTexts(user_texts) | |
| if errors then | |
| for _, err in ipairs(errors) do | |
| SetCustomFilteredUserText(err.user_text) | |
| end | |
| end | |
| end | |
| Msg("PreLoadSessionData", data) | |
| local sat_view = gv_SatelliteView | |
| local sectorToEnter = gv_ActiveCombat or gv_CurrentSectorId | |
| if sectorToEnter then | |
| local floatingTextConfig = config.FloatingTextEnabled | |
| config.FloatingTextEnabled = false | |
| EnterSector(sectorToEnter) | |
| config.FloatingTextEnabled = floatingTextConfig | |
| end | |
| if sat_view or not sectorToEnter then | |
| if gv_SaveCamera then | |
| SetCamera(unpack_params(gv_SaveCamera)) | |
| gv_SaveCamera = false | |
| end | |
| OpenSatelliteView(nil, (InitialConflictNotStarted() and not AnyPlayerSquads()) and "openLandingPage", "force_loading", "wait") | |
| end | |
| local abort_threshold = 5 * 40 -- 40 seconds eyeballed | |
| local count = 0 | |
| while IsChangingMap() or GetMap() == "" do | |
| WaitMsg("ChangeMapDone", 200) | |
| if GetMap() == "" and (IsInMultiplayerGame() ~= wasInMultiplayerGame or count >= abort_threshold) then | |
| ChangeGameState( { loading_savegame = false } ) | |
| collectgarbage("collect") | |
| collectgarbage("restart") | |
| return "lost connection or timed out during LoadGameSessionData" | |
| end | |
| count = count + 1 | |
| end | |
| Msg("LoadSessionData") | |
| if mapdata and metadata and mapdata.NetHash ~= metadata.mapNetHash then | |
| ValidateMapGameplay() | |
| end | |
| end | |
| local browser_history = data.gvars.PDABrowserHistoryState | |
| -- backward compatibility (old saves didn't need future DLC to open BobbyRayGun's webpage, saved in browser history) | |
| -- there is only at most one entry to remove so this should be safe | |
| if not IsDlcAvailable("future") then | |
| for i, v in ipairs(browser_history) do | |
| if v.mode == "banner_page" and v.mode_param == "PDABrowserBobbyRay" then | |
| table.remove(browser_history, i) | |
| break | |
| end | |
| end | |
| end | |
| ChangeGameState( { loading_savegame = false } ) | |
| collectgarbage("collect") | |
| collectgarbage("restart") | |
| return err | |
| end | |
| function CreateSessionCampaignObject(obj, class, session_objs, template_key) | |
| local id = obj.Id | |
| if not session_objs[id] then | |
| session_objs[id] = class:new{template_key = template_key} | |
| session_objs[id]:SetId(id) | |
| end | |
| end | |
| function DeleteSessionCampaignObject(obj, class, session_objs) | |
| local id = obj.Id | |
| if session_objs[id] then | |
| session_objs[id]:delete() | |
| session_objs[id] = nil | |
| end | |
| end | |
| function InitSessionCampaignObjects(class, session_objs, template_key) | |
| local campaign_objs = CampaignPresets[Game.Campaign][template_key] or empty_table | |
| for i = 1, #campaign_objs do | |
| CreateSessionCampaignObject(campaign_objs[i], class, session_objs, template_key) | |
| end | |
| end | |
| function PatchSessionCampaignObjects(class, session_objs, template_key) | |
| local campaign_objs = CampaignPresets[Game.Campaign][template_key] or empty_table | |
| for i = 1, #campaign_objs do | |
| local obj = campaign_objs[i] | |
| local id = obj.Id | |
| if not session_objs[id] then | |
| CreateSessionCampaignObject(obj, class, session_objs, template_key) | |
| end | |
| end | |
| end | |
| function ZuluNewGame(new_game_params, campaign) | |
| local unit_data = new_game_params and new_game_params.KeepUnitData and gv_UnitData | |
| DoneGame() | |
| local game = GameClass:new() | |
| game.save_id = random_encode64(48) | |
| game.combat_outcomes = {defeats = {}, retreats = {}, victories = {}} | |
| if not new_game_params or new_game_params.game_rules == nil then | |
| ForEachPreset("GameRuleDef", function(rule) | |
| if rule.init_as_active then | |
| game:AddGameRule(rule.id) | |
| end | |
| end) | |
| end | |
| Game = game | |
| ApplyNewGameOptions(new_game_params) | |
| InitGameVars() | |
| if campaign then | |
| Game.Campaign = campaign.id | |
| Game.CampaignTime = campaign.starting_timestamp | |
| SetUILCustomTime(Game.CampaignTime) | |
| Game.CampaignTimeStart = Game.CampaignTime | |
| end | |
| if unit_data then | |
| gv_UnitData = unit_data | |
| end | |
| if not GameState.loading_savegame then | |
| NetGossip("NewGame", game.id, game.save_id, GetGameSettingsTable(game)) | |
| local campaign = GetCurrentCampaignPreset() | |
| NetGossip("CampaignStage", campaign and campaign.id, "early", GetCurrentPlaytime(), Game.CampaignTime) | |
| end | |
| Msg("NewGame", game) | |
| CloseMenuDialogs() | |
| return game | |
| end | |
| function OnMsg.CombatEnd(combat) | |
| local team = GetCampaignPlayerTeam() | |
| local defeated = not team or team:IsDefeated() | |
| local stats_tbl = defeated and Game.combat_outcomes.defeats or Game.combat_outcomes.victories | |
| stats_tbl[gv_CurrentSectorId] = (stats_tbl[gv_CurrentSectorId] or 0) + 1 | |
| NetGossip("Combat", "Outcomes", Game.combat_outcomes, GetCurrentPlaytime(), Game and Game.CampaignTime) | |
| end | |
| function OnMsg.ConflictEnd(sector, _, __, ___, ____, isRetreat) | |
| if not isRetreat then return end | |
| local sector_id = sector.Id | |
| Game.combat_outcomes.retreats[sector_id] = (Game.combat_outcomes.retreats[sector_id] or 0) + 1 | |
| NetGossip("Combat", "Retreat", sector_id, GetCurrentPlaytime(), Game and Game.CampaignTime) | |
| NetGossip("Combat", "Outcomes", Game.combat_outcomes, GetCurrentPlaytime(), Game and Game.CampaignTime) | |
| end | |
| function NewGameSession(campaign, new_game_params) | |
| LogData = {} | |
| Msg("NewGameSessionStart") | |
| ZuluNewGame(new_game_params, campaign) | |
| InitSessionCampaignObjects(SatelliteSector, gv_Sectors, "Sectors") | |
| InitSessionCampaignObjects(CampaignCity, gv_Cities, "Cities") | |
| ForEachMerc(function(mId) | |
| CreateUnitData(mId, mId, InteractionRand()) | |
| end) | |
| Msg("InitSessionCampaignObjects") | |
| end | |
| function HasGameSession() | |
| return not not next(gv_Sectors or empty_table) | |
| end | |
| DefaultCampaign = "HotDiamonds" | |
| if FirstLoad then | |
| g_DisclaimerSplashScreen = false | |
| end | |
| function StartCampaign(campaign_id, new_game_params) | |
| local campaign = CampaignPresets[campaign_id or false] or GetCurrentCampaignPreset() | |
| if campaign.DisclaimerOnStart then | |
| local duration = ReadDurationFromText(_InternalTranslate(campaign.DisclaimerOnStart)) | |
| g_DisclaimerSplashScreen = SplashText(campaign.DisclaimerOnStart, "DisclaimerOnStart", 600, 1500, duration) | |
| g_DisclaimerSplashScreen:SetMouseCursor("UI/Cursors/Hand.tga") | |
| local res = g_DisclaimerSplashScreen:Wait() | |
| g_DisclaimerSplashScreen = false | |
| if res then | |
| return "abort" | |
| end | |
| end | |
| LoadingScreenOpen("idLoadingScreen", "new game") | |
| ClearItemIdData() | |
| NewGameSession(campaign, new_game_params) | |
| campaign:OnStartCampaign() | |
| NewGameObj = false | |
| LoadingScreenClose("idLoadingScreen", "new game") | |
| end | |
| function QuickStartCampaign(campaign_id, new_game_params, initSector) | |
| EditorDeactivate() | |
| local campaign = CampaignPresets[campaign_id or false] or GetCurrentCampaignPreset() | |
| local init_sector = initSector or campaign.InitialSector | |
| ClearItemIdData() | |
| NewGameSession(campaign, new_game_params) | |
| ForEachPreset("GameRuleDef", function(rule) | |
| if rule.init_as_active then | |
| Game:AddGameRule(rule.id) | |
| end | |
| end) | |
| SectorLoadingScreenOpen("idQuickStartCampaign", "quick start", init_sector) | |
| MapForEach("map", "Unit", DoneObject) | |
| Game.Money = const.Satellite.StartingMoneyQuickStart | |
| Game.isDev = not new_game_params or not new_game_params.testModGame | |
| local unit_ids = GetTestCampaignSquad() | |
| CreateNewSatelliteSquad({Side = "player1", CurrentSector = init_sector, Name = SquadName:GetNewSquadName("player1")}, unit_ids, 14, 1234567) | |
| for _, unitId in ipairs(unit_ids) do | |
| Msg("MercHired", unitId, 1234, 14) | |
| gv_UnitData[unitId]:CallReactions("OnMercHired", 1234, 14) | |
| SetMercStateFlag(unitId, "CurrentDailySalary", DivRound(1234, 14)) | |
| end | |
| campaign:OnStartCampaign("QuickStart") | |
| gv_InitialHiringDone = true | |
| SectorLoadingScreenClose("idQuickStartCampaign", "quick start") | |
| EnterFirstSector(init_sector, "force") | |
| end | |
| function GetTestCampaignSquad() | |
| local starting_parties = { | |
| { "Igor", "Kalyna", "Omryn", "Livewire", }, | |
| { "Barry", "MD", "Steroid", "Igor", }, | |
| { "Mouse", "Igor", "Grunty", "Fauda", }, | |
| { "Tex", "MD", "Buns", }, | |
| { "Wolf", "Steroid", "Fox", "Kalyna", }, | |
| { "Red", "Fauda", "Kalyna", "Igor", }, | |
| { "Thor", "Buns", "Steroid", "Livewire", }, | |
| } | |
| return table.rand(starting_parties) | |
| end | |
| GameVar("gv_InitialHiringDone", false) | |
| GameVar("gv_AIMBrowserEverClosed", false) | |
| function InitialConflictNotStarted() | |
| return not GameState.entered_sector and not gv_InitialHiringDone | |
| end | |
| function OnMsg.EnterSector() | |
| gv_InitialHiringDone = true | |
| end | |
| function CanLoadGame() | |
| if netInGame and not NetIsHost() | |
| or GetDialog("ModifyWeaponDlg") | |
| or (not Game and not IsMainMenuMap()) | |
| or IsGameReplayRunning() | |
| or IsGameReplayRecording() | |
| or AutosaveRequest | |
| or GetGameBlockingLoadingScreen() | |
| or g_PhotoMode | |
| or GetDialog("Intro") | |
| or GetDialog("Outro") | |
| or GetDialog("DemoOutro") | |
| then | |
| return false | |
| end | |
| return true | |
| end | |
| function OnMsg.CanSaveGameQuery(query) | |
| if not Game then | |
| query.no_game = true | |
| elseif not Game.CampaignStarted then | |
| query.no_real_campaign = true | |
| end | |
| end | |
| function HireInitialMercsWarning(callback, ...) | |
| local args = {...} | |
| CreateRealTimeThread(function() | |
| if WaitQuestion(GetInGameInterface(), T(1000599, "Warning"), T(133085287026, "You have no team. Are you sure you want to start?"), T(689884995409, "Yes"), T(782927325160, "No")) == "ok" then | |
| callback(table.unpack(args)) | |
| end | |
| end) | |
| end | |
| function OnMsg.DoneMap() | |
| CloseDialog("InGameInterface") | |
| CloseDialog("BadgeHolderDialog") | |
| CloseDialog("FloatingTextDialog") | |
| end | |
| ----------- Sector dynamic data ---------------------------- | |
| DefineClass.GameDynamicDataObject = { | |
| __parents = {"Object"}, | |
| } | |
| DefineClass.GameDynamicSpawnObject = { | |
| __parents = {"GameDynamicDataObject"}, | |
| spawner = false | |
| } | |
| local function save_val(data, name, val, default) | |
| data[name] = val ~= default and val or nil | |
| end | |
| local axis_z = axis_z | |
| local invalid_pos = InvalidPos() | |
| function GameDynamicSpawnObject:GetDynamicData(data) | |
| save_val(data, "pos", self:GetPos(), invalid_pos) | |
| save_val(data, "angle", self:GetAngle(), 0) | |
| save_val(data, "axis", self:GetAxis(), axis_z) | |
| save_val(data, "scale", self:GetScale(), 100) | |
| end | |
| local function load_val(obj, data, name, setter) | |
| local v = data[name] | |
| if v then | |
| obj[setter](obj, data[name]) | |
| end | |
| end | |
| function GameDynamicSpawnObject:SetDynamicData(data) | |
| load_val(self, data, "pos", "SetPos") | |
| load_val(self, data, "angle", "SetAngle") | |
| load_val(self, data, "axis", "SetAxis") | |
| load_val(self, data, "scale", "SetScale") | |
| end | |
| function GameDynamicSpawnObject:GetError() | |
| -- CheckSavegameObjectsAreEssentials | |
| local detail_class = self:GetDetailClass() | |
| if detail_class == "Default" then | |
| local entity = self:GetEntity() | |
| local entity_data = EntityData[self:GetEntity()] | |
| detail_class = entity and entity_data and entity_data.entity.DetailClass or "Essential" | |
| end | |
| if detail_class ~= "Essential" then | |
| return string.format("'%s' goes to savegame but is '%s'(must be Essential)!", self.class, detail_class) | |
| end | |
| end | |
| function ApplyDynamicData() | |
| local sector_data_code = gv_Sectors[gv_CurrentSectorId] and gv_Sectors[gv_CurrentSectorId].sector_data | |
| local err, sector_data = LuaCodeToTuple(sector_data_code) | |
| if err then | |
| assert(false, string.format("ApplydynamicData error:%s",err)) | |
| return | |
| end | |
| if next(sector_data) == nil then | |
| return | |
| end | |
| SuspendPassEdits("ApplyDynamicData") | |
| local dataToHandle = {} | |
| for i, data in ipairs(sector_data.dynamic_data) do | |
| local handle = data.handle | |
| dataToHandle[handle] = data | |
| end | |
| FixupSectorData(sector_data, dataToHandle) | |
| local spawn_data = sector_data.spawn | |
| -- remove(dont spawn) units that were destroyed | |
| -- todo: is this check actually correct? do we need to store which units were deleted or can we depend on this? | |
| local deleted = false | |
| local deleted_corpse_marker = {} | |
| local length = #(spawn_data or "") | |
| for i = 1, length, 2 do | |
| local class = spawn_data[i] | |
| if class == "Unit" then | |
| local handle = spawn_data[i + 1] | |
| local data = dataToHandle[handle] | |
| local sessionId = data.session_id | |
| local ud = gv_UnitData[sessionId] | |
| if not ud then | |
| spawn_data[i] = nil | |
| spawn_data[i + 1] = nil | |
| deleted = true | |
| end | |
| elseif IsKindOf(g_Classes[class], "AL_CorpseMarker") then | |
| local handle = spawn_data[i + 1] | |
| local data = dataToHandle[handle] | |
| if not data.corpse then | |
| spawn_data[i] = nil | |
| spawn_data[i + 1] = nil | |
| deleted = true | |
| deleted_corpse_marker[handle] = true | |
| end | |
| end | |
| end | |
| if deleted then | |
| table.compact(spawn_data) | |
| end | |
| -- spawn objects | |
| for i = 1, #(spawn_data or ""), 2 do | |
| local class = spawn_data[i] | |
| local handle = spawn_data[i + 1] | |
| assert(not HandleToObject[handle], string.format("'%s' with invalid saved handle[%d]", class, handle)) | |
| PlaceObject(class, {handle = handle or nil}) | |
| end | |
| -- load dynamic data | |
| -- NOTE: First load the AL markers so the units can properly restore their Visit command using the visitables | |
| -- AL_Mourn_FromCorspe/AL_Maraud_FromCorspe are spawned dynamicly and get handles greater than Units | |
| -- Hence AL markers need to be loaded before Units(editor AL markers are always with smaller handles than Units) | |
| local loaded = {} | |
| for _, data in ipairs(sector_data.dynamic_data) do | |
| local handle = data.handle | |
| local obj = HandleToObject[handle] | |
| if IsValid(obj) then | |
| if IsKindOf(obj, "AmbientLifeMarker") then | |
| loaded[handle] = true | |
| procall(obj.SetDynamicData, obj, data) | |
| if IsKindOfClasses(obj, "AL_Mourn", "AL_Maraud") and obj.corpse then | |
| -- SUB-NOTE: corpses should be loaded before their "visitors" | |
| -- since :CanVisit() needs the corpse side to re-check visit conditions | |
| local corpse_handle = obj.corpse.handle | |
| loaded[corpse_handle] = true | |
| procall(obj.corpse.SetDynamicData, obj.corpse, dataToHandle[corpse_handle]) | |
| end | |
| elseif IsKindOf(obj, "Unit") and data.behavior == "Visit" then | |
| -- NOTE: bug from r335315 was setting :SetBehavior(visitable) instead of :SetBehavior({visitable}) | |
| local marker_handle = type(data.behavior_params[1]) == "table" and data.behavior_params[1][1] or data.behavior_params[1] | |
| if marker_handle and deleted_corpse_marker[marker_handle] then | |
| data.behavior = false | |
| data.behavior_params = false | |
| end | |
| end | |
| end | |
| end | |
| RebuildVisitables() | |
| -- now other objects can be loaded | |
| local load_last = {} | |
| for _, data in ipairs(sector_data.dynamic_data) do | |
| local handle = data.handle | |
| if not loaded[handle] then | |
| local obj = HandleToObject[handle] | |
| if IsValid(obj) then | |
| if IsKindOf(obj, "Interactable") then | |
| -- NOTE: Interactable needs it objects in collections properly init to apply its effects | |
| table.insert(load_last, data) | |
| else | |
| loaded[handle] = true | |
| procall(obj.SetDynamicData, obj, data) | |
| end | |
| end | |
| end | |
| end | |
| -- Interactable now can apply its effects when all its elements are loaded, e.g. SetObjectMarking() | |
| for _, data in ipairs(load_last) do | |
| local handle = data.handle | |
| local obj = HandleToObject[handle] | |
| loaded[handle] = true | |
| procall(obj.SetDynamicData, obj, data) | |
| end | |
| -- load other 'handles' like 'combat' | |
| Msg("LoadDynamicData", sector_data) | |
| ResumePassEdits("ApplyDynamicData") | |
| return sector_data.lua_revision_on_save | |
| end | |
| -- Some dynamic data like object's vangle, vpos and such depend on visual properties which | |
| -- prevent two saves back to back to be the same. Additionally MapForEach is not | |
| -- deterministic across loading a save. | |
| if FirstLoad then | |
| g_TestingSaveLoadSystem = false | |
| end | |
| local gofPermanent = const.gofPermanent | |
| function GatherSectorDynamicData() | |
| if not GameState.entered_sector or not gv_CurrentSectorId or IsChangingMap() then | |
| return | |
| end | |
| local time = GetPreciseTicks() | |
| -- enum all spawned objects (how to tell them? use a base class for now) and create the sector_data.spawn section | |
| local spawn_data = {} | |
| local dynamic_data = {} | |
| local data = {} | |
| local function persist_non_permanent(obj) | |
| if obj:GetGameFlags(gofPermanent) ~= 0 then return end | |
| local handle = obj:GetHandle() | |
| spawn_data[#spawn_data + 1] = obj.class | |
| spawn_data[#spawn_data + 1] = handle | |
| procall(obj.GetDynamicData, obj, data) | |
| if next(data) ~= nil then | |
| data.handle = handle | |
| dynamic_data[#dynamic_data + 1] = data | |
| data = {} | |
| end | |
| end | |
| MapForEach("map", "GameDynamicSpawnObject", persist_non_permanent) | |
| MapForEach("detached", "Unit", persist_non_permanent) | |
| -- enum all Objects with gofPermanent and create the sector_data.dynamic_data section by calling GetDynamicData on each one | |
| PauseInfiniteLoopDetection("Saving") | |
| MapForEach("map", "GameDynamicDataObject", nil, nil, gofPermanent, function(obj) | |
| procall(obj.GetDynamicData, obj, data) | |
| if next(data) ~= nil then | |
| data.handle = obj:GetHandle() | |
| dynamic_data[#dynamic_data + 1] = data | |
| data = {} | |
| end | |
| end) | |
| ResumeInfiniteLoopDetection("Saving") | |
| --sort | |
| table.sortby_field(dynamic_data, "handle") | |
| local spawnDataPaired = {} | |
| for i = 1, #spawn_data, 2 do | |
| local class = spawn_data[i] | |
| local handle = spawn_data[i + 1] | |
| spawnDataPaired[#spawnDataPaired + 1] = { class = class, handle = handle } | |
| end | |
| table.sortby_field(spawnDataPaired, "handle") | |
| table.iclear(spawn_data) | |
| for i, pair in ipairs(spawnDataPaired) do | |
| spawn_data[#spawn_data + 1] = pair.class | |
| spawn_data[#spawn_data + 1] = pair.handle | |
| end | |
| if Platform.developer and g_TestingSaveLoadSystem then | |
| --remove keys that would always differ | |
| g_LoadingHintsNextIdx = 1 | |
| local badKeys = { "vangle", "vpos", "vpos_time", "vangle_time" } | |
| for i, dat in ipairs(dynamic_data) do | |
| for i, key in ipairs(badKeys) do | |
| if dat[key] then | |
| dat[key] = nil | |
| end | |
| end | |
| end | |
| end | |
| local sector_data = { spawn = spawn_data, dynamic_data = dynamic_data } | |
| Msg("SaveDynamicData", sector_data) | |
| Msg("PreSaveSectorData", sector_data) | |
| gv_Sectors[gv_CurrentSectorId] = gv_Sectors[gv_CurrentSectorId] or { } | |
| --[[ | |
| -- uncomment do debug invalid table key type assert fail in TableToLuaCode during save | |
| local function sanity_check(tbl, label) | |
| for k, v in pairs(tbl) do | |
| local kt = type(k) | |
| local kl | |
| if kt ~= "boolean" and kt ~= "number" and kt ~= "string" then | |
| assert(false, string.format("invalid key type (%s) found in %s", tostring(kt), label)) | |
| kl = string.format("invalid (type %s)", kt) | |
| else | |
| kl = tostring(k) | |
| end | |
| if type(v) == "table" then | |
| sanity_check(v, label .. string.format("[\"%s\"]", kl)) | |
| end | |
| end | |
| end | |
| sanity_check(sector_data, "sector_data")--]] | |
| gv_Sectors[gv_CurrentSectorId].sector_data = tostring(TableToLuaCode(sector_data, nil, pstr("", 1024))) | |
| if Platform.developer then | |
| local len = #gv_Sectors[gv_CurrentSectorId].sector_data | |
| time = GetPreciseTicks() - time | |
| printf("Sector %s data saved in %d b (%d kb) for %dms", gv_CurrentSectorId, len, len / 1024, time) | |
| end | |
| end | |
| function SavegameSectorDataFixups.FormatChange(sector_data) | |
| -- spawn_data to array | |
| local spawn_data = {} | |
| for i, data in ipairs(sector_data.spawn) do | |
| spawn_data[i * 2 - 1] = data[1] | |
| spawn_data[i * 2] = data[2] | |
| end | |
| sector_data.spawn = spawn_data | |
| -- fold outside table with handle to data | |
| local dynamic_data = sector_data.dynamic_data | |
| for i, data in ipairs(dynamic_data) do | |
| if data[1] == "exploration_selection" then | |
| sector_data.exploration_selection = data[2] | |
| else | |
| data[2].handle = data[1] | |
| dynamic_data[i] = data[2] | |
| end | |
| end | |
| -- move all values to sector_data | |
| for key, data in pairs(dynamic_data) do | |
| if type(key) == "string" then | |
| assert(sector_data[key] == nil) | |
| sector_data[key] = data | |
| end | |
| end | |
| end | |
| local members_to_nil = { | |
| "on_die_hit_descr", | |
| "default_move_style", | |
| "cur_move_style", | |
| "cur_idle_style", | |
| "on_die_hit_descr", | |
| "infected", | |
| "aware", | |
| } | |
| function SavegameSessionDataFixups.OptimizeUnitMembers(data) | |
| local sectors = GetGameVarFromSession(data, "gv_Sectors") | |
| for sector, sector_data_org in pairs(sectors) do | |
| local modified | |
| local err, sector_data = LuaCodeToTuple(sector_data_org.sector_data) | |
| if err then | |
| print(string.format("OptimizeSectorDatas error for sector %s: %s", sector, err)) | |
| elseif next(sector_data) ~= nil then | |
| for _, data in ipairs(sector_data.dynamic_data) do | |
| local mod | |
| for _, member_name in ipairs(members_to_nil) do | |
| if data[member_name] == false then | |
| data[member_name] = nil | |
| mod = true | |
| end | |
| end | |
| if mod then | |
| modified = (modified or 0) + 1 | |
| end | |
| end | |
| end | |
| if modified then | |
| sector_data_org.sector_data = tostring(TableToLuaCode(sector_data, nil, pstr("", 1024))) | |
| --print(string.format("%s: modified %d records", sector, modified)) | |
| end | |
| end | |
| end | |
| function OnMsg.GatherSessionData() | |
| GatherSectorDynamicData() | |
| end | |
| ---------------Spawners----------- | |
| function UpdateSpawners() | |
| FireNetSyncEventOnHostOnce("UpdateSpawners") | |
| end | |
| function NetSyncEvents.UpdateSpawners() | |
| UpdateSpawnersLocal() | |
| end | |
| function UpdateSpawnersLocal() | |
| if IsEditorActive() or | |
| GameState.loading_savegame or | |
| not mapdata.GameLogic | |
| then | |
| return | |
| end | |
| SuspendPassEdits("UpdateSpawners") | |
| -- despawners first - otherwise duplicate SessionID can arise | |
| MapForEach("map", "ConditionalSpawnMarker", function(obj) | |
| local _, despawn_cond = obj:GetSpawnDespawnConditions() | |
| if despawn_cond then | |
| obj:Update() | |
| end | |
| end) | |
| -- everything should be clean now | |
| MapForEach("map", "ConditionalSpawnMarker", function(obj) | |
| obj:Update() | |
| end) | |
| ResumePassEdits("UpdateSpawners") | |
| g_TriggerEnemySpawners = false | |
| end | |
| OnMsg.QuestParamChanged = UpdateSpawners | |
| OnMsg.CombatStart = UpdateSpawners | |
| OnMsg.TurnStart = UpdateSpawners | |
| OnMsg.CombatStartRepositionDone = UpdateSpawners | |
| OnMsg.DbgStartExploration = UpdateSpawners | |
| OnMsg.CloseConversationDialog = UpdateSpawners | |
| function OnMsg.CloseSatelliteView() | |
| if GameState.loading_savegame then return end | |
| UpdateSpawnersLocal() --this is a sync msg | |
| end | |
| function OnMsg.CombatEnd(combat) if not combat.test_combat then UpdateSpawners() end end | |
| function OnMsg.PostNewMapLoaded() | |
| if not GameState or not GameState.loading_savegame then | |
| -- update spawned objects when start the game or load the map from editor | |
| UpdateSpawners() | |
| end | |
| end | |
| function OnMsg.OnDbgLoadLocation() | |
| UpdateSpawners() | |
| end | |
| function GenerateUniqueUnitDataId(base, sector, unit) | |
| gv_NextUnitDataUniqueId = (gv_NextUnitDataUniqueId or 0) + 1 | |
| return string.format("%s:%s:%s:%d", base, sector, unit, gv_NextUnitDataUniqueId-1) | |
| end | |
| function OnMsg.PreLoadSessionData() | |
| ForEachMerc(function(mId) | |
| local unitData = CreateUnitData(mId, mId, InteractionRand()) | |
| local props = UnitPropertiesStats:GetProperties() | |
| for _, prop in ipairs(props) do | |
| if prop.category == "Stats" then | |
| unitData:SetBase(prop.id, unitData[prop.id]) | |
| end | |
| end | |
| end) | |
| end | |
| function UpdateCombatExplorationConflictState(reset_all) | |
| if reset_all then | |
| ChangeGameState{Combat = false, Exploration = false, Conflict = false, ConflictScripted = false} | |
| else | |
| local combat = not not g_Combat | |
| local conflict = not not IsConflictMode(gv_CurrentSectorId) | |
| ChangeGameState{Combat = combat, Exploration = not combat, Conflict = conflict, ConflictScripted = conflict} | |
| end | |
| end | |
| local function lUpdateCombatExplorationConflictStateNoReset() | |
| UpdateCombatExplorationConflictState() | |
| end | |
| OnMsg.CombatEnd = lUpdateCombatExplorationConflictStateNoReset | |
| OnMsg.CombatStart = lUpdateCombatExplorationConflictStateNoReset | |
| OnMsg.ConflictEnd = lUpdateCombatExplorationConflictStateNoReset | |
| OnMsg.ConflictStart = lUpdateCombatExplorationConflictStateNoReset | |
| OnMsg.ExplorationStart = lUpdateCombatExplorationConflictStateNoReset | |
| OnMsg.EnterSector = lUpdateCombatExplorationConflictStateNoReset | |
| function OnMsg.NewGameSessionStart() | |
| UpdateCombatExplorationConflictState("reset") | |
| end | |
| function OnMsg.GameStateChanged(changed) | |
| if changed.Combat == true then | |
| SetGroupVolumeReason("combat", "Ambience", 500, 1000) | |
| elseif changed.Combat == false then | |
| SetGroupVolumeReason("combat", "Ambience", const.SoundMaxVolume, 1000) | |
| end | |
| end | |
| AppendClass.MapDataPreset = { | |
| properties = { | |
| { category = "Jagged Alliance 3", id = "Region", editor = "preset_id", default = "Jungle", | |
| preset_class = "GameStateDef", preset_group = "region", | |
| }, | |
| { category = "Jagged Alliance 3", id = "MainMenuRegion", editor = "combo", default = "Default", items = function (self) return PresetsCombo("GameStateDef", "region", "Default") end, | |
| name = "Region for main menu", help = "Choose a region to use as a main menu scene for this map, overriding the normal region of the map", | |
| }, | |
| { category = "Jagged Alliance 3", id = "Weather", name = "Force weather", editor = "combo", default = "none", items = function (self) return PresetsCombo("GameStateDef", "weather", "none") end }, | |
| { category = "Jagged Alliance 3", id = "Tod", name = "Force time of day", editor = "combo", default = "none", items = function (self) return PresetsCombo("GameStateDef", "time of day", "none") end }, | |
| { category = "Audio", id = "Reverb" }, | |
| { category = "Audio", id = "ReverbIndoor", name = "Reverb Indoor", editor = "preset_id", | |
| extra_item = "default from Region", default = "default from Region", | |
| preset_class = "ReverbDef", preset_group = "Default", | |
| }, | |
| { category = "Audio", id = "ReverbOutdoor", name = "Reverb Outdoor", editor = "preset_id", | |
| extra_item = "default from Region", default = "default from Region", | |
| preset_class = "ReverbDef", preset_group = "Default", | |
| }, | |
| } | |
| } | |
| AppendClass.GameStateDef = { | |
| properties = { | |
| {id = "ReverbIndoor", name = "Reverb Indoor", editor = "preset_id", | |
| default = "AboveGround_Indoor", preset_class = "ReverbDef", preset_group = "Default", | |
| no_edit = function(self) return self.group ~= "region" end, | |
| }, | |
| {id = "ReverbOutdoor", name = "Reverb Outdoor", editor = "preset_id", | |
| default = "AboveGround_Outdoor", preset_class = "ReverbDef", preset_group = "Default", | |
| no_edit = function(self) return self.group ~= "region" end, | |
| }, | |
| {id = "WeatherCycle", name = "Weather Cycle", editor = "choice", default = false, | |
| no_edit = function(self) return self.group ~= "region" end, | |
| items = function (self) return table.keys2(WeatherCycle) end, | |
| }, | |
| } | |
| } | |
| function OnMsg.GameStateChanged(changed) | |
| if ChangingMap then return end | |
| local GameStateDefs = GameStateDefs | |
| for state_id, state in sorted_pairs(changed) do | |
| local state_def = GameStateDefs[state_id] | |
| if state_def and state_def.group == "weather" then | |
| -- wind markers should react to this | |
| DelayedCall(0, ApplyWindMarkers) | |
| break | |
| end | |
| end | |
| end | |
| ----- Timers | |
| MapVar("Timers", {}) | |
| function TimerCreate(id, text, time) | |
| table.remove_entry(Timers, Timers[id]) | |
| local timer = { | |
| id = id, | |
| text = text or "", | |
| time = time or 0, | |
| last_update = GameTime(), | |
| } | |
| Timers[id] = timer | |
| Timers[#Timers + 1] = timer | |
| TimersUpdateTime(gv_ActiveCombat == gv_CurrentSectorId and 0) | |
| return timer | |
| end | |
| function TimerGetData(id) | |
| return Timers[id] | |
| end | |
| function TimerWait(id) | |
| while true do | |
| local data = TimerGetData(id) | |
| if not data or data.time <= 0 or data.StopTCE then | |
| return data and (data.StopTCE and "break" or "finished") | |
| end | |
| WaitMsg("TimerFinished") | |
| end | |
| end | |
| function TimerDelete(id) | |
| table.remove_entry(Timers, Timers[id]) | |
| Timers[id] = nil | |
| TimersUpdateTime(gv_ActiveCombat == gv_CurrentSectorId and 0) | |
| end | |
| function TimersUpdateTime(delta) | |
| local dialog = GetInGameInterfaceModeDlg() | |
| local control = dialog and dialog:ResolveId("idTimerText") | |
| local context = control and control.context | |
| local now = GameTime() | |
| for i = #Timers, 1, -1 do | |
| local timer = Timers[i] | |
| local time = timer.time - (delta and delta or now - timer.last_update) | |
| if time <= 0 then | |
| timer.time = 0 | |
| Msg("TimerFinished", timer.id) | |
| -- Dont remove timers after they've finished | |
| -- as their finished state needs to be observed for | |
| -- an indeterminate amount of time. | |
| else | |
| timer.time = time | |
| timer.last_update = now | |
| if context then | |
| context.text = timer.text | |
| context.time = time | |
| ObjModified(context) | |
| context = nil | |
| end | |
| end | |
| end | |
| if context and context.time > 0 then | |
| context.time = 0 | |
| ObjModified(context) | |
| end | |
| end | |
| function OnMsg.CombatStart() | |
| TimersUpdateTime(0) | |
| end | |
| function OnMsg.CombatEnd() | |
| TimersUpdateTime(0) | |
| end | |
| function OnMsg.NewCombatTurn(team) | |
| if g_CurrentTeam and g_Teams[g_CurrentTeam].control == "UI" then | |
| TimersUpdateTime(const.Combat.TimerTurnTime or 0) | |
| end | |
| end | |
| function OnMsg.SaveDynamicData(data) | |
| for _, timer in ipairs(Timers) do | |
| data.Timers = data.Timers or {} | |
| data.Timers[#data.Timers + 1] = { | |
| id = timer.id, | |
| time = timer.time, | |
| textId = timer.text and TGetID(timer.text) | |
| } | |
| end | |
| end | |
| function OnMsg.LoadDynamicData(data) | |
| Timers = data.Timers or Timers | |
| for _, timer in ipairs(Timers) do | |
| timer.last_update = GameTime() | |
| if not timer.text and timer.textId then | |
| timer.text = T{timer.textId, TranslationTable[timer.textId]} or Untranslated("Name not localized") | |
| end | |
| Timers[timer.id] = timer | |
| end | |
| end | |
| function lGetGameStartTypes() | |
| local types = { | |
| { | |
| id = "Campaign", | |
| Name = T(831335436250, "NEW GAME"), | |
| func = function() | |
| StartNewGame() | |
| end, | |
| }, | |
| { | |
| id = "QuickStart", | |
| Name = T(419695626381, "QUICK START"), | |
| func = function() | |
| StartQuickStartGame() | |
| end | |
| } | |
| } | |
| return types | |
| end | |
| function MultipleCampaignPresetsPresent() | |
| local t = table.keys(CampaignPresets) | |
| return t and #t > 1 | |
| end | |
| function StartNewGame() | |
| EditorDeactivate() | |
| local IGmain = GetDialog("InGameMenu") | |
| if IGmain then | |
| CloseDialog("InGameMenu") | |
| end | |
| local dlg = GetDialog("PreGameMenu") | |
| local multipleCampaignPresets = MultipleCampaignPresetsPresent() | |
| if not dlg then | |
| CreateRealTimeThread(function() | |
| ResetGameSession() | |
| local dlg = OpenDialog("PreGameMenu") | |
| dlg:SetMode("NewGame") | |
| dlg:ResolveId("idSubMenuTittle"):SetText(T(831335436250, "NEW GAME")) | |
| dlg:ResolveId("idSubContent"):SetMode(multipleCampaignPresets and "newgame01" or "newgame02") | |
| Msg("PreGameMenuOpen") | |
| end) | |
| elseif dlg.Mode ~= "NewGame" then | |
| dlg:SetMode("NewGame") | |
| dlg:ResolveId("idSubMenuTittle"):SetText(T(831335436250, "NEW GAME")) | |
| dlg:ResolveId("idSubContent"):SetMode(multipleCampaignPresets and "newgame01" or "newgame02") | |
| end | |
| end | |
| function StartQuickStartGame() | |
| EditorDeactivate() | |
| CreateRealTimeThread(QuickStartCampaign, "HotDiamonds", { difficulty = "Normal" }) | |
| end | |
| GameStartTypes = lGetGameStartTypes() | |
| function ApplyNewGameOptions(newGameObj) | |
| if newGameObj then | |
| if Platform.console and IsInMultiplayerGame() and not NetIsHost() then | |
| newGameObj.campaign_name = GenerateMultiplayerGuestCampaignName() -- haven't seen a need for this (it is overwritten on LoadGame), but better safe than sorry | |
| end | |
| OptionsObj = OptionsObj or OptionsCreateAndLoad() | |
| Game.playthrough_name = newGameObj.campaign_name | |
| Game.testModGame = AreModdingToolsActive() | |
| if newGameObj["difficulty"] then | |
| Game.game_difficulty = newGameObj["difficulty"] | |
| OptionsObj["Difficulty"] = newGameObj["difficulty"] | |
| end | |
| for gameRule, value in pairs(newGameObj["game_rules"]) do | |
| if value then | |
| Game:AddGameRule(gameRule) | |
| end | |
| end | |
| SetGameRulesOptions() | |
| --do not apply settings in multiplayer game as they are locked | |
| if newGameObj["settings"] and not IsInMultiplayerGame() then | |
| for option, value in pairs(newGameObj["settings"]) do | |
| OptionsObj[option] = value | |
| end | |
| end | |
| CreateRealTimeThread(function() | |
| OptionsObject.WaitApplyOptions(OptionsObj) | |
| OptionsObj = false | |
| end) | |
| end | |
| end | |
| function OnMsg.DevUIMapChangePrep() | |
| local campaign = CampaignPresets["HotDiamonds" or false] or GetCurrentCampaignPreset() | |
| NewGameSession(campaign) | |
| end | |
| function IsDemoSector(sector) | |
| return not not table.find(Presets.Build_Settings.Demo.DemoMaps.Value, sector) | |
| end | |
| function OnMsg.GatherGameEntities(_, additional_blacklist_textures) | |
| local blacklist_sectors = {} | |
| for _, preset in ipairs(Presets.CampaignPreset or empty_table) do | |
| for _, campaign in ipairs(preset or empty_table) do | |
| for _, sector in ipairs(campaign.Sectors or empty_table) do | |
| if not IsDemoSector(sector.Map) then | |
| blacklist_sectors[sector.Id] = campaign.id | |
| end | |
| end | |
| end | |
| end | |
| local sectors = table.keys(blacklist_sectors, "sorted") | |
| for _, sector in ipairs(sectors) do | |
| local image = string.format("UI/LoadingScreens/%s/%s.dds", blacklist_sectors[sector], sector) | |
| table.insert(additional_blacklist_textures, image) | |
| end | |
| end | |
| local function FindUnitSafePos(unit, arrival_dir_destinations) | |
| local new_pos, interaction_pos | |
| if not unit:IsValidPos() then | |
| return | |
| end | |
| if unit.behavior == "Visit" then | |
| local marker = unit.behavior_params[1] and unit.behavior_params[1][1] | |
| local visitable = IsKindOf(marker, "AmbientLifeMarker") and marker:GetVisitable() | |
| local interaction_pos = visitable and (IsPoint(visitable[2]) and visitable[2] or marker:GetPos()) | |
| if not interaction_pos or not terrain.IsPassable(interaction_pos) then | |
| unit:SetBehavior() | |
| unit:SetCommand("Idle") | |
| end | |
| elseif unit:HasStatusEffect("ManningEmplacement") then | |
| local handle = unit:GetEffectValue("hmg_emplacement") | |
| local obj = HandleToObject[handle] | |
| if IsKindOf(obj, "MachineGunEmplacement") then | |
| interaction_pos = SnapToPassSlab(unit) | |
| else | |
| unit:LeaveEmplacement(true) | |
| end | |
| end | |
| if not interaction_pos or not terrain.IsPassable(interaction_pos) then | |
| local x, y, z = FindFallDownPos(interaction_pos or unit.traverse_tunnel and unit.traverse_tunnel:GetExit() or unit) | |
| if x then | |
| new_pos = point(x, y, z) | |
| end | |
| end | |
| -- find destinations | |
| local destinations | |
| if IsValid(unit.routine_spawner) then | |
| local dest = GetPassSlab(unit.routine_spawner) | |
| if not dest then | |
| local sx, sy, sz = FindFallDownPos(unit.routine_spawner) | |
| if sx then | |
| dest = point(sx, sy, sz) | |
| end | |
| end | |
| if dest then | |
| destinations = { dest } | |
| end | |
| end | |
| if not destinations then | |
| destinations = arrival_dir_destinations[unit.arrival_dir] | |
| if not destinations then | |
| destinations = {} | |
| arrival_dir_destinations[unit.arrival_dir] = destinations | |
| local markers = GetAvailableEntranceMarkers(unit.arrival_dir) | |
| for i, marker in ipairs(markers) do | |
| local pos = GetPassSlab(marker) | |
| if pos then | |
| table.insert(destinations, pos) | |
| end | |
| end | |
| end | |
| end | |
| if destinations and #destinations > 0 then | |
| local start_pos = new_pos or interaction_pos or GetPassSlab(unit) or unit:GetPos() | |
| local pfclass = CalcPFClass("player1") | |
| local pfflags = const.pfmImpassableSource | |
| if not new_pos then | |
| local has_path, closest_pos = pf.HasPosPath(start_pos, destinations, pfclass, 0, 0, nil, 0, nil, pfflags) | |
| if has_path and table.find(destinations, closest_pos) then | |
| return -- there is a path to destinations | |
| end | |
| end | |
| local best_pos | |
| for i, dest in ipairs(destinations) do | |
| local has_path, closest_pos = pf.HasPosPath(dest, start_pos, pfclass, 0, 0, nil, 0, nil, pfflags) | |
| if closest_pos and (not best_pos or IsCloser(start_pos, closest_pos, best_pos)) then | |
| best_pos = closest_pos | |
| end | |
| end | |
| if best_pos then | |
| new_pos = best_pos | |
| end | |
| end | |
| if unit:IsDead() then | |
| if new_pos then | |
| unit:SetPos(new_pos) -- the dead command will do the rest | |
| end | |
| return | |
| end | |
| if unit.ephemeral then | |
| DoneObject(unit) | |
| return | |
| end | |
| if not new_pos then | |
| new_pos = unit:GetPos() | |
| end | |
| if not CanOccupy(unit, new_pos) then | |
| local has_path, closest_pos = pf.HasPosPath(new_pos, new_pos, CalcPFClass("player1"), 0, 0, unit, 0, nil, const.pfmImpassableSource) | |
| if closest_pos then | |
| new_pos = closest_pos | |
| end | |
| end | |
| if unit.behavior == "Visit" then | |
| unit:SetBehavior() | |
| unit:SetCommand("Idle") | |
| elseif unit:HasStatusEffect("ManningEmplacement") then | |
| unit:LeaveEmplacement(true) | |
| end | |
| unit:SetPos(new_pos) | |
| end | |
| function ValidateMapGameplay() | |
| local arrival_dir_destinations = {} | |
| local units = g_Units | |
| for i = #units, 1, -1 do | |
| FindUnitSafePos(units[i], arrival_dir_destinations) | |
| end | |
| end | |