myspace / Lua /GameSession.lua
sirnii's picture
Upload 1816 files
b6a38d7 verified
raw
history blame
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