Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 15972) +++ ps/trunk/binaries/data/mods/public/gui/gamesetup/gamesetup.js (revision 15973) @@ -1,1870 +1,1873 @@ //////////////////////////////////////////////////////////////////////////////////////////////// // Constants const DEFAULT_NETWORKED_MAP = "Acropolis 01"; const DEFAULT_OFFLINE_MAP = "Acropolis 01"; const VICTORY_DEFAULTIDX = 1; // TODO: Move these somewhere like simulation\data\game_types.json, Atlas needs them too // Translation: Type of victory condition. const POPULATION_CAP = ["50", "100", "150", "200", "250", "300", translate("Unlimited")]; const POPULATION_CAP_DATA = [50, 100, 150, 200, 250, 300, 10000]; const POPULATION_CAP_DEFAULTIDX = 5; // Translation: Amount of starting resources. const STARTING_RESOURCES = [translateWithContext("startingResources", "Very Low"), translateWithContext("startingResources", "Low"), translateWithContext("startingResources", "Medium"), translateWithContext("startingResources", "High"), translateWithContext("startingResources", "Very High"), translateWithContext("startingResources", "Deathmatch")]; const STARTING_RESOURCES_DATA = [100, 300, 500, 1000, 3000, 50000]; const STARTING_RESOURCES_DEFAULTIDX = 1; // Max number of players for any map const MAX_PLAYERS = 8; //////////////////////////////////////////////////////////////////////////////////////////////// // Is this is a networked game, or offline var g_IsNetworked; // Is this user in control of game settings (i.e. is a network server, or offline player) var g_IsController; // Server name, if user is a server, connected to the multiplayer lobby var g_ServerName; // Are we currently updating the GUI in response to network messages instead of user input // (and therefore shouldn't send further messages to the network) var g_IsInGuiUpdate; // Is this user ready var g_IsReady; // There are some duplicate orders on init, we can ignore these [bool]. var g_ReadyInit = true; // If no one has changed ready status, we have no need to spam the settings changed message. // 2 - Host's initial ready, suppressed settings message, 1 - Will show settings message, <=0 - Suppressed settings message var g_ReadyChanged = 2; // Has the game started? var g_GameStarted = false; var g_PlayerAssignments = {}; // Default game setup attributes var g_DefaultPlayerData = []; var g_GameAttributes = { settings: {} }; var g_GameSpeeds = {}; var g_MapSizes = {}; var g_AIs = []; var g_ChatMessages = []; // Data caches var g_MapData = {}; var g_CivData = {}; var g_MapFilters = []; // Current number of assigned human players. var g_AssignedCount = 0; // To prevent the display locking up while we load the map metadata, // we'll start with a 'loading' message and switch to the main screen in the // tick handler var g_LoadingState = 0; // 0 = not started, 1 = loading, 2 = loaded //////////////////////////////////////////////////////////////////////////////////////////////// function init(attribs) { switch (attribs.type) { case "offline": g_IsNetworked = false; g_IsController = true; break; case "server": g_IsNetworked = true; g_IsController = true; break; case "client": g_IsNetworked = true; g_IsController = false; break; default: error(sprintf("Unexpected 'type' in gamesetup init: %(unexpectedType)s", { unexpectedType: attribs.type })); } if (attribs.serverName) g_ServerName = attribs.serverName; // Init the Cancel Button caption and tooltip var cancelButton = Engine.GetGUIObjectByName("cancelGame"); if(!Engine.HasXmppClient()) { cancelButton.tooltip = translate("Return to the main menu."); } else { cancelButton.tooltip = translate("Return to the lobby."); } } // Called after the map data is loaded and cached function initMain() { // Load AI list g_AIs = Engine.GetAIs(); // Sort AIs by displayed name g_AIs.sort(function (a, b) { return a.data.name < b.data.name ? -1 : b.data.name < a.data.name ? +1 : 0; }); // Get default player data - remove gaia g_DefaultPlayerData = initPlayerDefaults(); g_DefaultPlayerData.shift(); for (var i = 0; i < g_DefaultPlayerData.length; i++) g_DefaultPlayerData[i].Civ = "random"; g_GameSpeeds = initGameSpeeds(); g_MapSizes = initMapSizes(); // Init civs initCivNameList(); // Init map types var mapTypes = Engine.GetGUIObjectByName("mapTypeSelection"); mapTypes.list = [translateWithContext("map", "Skirmish"), translateWithContext("map", "Random"), translate("Scenario")]; mapTypes.list_data = ["skirmish","random","scenario"]; // Setup map filters - will appear in order they are added addFilter("default", translate("Default"), function(settings) { return settings && (settings.Keywords === undefined || !keywordTestOR(settings.Keywords, ["naval", "demo", "hidden"])); }); addFilter("naval", translate("Naval Maps"), function(settings) { return settings && settings.Keywords !== undefined && keywordTestAND(settings.Keywords, ["naval"]); }); addFilter("demo", translate("Demo Maps"), function(settings) { return settings && settings.Keywords !== undefined && keywordTestAND(settings.Keywords, ["demo"]); }); addFilter("all", translate("All Maps"), function(settings) { return true; }); // Populate map filters dropdown var mapFilters = Engine.GetGUIObjectByName("mapFilterSelection"); mapFilters.list = getFilterNames(); mapFilters.list_data = getFilterIds(); g_GameAttributes.mapFilter = "default"; // Setup controls for host only if (g_IsController) { mapTypes.selected = 0; mapFilters.selected = 0; // Create a unique ID for this match, to be used for identifying the same game reports // for the lobby. g_GameAttributes.matchID = Engine.GetMatchID(); initMapNameList(); var numPlayersSelection = Engine.GetGUIObjectByName("numPlayersSelection"); var players = []; for (var i = 1; i <= MAX_PLAYERS; ++i) players.push(i); numPlayersSelection.list = players; numPlayersSelection.list_data = players; numPlayersSelection.selected = MAX_PLAYERS - 1; var gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.hidden = false; Engine.GetGUIObjectByName("gameSpeedText").hidden = true; gameSpeed.list = g_GameSpeeds.names; gameSpeed.list_data = g_GameSpeeds.speeds; gameSpeed.onSelectionChange = function() { // Update attributes so other players can see change if (this.selected != -1) g_GameAttributes.gameSpeed = g_GameSpeeds.speeds[this.selected]; if (!g_IsInGuiUpdate) updateGameAttributes(); } gameSpeed.selected = g_GameSpeeds["default"]; var populationCaps = Engine.GetGUIObjectByName("populationCap"); populationCaps.list = POPULATION_CAP; populationCaps.list_data = POPULATION_CAP_DATA; populationCaps.selected = POPULATION_CAP_DEFAULTIDX; populationCaps.onSelectionChange = function() { if (this.selected != -1) g_GameAttributes.settings.PopulationCap = POPULATION_CAP_DATA[this.selected]; if (!g_IsInGuiUpdate) updateGameAttributes(); } var startingResourcesL = Engine.GetGUIObjectByName("startingResources"); startingResourcesL.list = STARTING_RESOURCES; startingResourcesL.list_data = STARTING_RESOURCES_DATA; startingResourcesL.selected = STARTING_RESOURCES_DEFAULTIDX; startingResourcesL.onSelectionChange = function() { if (this.selected != -1) g_GameAttributes.settings.StartingResources = STARTING_RESOURCES_DATA[this.selected]; if (!g_IsInGuiUpdate) updateGameAttributes(); } var victoryConditions = Engine.GetGUIObjectByName("victoryCondition"); var victories = getVictoryConditions(); victoryConditions.list = victories.text; victoryConditions.list_data = victories.data; victoryConditions.onSelectionChange = function() { // Update attributes so other players can see change if (this.selected != -1) { g_GameAttributes.settings.GameType = victories.data[this.selected]; g_GameAttributes.settings.VictoryScripts = victories.scripts[this.selected]; } if (!g_IsInGuiUpdate) updateGameAttributes(); }; victoryConditions.selected = VICTORY_DEFAULTIDX; var mapSize = Engine.GetGUIObjectByName("mapSize"); mapSize.list = g_MapSizes.names; mapSize.list_data = g_MapSizes.tiles; mapSize.onSelectionChange = function() { // Update attributes so other players can see change if (this.selected != -1) g_GameAttributes.settings.Size = g_MapSizes.tiles[this.selected]; if (!g_IsInGuiUpdate) updateGameAttributes(); }; mapSize.selected = 0; Engine.GetGUIObjectByName("revealMap").onPress = function() { // Update attributes so other players can see change g_GameAttributes.settings.RevealMap = this.checked; if (!g_IsInGuiUpdate) updateGameAttributes(); }; Engine.GetGUIObjectByName("exploreMap").onPress = function() { // Update attributes so other players can see change g_GameAttributes.settings.ExploreMap = this.checked; if (!g_IsInGuiUpdate) updateGameAttributes(); }; Engine.GetGUIObjectByName("lockTeams").onPress = function() { // Update attributes so other players can see change g_GameAttributes.settings.LockTeams = this.checked; if (!g_IsInGuiUpdate) updateGameAttributes(); }; Engine.GetGUIObjectByName("enableCheats").onPress = function() { // Update attributes so other players can see change g_GameAttributes.settings.CheatsEnabled = this.checked; if (!g_IsInGuiUpdate) updateGameAttributes(); }; Engine.GetGUIObjectByName("enableRating").onPress = function() { // Update attributes so other players can see change g_GameAttributes.settings.RatingEnabled = this.checked; Engine.SetRankedGame(this.checked); Engine.GetGUIObjectByName("enableCheats").enabled = !this.checked; Engine.GetGUIObjectByName("lockTeams").enabled = !this.checked; if (!g_IsInGuiUpdate) updateGameAttributes(); }; } else { // If we're a network client, disable all the map controls Engine.GetGUIObjectByName("mapTypeSelection").hidden = true; Engine.GetGUIObjectByName("mapTypeText").hidden = false; Engine.GetGUIObjectByName("mapFilterSelection").hidden = true; Engine.GetGUIObjectByName("mapFilterText").hidden = false; Engine.GetGUIObjectByName("mapSelectionText").hidden = false; Engine.GetGUIObjectByName("mapSelection").hidden = true; Engine.GetGUIObjectByName("victoryConditionText").hidden = false; Engine.GetGUIObjectByName("victoryCondition").hidden = true; Engine.GetGUIObjectByName("gameSpeedText").hidden = false; Engine.GetGUIObjectByName("gameSpeed").hidden = true; // Disable player and game options controls // TODO: Shouldn't players be able to choose their own assignment? for (var i = 0; i < MAX_PLAYERS; ++i) { Engine.GetGUIObjectByName("playerAssignment["+i+"]").hidden = true; Engine.GetGUIObjectByName("playerCiv["+i+"]").hidden = true; Engine.GetGUIObjectByName("playerTeam["+i+"]").hidden = true; } Engine.GetGUIObjectByName("numPlayersSelection").hidden = true; Engine.GetGUIObjectByName("startGame").enabled = true; } // Set up multiplayer/singleplayer bits: if (!g_IsNetworked) { Engine.GetGUIObjectByName("chatPanel").hidden = true; Engine.GetGUIObjectByName("enableCheats").checked = true; g_GameAttributes.settings.CheatsEnabled = true; } else { Engine.GetGUIObjectByName("optionCheats").hidden = false; Engine.GetGUIObjectByName("enableCheats").checked = false; g_GameAttributes.settings.CheatsEnabled = false; // Setup ranked option if we are connected to the lobby. if (Engine.HasXmppClient()) { Engine.GetGUIObjectByName("optionRating").hidden = false; Engine.GetGUIObjectByName("enableRating").checked = Engine.IsRankedGame(); g_GameAttributes.settings.RatingEnabled = Engine.IsRankedGame(); // We force locked teams and disabled cheats in ranked games. Engine.GetGUIObjectByName("enableCheats").enabled = !Engine.IsRankedGame(); Engine.GetGUIObjectByName("lockTeams").enabled = !Engine.IsRankedGame(); } if (g_IsController) { Engine.GetGUIObjectByName("enableCheatsText").hidden = true; Engine.GetGUIObjectByName("enableCheats").hidden = false; if (Engine.HasXmppClient()) { Engine.GetGUIObjectByName("enableRatingText").hidden = true; Engine.GetGUIObjectByName("enableRating").hidden = false; } } } // Settings for all possible player slots var boxSpacing = 32; for (var i = 0; i < MAX_PLAYERS; ++i) { // Space player boxes var box = Engine.GetGUIObjectByName("playerBox["+i+"]"); var boxSize = box.size; var h = boxSize.bottom - boxSize.top; boxSize.top = i * boxSpacing; boxSize.bottom = i * boxSpacing + h; box.size = boxSize; // Populate team dropdowns var team = Engine.GetGUIObjectByName("playerTeam["+i+"]"); team.list = [translateWithContext("team", "None"), "1", "2", "3", "4"]; team.list_data = [-1, 0, 1, 2, 3]; team.selected = 0; let playerSlot = i; // declare for inner function use team.onSelectionChange = function() { // Update team if (this.selected != -1) g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1; if (!g_IsInGuiUpdate) updateGameAttributes(); }; // Set events var civ = Engine.GetGUIObjectByName("playerCiv["+i+"]"); civ.onSelectionChange = function() { // Update civ if ((this.selected != -1)&&(g_GameAttributes.mapType !== "scenario")) g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected]; if (!g_IsInGuiUpdate) updateGameAttributes(); }; } if (g_IsNetworked) { // For multiplayer, focus the chat input box by default Engine.GetGUIObjectByName("chatInput").focus(); } else { // For single-player, focus the map list by default, // to allow easy keyboard selection of maps Engine.GetGUIObjectByName("mapSelection").focus(); } // Sync g_GameAttributes to everyone. if (g_IsController) updateGameAttributes(); } function handleNetMessage(message) { log("Net message: "+uneval(message)); switch (message.type) { case "netstatus": switch (message.status) { case "disconnected": cancelSetup(); if (Engine.HasXmppClient()) Engine.SwitchGuiPage("page_lobby.xml"); else Engine.SwitchGuiPage("page_pregame.xml"); reportDisconnect(message.reason); break; default: error("Unrecognised netstatus type "+message.status); break; } break; case "gamesetup": if (message.data) // (the host gets undefined data on first connect, so skip that) { g_GameAttributes = message.data; // Validate some settings for rated games. if (g_GameAttributes.settings.RatingEnabled) { // Cheats can never be on in rated games. g_GameAttributes.settings.CheatsEnabled = false; // Teams must be locked in rated games. g_GameAttributes.settings.LockTeams = true; } } onGameAttributesChange(); break; case "players": var resetReady = false; var newPlayer = ""; // Find and report all joinings/leavings for (var host in message.hosts) { if (! g_PlayerAssignments[host]) { // If we have extra player slots and we are the controller, give the player an ID. if (g_IsController && message.hosts[host].player === -1 && g_AssignedCount < g_GameAttributes.settings.PlayerData.length) Engine.AssignNetworkPlayer(g_AssignedCount + 1, host); addChatMessage({ "type": "connect", "username": message.hosts[host].name }); newPlayer = host; } } for (var host in g_PlayerAssignments) { if (! message.hosts[host]) { addChatMessage({ "type": "disconnect", "guid": host }); if (g_PlayerAssignments[host].player != -1) resetReady = true; // Observers shouldn't reset ready. } } // Update the player list g_PlayerAssignments = message.hosts; updatePlayerList(); if (g_PlayerAssignments[newPlayer] && g_PlayerAssignments[newPlayer].player != -1) resetReady = true; if (resetReady) resetReadyData(); // Observers shouldn't reset ready. updateReadyUI(); if (g_IsController) sendRegisterGameStanza(); break; case "start": if (g_IsController && Engine.HasXmppClient()) { var players = [ assignment.name for each (assignment in g_PlayerAssignments) ] Engine.SendChangeStateGame(Object.keys(g_PlayerAssignments).length, players.join(", ")); } Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked" : g_IsNetworked, "playerAssignments": g_PlayerAssignments, "isController": g_IsController }); break; case "chat": addChatMessage({ "type": "message", "guid": message.guid, "text": message.text }); break; // Singular client to host message case "ready": g_ReadyChanged -= 1; if (g_ReadyChanged < 1 && g_PlayerAssignments[message.guid].player != -1) addChatMessage({ "type": "ready", "guid": message.guid, "ready": +message.status == 1 }); if (!g_IsController) break; g_PlayerAssignments[message.guid].status = +message.status == 1; Engine.SetNetworkPlayerStatus(message.guid, +message.status); updateReadyUI(); break; default: error("Unrecognised net message type "+message.type); } } // Get display name from map data. function getMapDisplayName(map) { var mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Name) { // Give some msg that map format is unsupported log("Map data missing in scenario '"+map+"' - likely unsupported format"); return map; } return mapData.settings.Name; } // Get display name from map data function getMapPreview(map) { var mapData = loadMapData(map); if (!mapData || !mapData.settings || !mapData.settings.Preview) { // Give some msg that map format is unsupported return "nopreview.png"; } return mapData.settings.Preview; } // Get a setting if it exists or return default function getSetting(settings, defaults, property) { if (settings && (property in settings)) return settings[property]; // Use defaults if (defaults && (property in defaults)) return defaults[property]; return undefined; } // Initialize the dropdowns containing all the available civs function initCivNameList() { // Cache civ data g_CivData = loadCivData(); // Extract name/code, and skip civs that are explicitly disabled // (intended for unusable incomplete civs) var civList = [ { "name": civ.Name, "code": civ.Code } for each (civ in g_CivData) if (civ.SelectableInGameSetup !== false) ]; // Alphabetically sort the list, ignoring case civList.sort(sortNameIgnoreCase); var civListNames = [ civ.name for each (civ in civList) ]; var civListCodes = [ civ.code for each (civ in civList) ]; // Add random civ to beginning of list civListNames.unshift("[color=\"orange\"]" + translateWithContext("civilization", "Random")); civListCodes.unshift("random"); // Update the dropdowns for (var i = 0; i < MAX_PLAYERS; ++i) { var civ = Engine.GetGUIObjectByName("playerCiv["+i+"]"); civ.list = civListNames; civ.list_data = civListCodes; civ.selected = 0; } } // Initialise the list control containing all the available maps function initMapNameList() { // Get a list of map filenames // TODO: Should verify these are valid maps before adding to list var mapSelectionBox = Engine.GetGUIObjectByName("mapSelection") var mapFiles; switch (g_GameAttributes.mapType) { case "scenario": case "skirmish": mapFiles = getXMLFileList(g_GameAttributes.mapPath); break; case "random": mapFiles = getJSONFileList(g_GameAttributes.mapPath); break; default: error(sprintf("initMapNameList: Unexpected map type '%(mapType)s'", { mapType: g_GameAttributes.mapType })); return; } // Apply map filter, if any defined var mapList = []; for (var i = 0; i < mapFiles.length; ++i) { var file = g_GameAttributes.mapPath + mapFiles[i]; var mapData = loadMapData(file); if (g_GameAttributes.mapFilter && mapData && testFilter(g_GameAttributes.mapFilter, mapData.settings)) mapList.push({ "name": getMapDisplayName(file), "file": file }); } // Alphabetically sort the list, ignoring case translateObjectKeys(mapList, ["name"]); mapList.sort(sortNameIgnoreCase); var mapListNames = [ map.name for each (map in mapList) ]; var mapListFiles = [ map.file for each (map in mapList) ]; // Select the default map var selected = mapListFiles.indexOf(g_GameAttributes.map); // Default to the first element if list is not empty and we can't find the one we searched for if (selected == -1 && mapList.length) { selected = 0; } // Update the list control if (g_GameAttributes.mapType == "random") { mapListNames.unshift("[color=\"orange\"]" + translateWithContext("map", "Random") + "[/color]"); mapListFiles.unshift("random"); } mapSelectionBox.list = mapListNames; mapSelectionBox.list_data = mapListFiles; mapSelectionBox.selected = selected; } function loadMapData(name) { if (!name) return undefined; if (!g_MapData[name]) { switch (g_GameAttributes.mapType) { case "scenario": case "skirmish": g_MapData[name] = Engine.LoadMapSettings(name); break; case "random": if (name == "random") // To be defined later. g_MapData[name] = { settings: { "Name": "", "Description": "" } }; else g_MapData[name] = Engine.ReadJSONFile(name+".json"); break; default: error(sprintf("loadMapData: Unexpected map type '%(mapType)s'", { mapType: g_GameAttributes.mapType })); return undefined; } } return g_MapData[name]; } //////////////////////////////////////////////////////////////////////////////////////////////// // GUI event handlers function cancelSetup() { Engine.DisconnectNetworkGame(); if (Engine.HasXmppClient()) { // Set player presence Engine.LobbySetPlayerPresence("available"); // Unregister the game if (g_IsController) Engine.SendUnregisterGame(); } } var lastXmppClientPoll = Date.now(); function onTick() { // First tick happens before first render, so don't load yet if (g_LoadingState == 0) { g_LoadingState++; } else if (g_LoadingState == 1) { Engine.GetGUIObjectByName("loadingWindow").hidden = true; Engine.GetGUIObjectByName("setupWindow").hidden = false; initMain(); g_LoadingState++; } else if (g_LoadingState == 2) { while (true) { var message = Engine.PollNetworkClient(); if (!message) break; handleNetMessage(message); } } } // Called when user selects number of players function selectNumPlayers(num) { // Avoid recursion if (g_IsInGuiUpdate) return; // Network clients can't change number of players if (g_IsNetworked && !g_IsController) return; // Only meaningful for random maps if (g_GameAttributes.mapType != "random") return; // Update player data var pData = g_GameAttributes.settings.PlayerData; if (pData && num < pData.length) { // Remove extra player data g_GameAttributes.settings.PlayerData = pData.slice(0, num); } else { // Add player data from defaults for (var i = pData.length; i < num; ++i) g_GameAttributes.settings.PlayerData.push(g_DefaultPlayerData[i]); } // Some players may have lost their assigned slot for (var guid in g_PlayerAssignments) { var player = g_PlayerAssignments[guid].player; if (player > num) { if (g_IsNetworked) Engine.AssignNetworkPlayer(player, ""); else g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1, "civ": "", "team": -1, "ready": 0} }; } } updateGameAttributes(); } // Called when the user selects a map type from the list function selectMapType(type) { // Avoid recursion if (g_IsInGuiUpdate) return; // Network clients can't change map type if (g_IsNetworked && !g_IsController) return; // Reset game attributes g_GameAttributes.map = ""; g_GameAttributes.mapType = type; // Clear old map data g_MapData = {}; // Select correct path switch (g_GameAttributes.mapType) { case "scenario": // Set a default map // TODO: This should be remembered from the last session g_GameAttributes.mapPath = "maps/scenarios/"; g_GameAttributes.map = g_GameAttributes.mapPath + (g_IsNetworked ? DEFAULT_NETWORKED_MAP : DEFAULT_OFFLINE_MAP); + g_GameAttributes.settings.AISeed = Math.floor(Math.random() * 65536); break; case "skirmish": g_GameAttributes.mapPath = "maps/skirmishes/"; g_GameAttributes.settings = { PlayerData: g_DefaultPlayerData.slice(0, 4), Seed: Math.floor(Math.random() * 65536), + AISeed: Math.floor(Math.random() * 65536), CheatsEnabled: g_GameAttributes.settings.CheatsEnabled }; break; case "random": g_GameAttributes.mapPath = "maps/random/"; g_GameAttributes.settings = { PlayerData: g_DefaultPlayerData.slice(0, 4), Seed: Math.floor(Math.random() * 65536), + AISeed: Math.floor(Math.random() * 65536), CheatsEnabled: g_GameAttributes.settings.CheatsEnabled }; break; default: error(sprintf("selectMapType: Unexpected map type '%(mapType)s'", { mapType: g_GameAttributes.mapType })); return; } initMapNameList(); updateGameAttributes(); } function selectMapFilter(id) { // Avoid recursion if (g_IsInGuiUpdate) return; // Network clients can't change map filter if (g_IsNetworked && !g_IsController) return; g_GameAttributes.mapFilter = id; initMapNameList(); updateGameAttributes(); } // Called when the user selects a map from the list function selectMap(name) { // Avoid recursion if (g_IsInGuiUpdate) return; // Network clients can't change map if (g_IsNetworked && !g_IsController) return; // Return if we have no map if (!name) return; // reset some map specific properties which are not necessarily redefined on each map g_GameAttributes.settings.TriggerScripts = undefined; g_GameAttributes.settings.CircularMap = undefined; var mapData = loadMapData(name); var mapSettings = (mapData && mapData.settings ? deepcopy(mapData.settings) : {}); // Copy any new settings g_GameAttributes.map = name; g_GameAttributes.script = mapSettings.Script; if (g_GameAttributes.map !== "random") for (var prop in mapSettings) g_GameAttributes.settings[prop] = mapSettings[prop]; // Use default AI if the map doesn't specify any explicitly for (var i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i) { if (!('AI' in g_GameAttributes.settings.PlayerData[i])) g_GameAttributes.settings.PlayerData[i].AI = g_DefaultPlayerData[i].AI; if (!('AIDiff' in g_GameAttributes.settings.PlayerData[i])) g_GameAttributes.settings.PlayerData[i].AIDiff = g_DefaultPlayerData[i].AIDiff; } // Reset player assignments on map change if (!g_IsNetworked) { // Slot 1 g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1, "civ": "", "team": -1, "ready": 0} }; } else { var numPlayers = (mapSettings.PlayerData ? mapSettings.PlayerData.length : g_GameAttributes.settings.PlayerData.length); for (var guid in g_PlayerAssignments) { // Unassign extra players var player = g_PlayerAssignments[guid].player; if (player <= MAX_PLAYERS && player > numPlayers) Engine.AssignNetworkPlayer(player, ""); } } updateGameAttributes(); } function launchGame() { if (g_IsNetworked && !g_IsController) { error("Only host can start game"); return; } // Check that we have a map if (!g_GameAttributes.map) return; if (g_GameAttributes.map == "random") selectMap(Engine.GetGUIObjectByName("mapSelection").list_data[Math.floor(Math.random() * (Engine.GetGUIObjectByName("mapSelection").list.length - 1)) + 1]); if (!g_GameAttributes.settings.TriggerScripts) g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts; else g_GameAttributes.settings.TriggerScripts = g_GameAttributes.settings.VictoryScripts.concat(g_GameAttributes.settings.TriggerScripts); g_GameStarted = true; g_GameAttributes.settings.mapType = g_GameAttributes.mapType; var numPlayers = g_GameAttributes.settings.PlayerData.length; // Assign random civilizations to players with that choice // (this is synchronized because we're the host) var cultures = []; for each (var civ in g_CivData) if (civ.Culture !== undefined && cultures.indexOf(civ.Culture) < 0 && civ.SelectableInGameSetup !== false) cultures.push(civ.Culture); var allcivs = new Array(cultures.length); for (var i = 0; i < allcivs.length; ++i) allcivs[i] = []; for each (var civ in g_CivData) if (civ.Culture !== undefined && civ.SelectableInGameSetup !== false) allcivs[cultures.indexOf(civ.Culture)].push(civ.Code); const romanNumbers = [undefined, "I", "II", "III", "IV", "V", "VI", "VII", "VIII"]; for (var i = 0; i < numPlayers; ++i) { var civs = allcivs[Math.floor(Math.random()*allcivs.length)]; if (!g_GameAttributes.settings.PlayerData[i].Civ || g_GameAttributes.settings.PlayerData[i].Civ == "random") g_GameAttributes.settings.PlayerData[i].Civ = civs[Math.floor(Math.random()*civs.length)]; // Setting names for AI players. Check if the player is AI and the match is not a scenario if (g_GameAttributes.mapType !== "scenario" && g_GameAttributes.settings.PlayerData[i].AI) { // Get the civ specific names if (g_CivData[g_GameAttributes.settings.PlayerData[i].Civ].AINames !== undefined) var civAINames = shuffleArray(g_CivData[g_GameAttributes.settings.PlayerData[i].Civ].AINames); else var civAINames = [g_CivData[g_GameAttributes.settings.PlayerData[i].Civ].Name]; // Choose the name var usedName = 0; if (i < civAINames.length) var chosenName = civAINames[i]; else var chosenName = civAINames[Math.floor(Math.random() * civAINames.length)]; for (var j = 0; j < numPlayers; ++j) if (g_GameAttributes.settings.PlayerData[j].Name && g_GameAttributes.settings.PlayerData[j].Name.indexOf(chosenName) !== -1) usedName++; // Assign civ specific names to AI players chosenName = translate(chosenName); if (usedName) g_GameAttributes.settings.PlayerData[i].Name = sprintf(translate("%(playerName)s %(romanNumber)s"), { playerName: chosenName, romanNumber: romanNumbers[usedName+1]}); else g_GameAttributes.settings.PlayerData[i].Name = chosenName; } } if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); Engine.StartNetworkGame(); } else { // Find the player ID which the user has been assigned to var numPlayers = g_GameAttributes.settings.PlayerData.length; var playerID = -1; for (var i = 0; i < numPlayers; ++i) { var assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]"); if (assignBox.list_data[assignBox.selected] == "local") playerID = i+1; } // Remove extra player data g_GameAttributes.settings.PlayerData = g_GameAttributes.settings.PlayerData.slice(0, numPlayers); Engine.StartGame(g_GameAttributes, playerID); Engine.SwitchGuiPage("page_loading.xml", { "attribs": g_GameAttributes, "isNetworked" : g_IsNetworked, "playerAssignments": g_PlayerAssignments }); } } //////////////////////////////////////////////////////////////////////////////////////////////// function onGameAttributesChange() { g_IsInGuiUpdate = true; // Don't set any attributes here, just show the changes in GUI var mapName = g_GameAttributes.map || ""; var mapSettings = g_GameAttributes.settings; var numPlayers = (mapSettings.PlayerData ? mapSettings.PlayerData.length : MAX_PLAYERS); // Update some controls for clients if (!g_IsController) { var mapFilterSelection = Engine.GetGUIObjectByName("mapFilterSelection"); var mapFilterId = mapFilterSelection.list_data.indexOf(g_GameAttributes.mapFilter); Engine.GetGUIObjectByName("mapFilterText").caption = mapFilterSelection.list[mapFilterId]; var mapTypeSelection = Engine.GetGUIObjectByName("mapTypeSelection"); var idx = mapTypeSelection.list_data.indexOf(g_GameAttributes.mapType); Engine.GetGUIObjectByName("mapTypeText").caption = mapTypeSelection.list[idx]; var mapSelectionBox = Engine.GetGUIObjectByName("mapSelection"); mapSelectionBox.selected = mapSelectionBox.list_data.indexOf(mapName); Engine.GetGUIObjectByName("mapSelectionText").caption = translate(getMapDisplayName(mapName)); if (mapSettings.PopulationCap) { var populationCapBox = Engine.GetGUIObjectByName("populationCap"); populationCapBox.selected = populationCapBox.list_data.indexOf(mapSettings.PopulationCap); } if (mapSettings.StartingResources) { var startingResourcesBox = Engine.GetGUIObjectByName("startingResources"); startingResourcesBox.selected = startingResourcesBox.list_data.indexOf(mapSettings.StartingResources); } initMapNameList(); } // Controls common to all map types var numPlayersSelection = Engine.GetGUIObjectByName("numPlayersSelection"); var revealMap = Engine.GetGUIObjectByName("revealMap"); var exploreMap = Engine.GetGUIObjectByName("exploreMap"); var victoryCondition = Engine.GetGUIObjectByName("victoryCondition"); var lockTeams = Engine.GetGUIObjectByName("lockTeams"); var mapSize = Engine.GetGUIObjectByName("mapSize"); var enableCheats = Engine.GetGUIObjectByName("enableCheats"); var enableRating = Engine.GetGUIObjectByName("enableRating"); var populationCap = Engine.GetGUIObjectByName("populationCap"); var startingResources = Engine.GetGUIObjectByName("startingResources"); var numPlayersText= Engine.GetGUIObjectByName("numPlayersText"); var mapSizeDesc = Engine.GetGUIObjectByName("mapSizeDesc"); var mapSizeText = Engine.GetGUIObjectByName("mapSizeText"); var revealMapText = Engine.GetGUIObjectByName("revealMapText"); var exploreMapText = Engine.GetGUIObjectByName("exploreMapText"); var victoryConditionText = Engine.GetGUIObjectByName("victoryConditionText"); var lockTeamsText = Engine.GetGUIObjectByName("lockTeamsText"); var enableCheatsText = Engine.GetGUIObjectByName("enableCheatsText"); var enableRatingText = Engine.GetGUIObjectByName("enableRatingText"); var populationCapText = Engine.GetGUIObjectByName("populationCapText"); var startingResourcesText = Engine.GetGUIObjectByName("startingResourcesText"); var gameSpeedText = Engine.GetGUIObjectByName("gameSpeedText"); // We have to check for undefined on these properties as not all maps define them. var sizeIdx = (mapSettings.Size !== undefined && g_MapSizes.tiles.indexOf(mapSettings.Size) != -1 ? g_MapSizes.tiles.indexOf(mapSettings.Size) : g_MapSizes["default"]); var speedIdx = (g_GameAttributes.gameSpeed !== undefined && g_GameSpeeds.speeds.indexOf(g_GameAttributes.gameSpeed) != -1) ? g_GameSpeeds.speeds.indexOf(g_GameAttributes.gameSpeed) : g_GameSpeeds["default"]; var victories = getVictoryConditions(); var victoryIdx = (mapSettings.GameType !== undefined && victories.data.indexOf(mapSettings.GameType) != -1 ? victories.data.indexOf(mapSettings.GameType) : VICTORY_DEFAULTIDX); enableCheats.checked = (mapSettings.CheatsEnabled === undefined || !mapSettings.CheatsEnabled ? false : true) enableCheatsText.caption = (enableCheats.checked ? "Yes" : "No"); if (mapSettings.RatingEnabled !== undefined) { enableRating.checked = mapSettings.RatingEnabled; Engine.SetRankedGame(enableRating.checked); enableRatingText.caption = (enableRating.checked ? "Yes" : "No"); } else enableRatingText.caption = "Unknown"; gameSpeedText.caption = g_GameSpeeds.names[speedIdx]; populationCap.selected = (mapSettings.PopulationCap !== undefined && POPULATION_CAP_DATA.indexOf(mapSettings.PopulationCap) != -1 ? POPULATION_CAP_DATA.indexOf(mapSettings.PopulationCap) : POPULATION_CAP_DEFAULTIDX); populationCapText.caption = POPULATION_CAP[populationCap.selected]; startingResources.selected = (mapSettings.StartingResources !== undefined && STARTING_RESOURCES_DATA.indexOf(mapSettings.StartingResources) != -1 ? STARTING_RESOURCES_DATA.indexOf(mapSettings.StartingResources) : STARTING_RESOURCES_DEFAULTIDX); startingResourcesText.caption = STARTING_RESOURCES[startingResources.selected]; // Update map preview Engine.GetGUIObjectByName("mapPreview").sprite = "cropped:(0.78125,0.5859375)session/icons/mappreview/" + getMapPreview(mapName); // Handle map type specific logic switch (g_GameAttributes.mapType) { case "random": mapSizeDesc.hidden = false; if (g_IsController) { //Host numPlayersSelection.selected = numPlayers - 1; numPlayersSelection.hidden = false; mapSize.hidden = false; revealMap.hidden = false; exploreMap.hidden = false; victoryCondition.hidden = false; lockTeams.hidden = false; populationCap.hidden = false; startingResources.hidden = false; numPlayersText.hidden = true; mapSizeText.hidden = true; revealMapText.hidden = true; exploreMapText.hidden = true; victoryConditionText.hidden = true; lockTeamsText.hidden = true; populationCapText.hidden = true; startingResourcesText.hidden = true; mapSizeText.caption = translate("Map Size:"); mapSize.selected = sizeIdx; revealMapText.caption = translate("Revealed Map:"); exploreMapText.caption = translate("Explored Map:"); revealMap.checked = (mapSettings.RevealMap ? true : false); exploreMap.checked = (mapSettings.ExploreMap ? true : false); victoryConditionText.caption = translate("Victory Condition:"); victoryCondition.selected = victoryIdx; lockTeamsText.caption = translate("Teams Locked:"); lockTeams.checked = (mapSettings.LockTeams ? true : false); } else { // Client numPlayersText.hidden = false; mapSizeText.hidden = false; revealMapText.hidden = false; exploreMapText.hidden = false; victoryConditionText.hidden = false; lockTeamsText.hidden = false; populationCap.hidden = true; populationCapText.hidden = false; startingResources.hidden = true; startingResourcesText.hidden = false; numPlayersText.caption = numPlayers; mapSizeText.caption = g_MapSizes.names[sizeIdx]; revealMapText.caption = (mapSettings.RevealMap ? translate("Yes") : translate("No")); exploreMapText.caption = (mapSettings.ExporeMap ? translate("Yes") : translate("No")); victoryConditionText.caption = victories.text[victoryIdx]; lockTeamsText.caption = (mapSettings.LockTeams ? translate("Yes") : translate("No")); } break; case "skirmish": mapSizeText.caption = translate("Default"); numPlayersText.caption = numPlayers; numPlayersSelection.hidden = true; mapSize.hidden = true; mapSizeText.hidden = true; mapSizeDesc.hidden = true; if (g_IsController) { //Host revealMap.hidden = false; exploreMap.hidden = false; victoryCondition.hidden = false; lockTeams.hidden = false; populationCap.hidden = false; startingResources.hidden = false; numPlayersText.hidden = false; revealMapText.hidden = true; exploreMapText.hidden = true; victoryConditionText.hidden = true; lockTeamsText.hidden = true; populationCapText.hidden = true; startingResourcesText.hidden = true; revealMapText.caption = translate("Revealed Map:"); exploreMapText.caption = translate("Explored Map:"); revealMap.checked = (mapSettings.RevealMap ? true : false); exploreMap.checked = (mapSettings.ExploreMap ? true : false); victoryConditionText.caption = translate("Victory Condition:"); victoryCondition.selected = victoryIdx; lockTeamsText.caption = translate("Teams Locked:"); lockTeams.checked = (mapSettings.LockTeams ? true : false); } else { // Client numPlayersText.hidden = false; revealMapText.hidden = false; exploreMapText.hidden = false; victoryConditionText.hidden = false; lockTeamsText.hidden = false; populationCap.hidden = true; populationCapText.hidden = false; startingResources.hidden = true; startingResourcesText.hidden = false; revealMapText.caption = (mapSettings.RevealMap ? translate("Yes") : translate("No")); exploreMapText.caption = (mapSettings.ExploreMap ? translate("Yes") : translate("No")); victoryConditionText.caption = victories.text[victoryIdx]; lockTeamsText.caption = (mapSettings.LockTeams ? translate("Yes") : translate("No")); } break; case "scenario": // For scenario just reflect settings for the current map numPlayersSelection.hidden = true; mapSize.hidden = true; revealMap.hidden = true; exploreMap.hidden = true; victoryCondition.hidden = true; lockTeams.hidden = true; numPlayersText.hidden = false; mapSizeText.hidden = true; mapSizeDesc.hidden = true; revealMapText.hidden = false; exploreMapText.hidden = false; victoryConditionText.hidden = false; lockTeamsText.hidden = false; populationCap.hidden = true; populationCapText.hidden = false; startingResources.hidden = true; startingResourcesText.hidden = false; numPlayersText.caption = numPlayers; mapSizeText.caption = translate("Default"); revealMapText.caption = (mapSettings.RevealMap ? translate("Yes") : translate("No")); exploreMapText.caption = (mapSettings.ExploreMap ? translate("Yes") : translate("No")); victoryConditionText.caption = victories.text[victoryIdx]; lockTeamsText.caption = (mapSettings.LockTeams ? translate("Yes") : translate("No")); Engine.GetGUIObjectByName("populationCap").selected = POPULATION_CAP_DEFAULTIDX; break; default: error(sprintf("onGameAttributesChange: Unexpected map type '%(mapType)s'", { mapType: g_GameAttributes.mapType })); return; } // Display map name if (mapName == "random") { var mapDisplayName = translateWithContext("map", "Random"); mapSettings.Description = markForTranslation("Randomly selects a map from the list"); } else var mapDisplayName = translate(getMapDisplayName(mapName)); Engine.GetGUIObjectByName("mapInfoName").caption = mapDisplayName; // Load the description from the map file, if there is one var description = mapSettings.Description ? translate(mapSettings.Description) : translate("Sorry, no description available."); // Describe the number of players var playerString = sprintf(translatePlural("%(number)s player. %(description)s", "%(number)s players. %(description)s", numPlayers), { number: numPlayers, description: description }); for (var i = 0; i < MAX_PLAYERS; ++i) { // Show only needed player slots Engine.GetGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers); // Show player data or defaults as necessary if (i >= numPlayers) continue; var pName = Engine.GetGUIObjectByName("playerName["+i+"]"); var pAssignment = Engine.GetGUIObjectByName("playerAssignment["+i+"]"); var pAssignmentText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]"); var pCiv = Engine.GetGUIObjectByName("playerCiv["+i+"]"); var pCivText = Engine.GetGUIObjectByName("playerCivText["+i+"]"); var pTeam = Engine.GetGUIObjectByName("playerTeam["+i+"]"); var pTeamText = Engine.GetGUIObjectByName("playerTeamText["+i+"]"); var pColor = Engine.GetGUIObjectByName("playerColour["+i+"]"); // Player data / defaults var pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {}; var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {}; // Common to all game types var color = rgbToGuiColor(getSetting(pData, pDefs, "Colour")); pColor.sprite = "colour:" + color + " 100"; pName.caption = translate(getSetting(pData, pDefs, "Name")); var team = getSetting(pData, pDefs, "Team"); var civ = getSetting(pData, pDefs, "Civ"); // Nobody but the controller can assign people pAssignmentText.hidden = g_IsController; pAssignment.hidden = !g_IsController; if (!pAssignment.list[0]) pAssignmentText.caption = translate("Loading..."); else pAssignmentText.caption = pAssignment.list[pAssignment.selected === -1 ? 0 : pAssignment.selected]; // For clients or scenarios, hide some player dropdowns // TODO: Allow clients to choose their own civ and team if (!g_IsController || g_GameAttributes.mapType == "scenario") { pCivText.hidden = false; pCiv.hidden = true; pTeamText.hidden = false; pTeam.hidden = true; // Set text values if (civ == "random") pCivText.caption = "[color=\"orange\"]" + translateWithContext("civilization", "Random"); else pCivText.caption = g_CivData[civ].Name; pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-"; } else if (g_GameAttributes.mapType != "scenario") { pCivText.hidden = true; pCiv.hidden = false; pTeamText.hidden = true; pTeam.hidden = false; // Set dropdown values pCiv.selected = (civ ? pCiv.list_data.indexOf(civ) : 0); pTeam.selected = (team !== undefined && team >= 0) ? team+1 : 0; } } Engine.GetGUIObjectByName("mapInfoDescription").caption = playerString; g_IsInGuiUpdate = false; // Game attributes include AI settings, so update the player list updatePlayerList(); // We should have everyone confirm that the new settings are acceptable. resetReadyData(); } function updateGameAttributes() { if (g_IsNetworked) { Engine.SetNetworkGameAttributes(g_GameAttributes); if (g_IsController && g_LoadingState >= 2) sendRegisterGameStanza(); } else onGameAttributesChange(); } function AIConfigCallback(ai) { g_GameAttributes.settings.PlayerData[ai.playerSlot].AI = ai.id; g_GameAttributes.settings.PlayerData[ai.playerSlot].AIDiff = ai.difficulty; if (g_IsNetworked) Engine.SetNetworkGameAttributes(g_GameAttributes); else updatePlayerList(); } function updatePlayerList() { g_IsInGuiUpdate = true; var hostNameList = []; var hostGuidList = []; var assignments = []; var aiAssignments = {}; var noAssignment; g_AssignedCount = 0; for (var guid in g_PlayerAssignments) { var name = g_PlayerAssignments[guid].name; var hostID = hostNameList.length; var player = g_PlayerAssignments[guid].player; hostNameList.push(name); hostGuidList.push(guid); assignments[player] = hostID; if (player != -1) g_AssignedCount++; } // Only enable start button if we have enough assigned players if (g_IsController) Engine.GetGUIObjectByName("startGame").enabled = (g_AssignedCount > 0); for each (var ai in g_AIs) { if (ai.data.hidden) { // If the map uses a hidden AI then don't hide it var usedByMap = false; for (var i = 0; i < MAX_PLAYERS; ++i) if (i < g_GameAttributes.settings.PlayerData.length && g_GameAttributes.settings.PlayerData[i].AI == ai.id) { usedByMap = true; break; } if (!usedByMap) continue; } // Give AI a different color so it stands out aiAssignments[ai.id] = hostNameList.length; hostNameList.push("[color=\"70 150 70 255\"]" + sprintf(translate("AI: %(ai)s"), { ai: translate(ai.data.name) })); hostGuidList.push("ai:" + ai.id); } noAssignment = hostNameList.length; hostNameList.push("[color=\"140 140 140 255\"]" + translate("Unassigned")); hostGuidList.push(""); for (var i = 0; i < MAX_PLAYERS; ++i) { let playerSlot = i; let playerID = i+1; // we don't show Gaia, so first slot is ID 1 var selection = assignments[playerID]; var configButton = Engine.GetGUIObjectByName("playerConfig["+i+"]"); configButton.hidden = true; // Look for valid player slots if (playerSlot >= g_GameAttributes.settings.PlayerData.length) continue; // If no human is assigned, look for an AI instead if (selection === undefined) { var aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI; if (aiId) { // Check for a valid AI if (aiId in aiAssignments) selection = aiAssignments[aiId]; else { g_GameAttributes.settings.PlayerData[playerSlot].AI = ""; warn(sprintf("AI \"%(id)s\" not present. Defaulting to unassigned.", { id: aiId })); } } if (!selection) selection = noAssignment; // Since no human is assigned, show the AI config button if (g_IsController) { configButton.hidden = false; configButton.onpress = function() { Engine.PushGuiPage("page_aiconfig.xml", { ais: g_AIs, id: g_GameAttributes.settings.PlayerData[playerSlot].AI, difficulty: g_GameAttributes.settings.PlayerData[playerSlot].AIDiff, callback: "AIConfigCallback", playerSlot: playerSlot // required by the callback function }); }; } } // There was a human, so make sure we don't have any AI left // over in their slot, if we're in charge of the attributes else if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI && g_GameAttributes.settings.PlayerData[playerSlot].AI != "") { g_GameAttributes.settings.PlayerData[playerSlot].AI = ""; if (g_IsNetworked) Engine.SetNetworkGameAttributes(g_GameAttributes); } var assignBox = Engine.GetGUIObjectByName("playerAssignment["+i+"]"); var assignBoxText = Engine.GetGUIObjectByName("playerAssignmentText["+i+"]"); assignBox.list = hostNameList; assignBox.list_data = hostGuidList; if (assignBox.selected != selection) assignBox.selected = selection; assignBoxText.caption = hostNameList[selection]; if (g_IsController) { assignBox.onselectionchange = function () { if (!g_IsInGuiUpdate) { var guid = hostGuidList[this.selected]; if (guid == "") { if (g_IsNetworked) // Unassign any host from this player slot Engine.AssignNetworkPlayer(playerID, ""); // Remove AI from this player slot g_GameAttributes.settings.PlayerData[playerSlot].AI = ""; } else if (guid.substr(0, 3) == "ai:") { if (g_IsNetworked) // Unassign any host from this player slot Engine.AssignNetworkPlayer(playerID, ""); // Set the AI for this player slot g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3); } else swapPlayers(guid, playerSlot); if (g_IsNetworked) Engine.SetNetworkGameAttributes(g_GameAttributes); else updatePlayerList(); updateReadyUI(); } }; } } g_IsInGuiUpdate = false; } function swapPlayers(guid, newSlot) { // Player slots are indexed from 0 as Gaia is omitted. var newPlayerID = newSlot + 1; var playerID = g_PlayerAssignments[guid].player; // Attempt to swap the player or AI occupying the target slot, // if any, into the slot this player is currently in. if (playerID != -1) { for (var i in g_PlayerAssignments) { // Move the player in the destination slot into the current slot. if (g_PlayerAssignments[i].player == newPlayerID) { if (g_IsNetworked) Engine.AssignNetworkPlayer(playerID, i); else g_PlayerAssignments[i].player = playerID; break; } } // Transfer the AI from the target slot to the current slot. g_GameAttributes.settings.PlayerData[playerID - 1].AI = g_GameAttributes.settings.PlayerData[newSlot].AI; } if (g_IsNetworked) Engine.AssignNetworkPlayer(newPlayerID, guid); else g_PlayerAssignments[guid].player = newPlayerID; // Remove AI from this player slot g_GameAttributes.settings.PlayerData[newSlot].AI = ""; } function submitChatInput() { var input = Engine.GetGUIObjectByName("chatInput"); var text = input.caption; if (text.length) { Engine.SendNetworkChat(text); input.caption = ""; } } function addChatMessage(msg) { var username = ""; if (msg.username) username = escapeText(msg.username); else if (msg.guid && g_PlayerAssignments[msg.guid]) username = escapeText(g_PlayerAssignments[msg.guid].name); var message = ""; if ("text" in msg && msg.text) message = escapeText(msg.text); // TODO: Maybe host should have distinct font/color? var color = "white"; if (msg.guid && g_PlayerAssignments[msg.guid] && g_PlayerAssignments[msg.guid].player != -1) { // Valid player who has been assigned - get player colour var player = g_PlayerAssignments[msg.guid].player - 1; var mapName = g_GameAttributes.map; var mapData = loadMapData(mapName); var mapSettings = (mapData && mapData.settings ? mapData.settings : {}); var pData = mapSettings.PlayerData ? mapSettings.PlayerData[player] : {}; var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[player] : {}; color = rgbToGuiColor(getSetting(pData, pDefs, "Colour")); } var formatted; switch (msg.type) { case "connect": var formattedUsername = '[color="'+ color +'"]' + username + '[/color]'; formatted = '[font="sans-bold-13"] ' + sprintf(translate("== %(message)s"), { message: sprintf(translate("%(username)s has joined"), { username: formattedUsername }) }) + '[/font]'; break; case "disconnect": var formattedUsername = '[color="'+ color +'"]' + username + '[/color]'; formatted = '[font="sans-bold-13"] ' + sprintf(translate("== %(message)s"), { message: sprintf(translate("%(username)s has left"), { username: formattedUsername }) }) + '[/font]'; break; case "message": var formattedUsername = '[color="'+ color +'"]' + username + '[/color]'; var formattedUsernamePrefix = '[font="sans-bold-13"]' + sprintf(translate("<%(username)s>"), { username: formattedUsername }) + '[/font]' formatted = sprintf(translate("%(username)s %(message)s"), { username: formattedUsernamePrefix, message: message }); break; case "ready": var formattedUsername = '[font="sans-bold-13"][color="'+ color +'"]' + username + '[/color][/font]' if (msg.ready) formatted = ' ' + sprintf(translate("* %(username)s is ready!"), { username: formattedUsername }); else formatted = ' ' + sprintf(translate("* %(username)s is not ready."), { username: formattedUsername }); break; case "settings": formatted = '[font="sans-bold-13"] ' + sprintf(translate("== %(message)s"), { message: translate('Game settings have been changed') }) + '[/font]'; break; default: error(sprintf("Invalid chat message '%(message)s'", { message: uneval(msg) })); return; } g_ChatMessages.push(formatted); Engine.GetGUIObjectByName("chatText").caption = g_ChatMessages.join("\n"); } function toggleMoreOptions() { Engine.GetGUIObjectByName("moreOptionsFade").hidden = !Engine.GetGUIObjectByName("moreOptionsFade").hidden; Engine.GetGUIObjectByName("moreOptions").hidden = !Engine.GetGUIObjectByName("moreOptions").hidden; } function toggleReady() { g_IsReady = !g_IsReady; if (g_IsReady) { Engine.SendNetworkReady(1); Engine.GetGUIObjectByName("startGame").caption = translate("I'm not ready"); Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you are not ready to play."); } else { Engine.SendNetworkReady(0); Engine.GetGUIObjectByName("startGame").caption = translate("I'm ready!"); Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you are ready to play!"); } } function updateReadyUI() { if (!g_IsNetworked) return; // Disabled for single-player games. var isAI = new Array(MAX_PLAYERS + 1); for (var i = 0; i < isAI.length; ++i) isAI[i] = true; var allReady = true; for (var guid in g_PlayerAssignments) { // We don't really care whether observers are ready. if (g_PlayerAssignments[guid].player == -1 || !g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1]) continue; var pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[g_PlayerAssignments[guid].player - 1] : {}; var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[g_PlayerAssignments[guid].player - 1] : {}; isAI[g_PlayerAssignments[guid].player] = false; if (g_PlayerAssignments[guid].status || !g_IsNetworked) Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = '[color="0 255 0"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]'; else { Engine.GetGUIObjectByName("playerName[" + (g_PlayerAssignments[guid].player - 1) + "]").caption = translate(getSetting(pData, pDefs, "Name")); allReady = false; } } // AIs are always ready. for (var playerid = 0; playerid < MAX_PLAYERS; playerid++) { if (!g_GameAttributes.settings.PlayerData[playerid]) continue; var pData = g_GameAttributes.settings.PlayerData ? g_GameAttributes.settings.PlayerData[playerid] : {}; var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[playerid] : {}; if (isAI[playerid + 1]) Engine.GetGUIObjectByName("playerName[" + playerid + "]").caption = '[color="0 255 0"]' + translate(getSetting(pData, pDefs, "Name")) + '[/color]'; } // The host is not allowed to start until everyone is ready. if (g_IsNetworked && g_IsController) { var startGameButton = Engine.GetGUIObjectByName("startGame"); startGameButton.enabled = allReady; // Add a explanation on to the tooltip if disabled. var disabledIndex = startGameButton.tooltip.indexOf('Disabled'); if (disabledIndex != -1 && allReady) startGameButton.tooltip = startGameButton.tooltip.substring(0, disabledIndex - 2); else if (disabledIndex == -1 && !allReady) startGameButton.tooltip = startGameButton.tooltip + " (Disabled until all players are ready)"; } } function resetReadyData() { if (g_GameStarted) return; if (g_ReadyChanged < 1) addChatMessage({ "type": "settings"}); else if (g_ReadyChanged == 2 && !g_ReadyInit) return; // duplicate calls on init else g_ReadyInit = false; g_ReadyChanged = 2; if (!g_IsNetworked) g_IsReady = true; else if (g_IsController) { Engine.ClearAllPlayerReady(); g_IsReady = true; Engine.SendNetworkReady(1); } else { g_IsReady = false; Engine.GetGUIObjectByName("startGame").caption = translate("I'm ready!"); Engine.GetGUIObjectByName("startGame").tooltip = translate("State that you accept the current settings and are ready to play!"); } } //////////////////////////////////////////////////////////////////////////////////////////////// // Basic map filters API // Add a new map list filter function addFilter(id, name, filterFunc) { if (filterFunc instanceof Object) { // Basic validity test var newFilter = {}; newFilter.id = id; newFilter.name = name; newFilter.filter = filterFunc; g_MapFilters.push(newFilter); } else { error(sprintf("Invalid map filter: %(name)s", { name: name })); } } // Get array of map filter IDs function getFilterIds() { var filters = []; for (var i = 0; i < g_MapFilters.length; ++i) filters.push(g_MapFilters[i].id); return filters; } // Get array of map filter names function getFilterNames() { var filters = []; for (var i = 0; i < g_MapFilters.length; ++i) filters.push(g_MapFilters[i].name); return filters; } // Test map filter on given map settings object function testFilter(id, mapSettings) { for (var i = 0; i < g_MapFilters.length; ++i) if (g_MapFilters[i].id == id) return g_MapFilters[i].filter(mapSettings); error(sprintf("Invalid map filter: %(id)s", { id: id })); return false; } // Test an array of keywords against a match array using AND logic function keywordTestAND(keywords, matches) { if (!keywords || !matches) return false; for (var m = 0; m < matches.length; ++m) if (keywords.indexOf(matches[m]) == -1) return false; return true; } // Test an array of keywords against a match array using OR logic function keywordTestOR(keywords, matches) { if (!keywords || !matches) return false; for (var m = 0; m < matches.length; ++m) if (keywords.indexOf(matches[m]) != -1) return true; return false; } function sendRegisterGameStanza() { if (!Engine.HasXmppClient()) return; var selectedMapSize = Engine.GetGUIObjectByName("mapSize").selected; var selectedVictoryCondition = Engine.GetGUIObjectByName("victoryCondition").selected; // Map sizes only apply to random maps. if (g_GameAttributes.mapType == "random") var mapSize = Engine.GetGUIObjectByName("mapSize").list_data[selectedMapSize]; else var mapSize = "Default"; var victoryCondition = Engine.GetGUIObjectByName("victoryCondition").list[selectedVictoryCondition]; var numberOfPlayers = Object.keys(g_PlayerAssignments).length; var players = [ assignment.name for each (assignment in g_PlayerAssignments) ].join(", "); var nbp = numberOfPlayers ? numberOfPlayers : 1; var tnbp = g_GameAttributes.settings.PlayerData.length; var gameData = { "name":g_ServerName, "mapName":g_GameAttributes.map, "niceMapName":getMapDisplayName(g_GameAttributes.map), "mapSize":mapSize, "mapType":g_GameAttributes.mapType, "victoryCondition":victoryCondition, "nbp":nbp, "tnbp":tnbp, "players":players }; Engine.SendRegisterGame(gameData); } function getVictoryConditions() { var r = {}; r.text = [translate("None")]; r.data = ["endless"]; r.scripts = [[]]; for (var vc in g_VictoryConditions) { r.data.push(vc); r.text.push(g_VictoryConditions[vc].name); r.scripts.push(g_VictoryConditions[vc].scripts); } return r; } Index: ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js =================================================================== --- ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 15972) +++ ps/trunk/binaries/data/mods/public/simulation/helpers/InitGame.js (revision 15973) @@ -1,59 +1,61 @@ function ReplaceSkirmishGlobals() { // This will be called after the map settings have been loaded, // before the simulation has started. // This is only called at the start of a new game, not when loading // a saved game. Engine.BroadcastMessage(MT_SkirmishReplace, {}); } function InitGame(settings) { // No settings when loading a map in Atlas, so do nothing if (!settings) return; if (settings.ExploreMap) { var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); if (cmpRangeManager) for (var i = 0; i < settings.PlayerData.length; i++) cmpRangeManager.ExploreAllTiles(i+1); } else { // Explore the map only inside the players' territory borders var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager); cmpRangeManager.ExploreTerritories(); } var cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager); var cmpAIManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_AIManager); for (var i = 0; i < settings.PlayerData.length; ++i) { var cmpPlayer = Engine.QueryInterface(cmpPlayerManager.GetPlayerByID(i+1), IID_Player); cmpPlayer.SetCheatsEnabled(!!settings.CheatsEnabled); if (settings.PlayerData[i] && settings.PlayerData[i].AI && settings.PlayerData[i].AI != "") { cmpAIManager.AddPlayer(settings.PlayerData[i].AI, i+1, +settings.PlayerData[i].AIDiff); cmpPlayer.SetAI(true); cmpPlayer.SetGatherRateMultiplier(+Math.max(0.5,(+settings.PlayerData[i].AIDiff+1)/3.0)); // Sandbox: 50%, easy: 66%, Medium: 100%, hard: 133%, very hard: 166% } if (settings.PopulationCap) cmpPlayer.SetMaxPopulation(settings.PopulationCap); if (settings.mapType !== "scenario" && settings.StartingResources) { var resourceCounts = cmpPlayer.GetResourceCounts(); var newResourceCounts = {}; for (var resouces in resourceCounts) newResourceCounts[resouces] = settings.StartingResources; cmpPlayer.SetResourceCounts(newResourceCounts); } } + let seed = settings.AISeed ? settings.AISeed : 0; + cmpAIManager.SetRNGSeed(seed); cmpAIManager.TryLoadSharedComponent(); cmpAIManager.RunGamestateInit(); } Engine.RegisterGlobal("ReplaceSkirmishGlobals", ReplaceSkirmishGlobals); Engine.RegisterGlobal("InitGame", InitGame); Index: ps/trunk/binaries/system/readme.txt =================================================================== --- ps/trunk/binaries/system/readme.txt (revision 15972) +++ ps/trunk/binaries/system/readme.txt (revision 15973) @@ -1,79 +1,80 @@ COMMAND LINE OPTIONS Basic gameplay: -autostart=... load a map instead of showing main menu (see below) -editor launch the Atlas scenario editor -mod=NAME start the game using NAME mod -quickstart load faster (disables audio and some system info logging) Autostart: --autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random --autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) --autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: easy, 3: very hard) --autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) +-autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random +-autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) +-autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: sandbox, 4: very hard) +-autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) +-autostart-aiseed=AISEED sets the seed used for the AI random generator (default 0, use -1 for random) Multiplayer: --autostart-playername=NAME sets local player NAME (default 'anonymous') --autostart-host sets multiplayer host mode --autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) --autostart-client=IP sets multiplayer client to join host at given IP address +-autostart-playername=NAME sets local player NAME (default 'anonymous') +-autostart-host sets multiplayer host mode +-autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) +-autostart-client=IP sets multiplayer client to join host at given IP address Random maps only: --autostart-seed=SEED sets random map SEED value (default 0, use -1 for random) --autostart-size=TILES sets random map size in TILES (default 192) --autostart-players=NUMBER sets NUMBER of players on random map (default 2) +-autostart-seed=SEED sets random map SEED value (default 0, use -1 for random) +-autostart-size=TILES sets random map size in TILES (default 192) +-autostart-players=NUMBER sets NUMBER of players on random map (default 2) Examples: 1) "Bob" will host a 2 player game on the Arcadia map: -autostart="scenarios/Arcadia 02" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra Configuration: -conf=KEY:VALUE set a config value -g=F set the gamma correction to 'F' (default 1.0) -nosound disable audio -noUserMod disable loading of the user mod -shadows enable shadows -vsync enable VSync, i.e. lock FPS to monitor refresh rate -xres=N set screen X resolution to 'N' -yres=N set screen Y resolution to 'N' Advanced / diagnostic: -dumpSchema creates a file entity.rng in the working directory, containing complete entity XML schema, used by various analysis tools -entgraph (disabled) -listfiles (disabled) -profile=NAME (disabled) -replay=PATH non-visual replay of a previous game, used for analysis purposes PATH is system path to commands.txt containing simulation log -writableRoot store runtime game data in root data directory (only use if you have write permissions on that directory) -ooslog dumps simulation state in binary and ASCII representations each turn, files created in sim_log within the game's log folder. NOTE: game will run much slower with this option! -serializationtest checks simulation state each turn for serialization errors; on test failure, error is displayed and logs created in oos_log within the game's log folder. NOTE: game will run much slower with this option! Windows-specific: -wQpcTscSafe allow timing via QueryPerformanceCounter despite the fact that it's using TSC and it may be unsafe. has no effect if a better timer (i.e. the HPET) is available. should only be specified if: - you are sure your system does not engage in thermal throttling (including STPCLK) OR - an "RDTSC patch" is installed this flag is also useful if all other alternatives are worse than a potentially risky or slightly broken TSC-based QPC. -wNoMahaf prevent any physical memory mapping or direct port I/O. this disables all ACPI-related code and thus some of the timer backends. specify this if problems are observed with one of the abovementioned subsystems. Archive builder: -archivebuild=PATH system PATH of the base directory containing mod data to be archived/precached specify all mods it depends on with -mod=NAME -archivebuild-output=PATH system PATH to output of the resulting .zip archive (use with archivebuild) -archivebuild-compress enable deflate compression in the .zip (no zip compression by default since it hurts compression of release packages) Index: ps/trunk/source/ps/GameSetup/GameSetup.cpp =================================================================== --- ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 15972) +++ ps/trunk/source/ps/GameSetup/GameSetup.cpp (revision 15973) @@ -1,1489 +1,1502 @@ /* Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "lib/app_hooks.h" #include "lib/config2.h" #include "lib/input.h" #include "lib/ogl.h" #include "lib/timer.h" #include "lib/external_libraries/libsdl.h" #include "lib/file/common/file_stats.h" #include "lib/res/h_mgr.h" #include "lib/res/graphics/cursor.h" #include "lib/sysdep/cursor.h" #include "lib/sysdep/cpu.h" #include "lib/sysdep/gfx.h" #include "lib/sysdep/os_cpu.h" #include "lib/tex/tex.h" #if OS_WIN #include "lib/sysdep/os/win/wversion.h" #endif #include "graphics/CinemaTrack.h" #include "graphics/FontMetrics.h" #include "graphics/GameView.h" #include "graphics/LightEnv.h" #include "graphics/MapReader.h" #include "graphics/MaterialManager.h" #include "graphics/TerrainTextureManager.h" #include "gui/GUI.h" #include "gui/GUIManager.h" #include "gui/scripting/ScriptFunctions.h" #include "i18n/L10n.h" #include "maths/MathUtil.h" #include "network/NetServer.h" #include "network/NetClient.h" #include "ps/CConsole.h" #include "ps/CLogger.h" #include "ps/ConfigDB.h" #include "ps/Filesystem.h" #include "ps/Game.h" #include "ps/GameSetup/Atlas.h" #include "ps/GameSetup/GameSetup.h" #include "ps/GameSetup/Paths.h" #include "ps/GameSetup/Config.h" #include "ps/GameSetup/CmdLineArgs.h" #include "ps/GameSetup/HWDetect.h" #include "ps/Globals.h" #include "ps/Hotkey.h" #include "ps/Joystick.h" #include "ps/Loader.h" #include "ps/Mod.h" #include "ps/Overlay.h" #include "ps/Profile.h" #include "ps/ProfileViewer.h" #include "ps/Profiler2.h" #include "ps/Pyrogenesis.h" // psSetLogDir #include "ps/scripting/JSInterface_Console.h" #include "ps/TouchInput.h" #include "ps/UserReport.h" #include "ps/Util.h" #include "ps/VideoMode.h" #include "ps/World.h" #include "renderer/Renderer.h" #include "renderer/VertexBufferManager.h" #include "renderer/ModelRenderer.h" #include "scriptinterface/DebuggingServer.h" #include "scriptinterface/ScriptInterface.h" #include "scriptinterface/ScriptStats.h" #include "simulation2/Simulation2.h" #include "lobby/IXmppClient.h" #include "soundmanager/scripting/JSInterface_Sound.h" #include "soundmanager/ISoundManager.h" #include "tools/atlas/GameInterface/GameLoop.h" #include "tools/atlas/GameInterface/View.h" #if !(OS_WIN || OS_MACOSX || OS_ANDROID) // assume all other platforms use X11 for wxWidgets #define MUST_INIT_X11 1 #include #else #define MUST_INIT_X11 0 #endif #if OS_WIN extern void wmi_Shutdown(); #endif extern void restart_engine(); #include #include #include ERROR_GROUP(System); ERROR_TYPE(System, SDLInitFailed); ERROR_TYPE(System, VmodeFailed); ERROR_TYPE(System, RequiredExtensionsMissing); bool g_DoRenderGui = true; bool g_DoRenderLogger = true; bool g_DoRenderCursor = true; shared_ptr g_ScriptRuntime; static const int SANE_TEX_QUALITY_DEFAULT = 5; // keep in sync with code bool g_InDevelopmentCopy; bool g_CheckedIfInDevelopmentCopy = false; static void SetTextureQuality(int quality) { int q_flags; GLint filter; retry: // keep this in sync with SANE_TEX_QUALITY_DEFAULT switch(quality) { // worst quality case 0: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_NEAREST; break; // [perf] add bilinear filtering case 1: q_flags = OGL_TEX_HALF_RES|OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] no longer reduce resolution case 2: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR; break; // [vmem] add mipmaps case 3: q_flags = OGL_TEX_HALF_BPP; filter = GL_NEAREST_MIPMAP_LINEAR; break; // [perf] better filtering case 4: q_flags = OGL_TEX_HALF_BPP; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [vmem] no longer reduce bpp case SANE_TEX_QUALITY_DEFAULT: q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // [perf] add anisotropy case 6: // TODO: add anisotropic filtering q_flags = OGL_TEX_FULL_QUALITY; filter = GL_LINEAR_MIPMAP_LINEAR; break; // invalid default: debug_warn(L"SetTextureQuality: invalid quality"); quality = SANE_TEX_QUALITY_DEFAULT; // careful: recursion doesn't work and we don't want to duplicate // the "sane" default values. goto retry; } ogl_tex_set_defaults(q_flags, filter); } //---------------------------------------------------------------------------- // GUI integration //---------------------------------------------------------------------------- // display progress / description in loading screen void GUI_DisplayLoadProgress(int percent, const wchar_t* pending_task) { g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_Progress", percent, true); g_GUI->GetActiveGUI()->GetScriptInterface()->SetGlobal("g_LoadDescription", pending_task, true); g_GUI->GetActiveGUI()->SendEventToAll("progress"); } void Render() { PROFILE3("render"); if (g_SoundManager) g_SoundManager->IdleTask(); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameStart(); ogl_WarnIfError(); // prepare before starting the renderer frame if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->BeginFrame(); if (g_Game) g_Renderer.SetSimulation(g_Game->GetSimulation2()); // start new frame g_Renderer.BeginFrame(); ogl_WarnIfError(); if (g_Game && g_Game->IsGameStarted()) g_Game->GetView()->Render(); ogl_WarnIfError(); g_Renderer.RenderTextOverlays(); if (g_DoRenderGui) g_GUI->Draw(); ogl_WarnIfError(); // If we're in Atlas game view, render special overlays (e.g. editor bandbox) if (g_AtlasGameLoop && g_AtlasGameLoop->view) { g_AtlasGameLoop->view->DrawOverlays(); ogl_WarnIfError(); } // Text: glDisable(GL_DEPTH_TEST); g_Console->Render(); ogl_WarnIfError(); if (g_DoRenderLogger) g_Logger->Render(); ogl_WarnIfError(); // Profile information g_ProfileViewer.RenderProfile(); ogl_WarnIfError(); // Draw the cursor (or set the Windows cursor, on Windows) if (g_DoRenderCursor) { PROFILE3_GPU("cursor"); CStrW cursorName = g_CursorName; if (cursorName.empty()) { cursor_draw(g_VFS, NULL, g_mouse_x, g_yres-g_mouse_y, false); } else { bool forceGL = false; CFG_GET_VAL("nohwcursor", Bool, forceGL); #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // set up transform for GL cursor glMatrixMode(GL_PROJECTION); glPushMatrix(); glLoadIdentity(); glMatrixMode(GL_MODELVIEW); glPushMatrix(); glLoadIdentity(); CMatrix3D transform; transform.SetOrtho(0.f, (float)g_xres, 0.f, (float)g_yres, -1.f, 1000.f); glLoadMatrixf(&transform._11); #endif #if OS_ANDROID #warning TODO: cursors for Android #else if (cursor_draw(g_VFS, cursorName.c_str(), g_mouse_x, g_yres-g_mouse_y, forceGL) < 0) LOGWARNING(L"Failed to draw cursor '%ls'", cursorName.c_str()); #endif #if CONFIG2_GLES #warning TODO: implement cursors for GLES #else // restore transform glMatrixMode(GL_PROJECTION); glPopMatrix(); glMatrixMode(GL_MODELVIEW); glPopMatrix(); #endif } } glEnable(GL_DEPTH_TEST); g_Renderer.EndFrame(); PROFILE2_ATTR("draw calls: %d", (int)g_Renderer.GetStats().m_DrawCalls); PROFILE2_ATTR("terrain tris: %d", (int)g_Renderer.GetStats().m_TerrainTris); PROFILE2_ATTR("water tris: %d", (int)g_Renderer.GetStats().m_WaterTris); PROFILE2_ATTR("model tris: %d", (int)g_Renderer.GetStats().m_ModelTris); PROFILE2_ATTR("overlay tris: %d", (int)g_Renderer.GetStats().m_OverlayTris); PROFILE2_ATTR("blend splats: %d", (int)g_Renderer.GetStats().m_BlendSplats); PROFILE2_ATTR("particles: %d", (int)g_Renderer.GetStats().m_Particles); ogl_WarnIfError(); g_Profiler2.RecordGPUFrameEnd(); ogl_WarnIfError(); } static size_t OperatingSystemFootprint() { #if OS_WIN switch(wversion_Number()) { case WVERSION_2K: case WVERSION_XP: return 150; case WVERSION_XP64: return 200; default: // newer Windows version: assume the worst, and don't warn case WVERSION_VISTA: return 300; case WVERSION_7: return 250; } #else return 200; #endif } static size_t ChooseCacheSize() { // (all sizes in MiB and signed to allow temporarily negative computations) const ssize_t total = (ssize_t)os_cpu_MemorySize(); // (NB: os_cpu_MemoryAvailable is useless on Linux because free memory // is marked as "in use" by OS caches.) const ssize_t os = (ssize_t)OperatingSystemFootprint(); const ssize_t game = 300; // estimated working set ssize_t cache = 400; // upper bound: total size of our data // the cache reserves contiguous address space, which is a precious // resource on 32-bit systems, so don't use too much: if(ARCH_IA32 || sizeof(void*) == 4) cache = std::min(cache, (ssize_t)200); // try to leave over enough memory for the OS and game cache = std::min(cache, total-os-game); // always provide at least this much to ensure correct operation cache = std::max(cache, (ssize_t)64); debug_printf(L"Cache: %d (total: %d) MiB\n", (int)cache, (int)total); return size_t(cache)*MiB; } ErrorReactionInternal psDisplayError(const wchar_t* UNUSED(text), size_t UNUSED(flags)) { // If we're fullscreen, then sometimes (at least on some particular drivers on Linux) // displaying the error dialog hangs the desktop since the dialog box is behind the // fullscreen window. So we just force the game to windowed mode before displaying the dialog. // (But only if we're in the main thread, and not if we're being reentrant.) if (ThreadUtil::IsMainThread()) { static bool reentering = false; if (!reentering) { reentering = true; g_VideoMode.SetFullscreen(false); reentering = false; } } // We don't actually implement the error display here, so return appropriately return ERI_NOT_IMPLEMENTED; } std::vector& GetMods(const CmdLineArgs& args, int flags) { const bool init_mods = (flags & INIT_MODS) == INIT_MODS; const bool add_user = !InDevelopmentCopy() && !args.Has("noUserMod"); const bool add_public = (flags & INIT_MODS_PUBLIC) == INIT_MODS_PUBLIC; if (!init_mods) { // Add the user mod if it should be present if (add_user && (g_modsLoaded.empty() || g_modsLoaded.back() != "user")) g_modsLoaded.push_back("user"); return g_modsLoaded; } g_modsLoaded = args.GetMultiple("mod"); if (add_public) g_modsLoaded.insert(g_modsLoaded.begin(), "public"); g_modsLoaded.insert(g_modsLoaded.begin(), "mod"); // Add the user mod if not explicitly disabled or we have a dev copy so // that saved files end up in version control and not in the user mod. if (add_user) g_modsLoaded.push_back("user"); return g_modsLoaded; } void MountMods(const Paths& paths, const std::vector& mods) { OsPath modPath = paths.RData()/"mods"; OsPath modUserPath = paths.UserData()/"mods"; for (size_t i = 0; i < mods.size(); ++i) { size_t priority = (i+1)*2; // mods are higher priority than regular mountings, which default to priority 0 size_t userFlags = VFS_MOUNT_WATCH|VFS_MOUNT_ARCHIVABLE|VFS_MOUNT_REPLACEABLE; size_t baseFlags = userFlags|VFS_MOUNT_MUST_EXIST; OsPath modName(mods[i]); if (InDevelopmentCopy()) { // We are running a dev copy, so only mount mods in the user mod path // if the mod does not exist in the data path. if (DirectoryExists(modPath / modName/"")) g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); else g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority); } else { g_VFS->Mount(L"", modPath / modName/"", baseFlags, priority); // Ensure that user modified files are loaded, if they are present g_VFS->Mount(L"", modUserPath / modName/"", userFlags, priority+1); } } } static void InitVfs(const CmdLineArgs& args, int flags) { TIMER(L"InitVfs"); const bool setup_error = (flags & INIT_HAVE_DISPLAY_ERROR) == 0; const Paths paths(args); OsPath logs(paths.Logs()); CreateDirectories(logs, 0700); psSetLogDir(logs); // desired location for crashlog is now known. update AppHooks ASAP // (particularly before the following error-prone operations): AppHooks hooks = {0}; hooks.bundle_logs = psBundleLogs; hooks.get_log_dir = psLogDir; if (setup_error) hooks.display_error = psDisplayError; app_hooks_update(&hooks); const size_t cacheSize = ChooseCacheSize(); g_VFS = CreateVfs(cacheSize); const OsPath readonlyConfig = paths.RData()/"config"/""; g_VFS->Mount(L"config/", readonlyConfig); // Engine localization files. g_VFS->Mount(L"l10n/", paths.RData()/"l10n"/""); MountMods(paths, GetMods(args, flags)); // We mount these dirs last as otherwise writing could result in files being placed in a mod's dir. g_VFS->Mount(L"screenshots/", paths.UserData()/"screenshots"/""); g_VFS->Mount(L"saves/", paths.UserData()/"saves"/"", VFS_MOUNT_WATCH); // Mounting with highest priority, so that a mod supplied user.cfg is harmless g_VFS->Mount(L"config/", readonlyConfig, 0, (size_t)-1); if(readonlyConfig != paths.Config()) g_VFS->Mount(L"config/", paths.Config(), 0, (size_t)-1); g_VFS->Mount(L"cache/", paths.Cache(), VFS_MOUNT_ARCHIVABLE); // (adding XMBs to archive speeds up subsequent reads) // note: don't bother with g_VFS->TextRepresentation - directories // haven't yet been populated and are empty. } static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcScriptInterface, JS::HandleValue initData) { { // console TIMER(L"ps_console"); g_Console->UpdateScreenSize(g_xres, g_yres); // Calculate and store the line spacing CFontMetrics font(CStrIntern(CONSOLE_FONT)); g_Console->m_iFontHeight = font.GetLineSpacing(); g_Console->m_iFontWidth = font.GetCharacterWidth(L'C'); g_Console->m_charsPerPage = (size_t)(g_xres / g_Console->m_iFontWidth); // Offset by an arbitrary amount, to make it fit more nicely g_Console->m_iFontOffset = 7; double blinkRate = 0.5; CFG_GET_VAL("gui.cursorblinkrate", Double, blinkRate); g_Console->SetCursorBlinkRate(blinkRate); } // hotkeys { TIMER(L"ps_lang_hotkeys"); LoadHotkeys(); } if (!setup_gui) { // We do actually need *some* kind of GUI loaded, so use the // (currently empty) Atlas one g_GUI->SwitchPage(L"page_atlas.xml", srcScriptInterface, initData); return; } // GUI uses VFS, so this must come after VFS init. g_GUI->SwitchPage(gui_page, srcScriptInterface, initData); } static void InitInput() { #if !SDL_VERSION_ATLEAST(2, 0, 0) SDL_EnableKeyRepeat(SDL_DEFAULT_REPEAT_DELAY, SDL_DEFAULT_REPEAT_INTERVAL); #endif g_Joystick.Initialise(); // register input handlers // This stack is constructed so the first added, will be the last // one called. This is important, because each of the handlers // has the potential to block events to go further down // in the chain. I.e. the last one in the list added, is the // only handler that can block all messages before they are // processed. in_add_handler(game_view_handler); in_add_handler(CProfileViewer::InputThunk); in_add_handler(conInputHandler); in_add_handler(HotkeyInputHandler); // gui_handler needs to be registered after (i.e. called before!) the // hotkey handler so that input boxes can be typed in without // setting off hotkeys. in_add_handler(gui_handler); in_add_handler(touch_input_handler); // must be registered after (called before) the GUI which relies on these globals in_add_handler(GlobalsInputHandler); } static void ShutdownPs() { SAFE_DELETE(g_GUI); UnloadHotkeys(); // disable the special Windows cursor, or free textures for OGL cursors cursor_draw(g_VFS, 0, g_mouse_x, g_yres-g_mouse_y, false); } static void InitRenderer() { TIMER(L"InitRenderer"); if(g_NoGLS3TC) ogl_tex_override(OGL_TEX_S3TC, OGL_TEX_DISABLE); if(g_NoGLAutoMipmap) ogl_tex_override(OGL_TEX_AUTO_MIPMAP_GEN, OGL_TEX_DISABLE); // create renderer new CRenderer; // set renderer options from command line options - NOVBO must be set before opening the renderer g_Renderer.SetOptionBool(CRenderer::OPT_NOVBO, g_NoGLVBO); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWS, g_Shadows); g_Renderer.SetOptionBool(CRenderer::OPT_WATERUGLY, g_WaterUgly); g_Renderer.SetOptionBool(CRenderer::OPT_WATERFANCYEFFECTS, g_WaterFancyEffects); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREALDEPTH, g_WaterRealDepth); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFLECTION, g_WaterReflection); g_Renderer.SetOptionBool(CRenderer::OPT_WATERREFRACTION, g_WaterRefraction); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWSONWATER, g_WaterShadows); g_Renderer.SetRenderPath(CRenderer::GetRenderPathByName(g_RenderPath)); g_Renderer.SetOptionBool(CRenderer::OPT_SHADOWPCF, g_ShadowPCF); g_Renderer.SetOptionBool(CRenderer::OPT_PARTICLES, g_Particles); g_Renderer.SetOptionBool(CRenderer::OPT_SILHOUETTES, g_Silhouettes); g_Renderer.SetOptionBool(CRenderer::OPT_SHOWSKY, g_ShowSky); // create terrain related stuff new CTerrainTextureManager; g_Renderer.Open(g_xres, g_yres); // Setup lighting environment. Since the Renderer accesses the // lighting environment through a pointer, this has to be done before // the first Frame. g_Renderer.SetLightEnv(&g_LightEnv); // I haven't seen the camera affecting GUI rendering and such, but the // viewport has to be updated according to the video mode SViewPort vp; vp.m_X = 0; vp.m_Y = 0; vp.m_Width = g_xres; vp.m_Height = g_yres; g_Renderer.SetViewport(vp); ColorActivateFastImpl(); ModelRenderer::Init(); } static void InitSDL() { #if OS_LINUX // In fullscreen mode when SDL is compiled with DGA support, the mouse // sensitivity often appears to be unusably wrong (typically too low). // (This seems to be reported almost exclusively on Ubuntu, but can be // reproduced on Gentoo after explicitly enabling DGA.) // Disabling the DGA mouse appears to fix that problem, and doesn't // have any obvious negative effects. setenv("SDL_VIDEO_X11_DGAMOUSE", "0", 0); #endif if(SDL_Init(SDL_INIT_VIDEO|SDL_INIT_TIMER|SDL_INIT_NOPARACHUTE) < 0) { LOGERROR(L"SDL library initialization failed: %hs", SDL_GetError()); throw PSERROR_System_SDLInitFailed(); } atexit(SDL_Quit); #if !SDL_VERSION_ATLEAST(2, 0, 0) SDL_EnableUNICODE(1); #endif } static void ShutdownSDL() { SDL_Quit(); sys_cursor_reset(); } void EndGame() { SAFE_DELETE(g_NetClient); SAFE_DELETE(g_NetServer); SAFE_DELETE(g_Game); ISoundManager::CloseGame(); } void Shutdown(int flags) { if ((flags & SHUTDOWN_FROM_CONFIG)) goto from_config; EndGame(); SAFE_DELETE(g_XmppClient); ShutdownPs(); TIMER_BEGIN(L"shutdown TexMan"); delete &g_TexMan; TIMER_END(L"shutdown TexMan"); // destroy renderer TIMER_BEGIN(L"shutdown Renderer"); delete &g_Renderer; g_VBMan.Shutdown(); TIMER_END(L"shutdown Renderer"); g_Profiler2.ShutdownGPU(); #if OS_WIN TIMER_BEGIN(L"shutdown wmi"); wmi_Shutdown(); TIMER_END(L"shutdown wmi"); #endif // Free cursors before shutting down SDL, as they may depend on SDL. cursor_shutdown(); TIMER_BEGIN(L"shutdown SDL"); ShutdownSDL(); TIMER_END(L"shutdown SDL"); g_VideoMode.Shutdown(); TIMER_BEGIN(L"shutdown UserReporter"); g_UserReporter.Deinitialize(); TIMER_END(L"shutdown UserReporter"); // JS debugger temporarily disabled during the SpiderMonkey upgrade (check trac ticket #2348 for details) //TIMER_BEGIN(L"shutdown DebuggingServer (if active)"); //delete g_DebuggingServer; //TIMER_END(L"shutdown DebuggingServer (if active)"); delete &g_L10n; from_config: TIMER_BEGIN(L"shutdown ConfigDB"); delete &g_ConfigDB; TIMER_END(L"shutdown ConfigDB"); SAFE_DELETE(g_Console); // This is needed to ensure that no callbacks from the JSAPI try to use // the profiler when it's already destructed g_ScriptRuntime.reset(); // resource // first shut down all resource owners, and then the handle manager. TIMER_BEGIN(L"resource modules"); ISoundManager::SetEnabled(false); g_VFS.reset(); // this forcibly frees all open handles (thus preventing real leaks), // and makes further access to h_mgr impossible. h_mgr_shutdown(); file_stats_dump(); TIMER_END(L"resource modules"); TIMER_BEGIN(L"shutdown misc"); timer_DisplayClientTotals(); CNetHost::Deinitialize(); SAFE_DELETE(g_ScriptStatsTable); // should be last, since the above use them SAFE_DELETE(g_Logger); delete &g_Profiler; delete &g_ProfileViewer; TIMER_END(L"shutdown misc"); } #if OS_UNIX static void FixLocales() { #if OS_MACOSX || OS_BSD // OS X requires a UTF-8 locale in LC_CTYPE so that *wprintf can handle // wide characters. Peculiarly the string "UTF-8" seems to be acceptable // despite not being a real locale, and it's conveniently language-agnostic, // so use that. setlocale(LC_CTYPE, "UTF-8"); #endif // On misconfigured systems with incorrect locale settings, we'll die // with a C++ exception when some code (e.g. Boost) tries to use locales. // To avoid death, we'll detect the problem here and warn the user and // reset to the default C locale. // For informing the user of the problem, use the list of env vars that // glibc setlocale looks at. (LC_ALL is checked first, and LANG last.) const char* const LocaleEnvVars[] = { "LC_ALL", "LC_COLLATE", "LC_CTYPE", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LC_MESSAGES", "LANG" }; try { // this constructor is similar to setlocale(LC_ALL, ""), // but instead of returning NULL, it throws runtime_error // when the first locale env variable found contains an invalid value std::locale(""); } catch (std::runtime_error&) { LOGWARNING(L"Invalid locale settings"); for (size_t i = 0; i < ARRAY_SIZE(LocaleEnvVars); i++) { if (char* envval = getenv(LocaleEnvVars[i])) LOGWARNING(L" %hs=\"%hs\"", LocaleEnvVars[i], envval); else LOGWARNING(L" %hs=\"(unset)\"", LocaleEnvVars[i]); } // We should set LC_ALL since it overrides LANG if (setenv("LC_ALL", std::locale::classic().name().c_str(), 1)) debug_warn(L"Invalid locale settings, and unable to set LC_ALL env variable."); else LOGWARNING(L"Setting LC_ALL env variable to: %hs", getenv("LC_ALL")); } } #else static void FixLocales() { // Do nothing on Windows } #endif void EarlyInit() { // If you ever want to catch a particular allocation: //_CrtSetBreakAlloc(232647); ThreadUtil::SetMainThread(); debug_SetThreadName("main"); // add all debug_printf "tags" that we are interested in: debug_filter_add(L"TIMER"); timer_LatchStartTime(); // initialise profiler early so it can profile startup, // but only after LatchStartTime g_Profiler2.Initialise(); FixLocales(); // Because we do GL calls from a secondary thread, Xlib needs to // be told to support multiple threads safely. // This is needed for Atlas, but we have to call it before any other // Xlib functions (e.g. the ones used when drawing the main menu // before launching Atlas) #if MUST_INIT_X11 int status = XInitThreads(); if (status == 0) debug_printf(L"Error enabling thread-safety via XInitThreads\n"); #endif // Initialise the low-quality rand function srand(time(NULL)); // NOTE: this rand should *not* be used for simulation! } bool Autostart(const CmdLineArgs& args); bool Init(const CmdLineArgs& args, int flags) { h_mgr_init(); // Do this as soon as possible, because it chdirs // and will mess up the error reporting if anything // crashes before the working directory is set. InitVfs(args, flags); // This must come after VFS init, which sets the current directory // (required for finding our output log files). g_Logger = new CLogger; // Using a global object for the runtime is a workaround until Simulation and AI use // their own threads and also their own runtimes. const int runtimeSize = 384 * 1024 * 1024; const int heapGrowthBytesGCTrigger = 20 * 1024 * 1024; g_ScriptRuntime = ScriptInterface::CreateRuntime(runtimeSize, heapGrowthBytesGCTrigger); // Special command-line mode to dump the entity schemas instead of running the game. // (This must be done after loading VFS etc, but should be done before wasting time // on anything else.) if (args.Has("dumpSchema")) { CSimulation2 sim(NULL, g_ScriptRuntime, NULL); sim.LoadDefaultScripts(); std::ofstream f("entity.rng", std::ios_base::out | std::ios_base::trunc); f << sim.GenerateSchema(); std::cout << "Generated entity.rng\n"; exit(0); } // override ah_translate with our i18n code. AppHooks hooks = {0}; hooks.translate = psTranslate; hooks.translate_free = psTranslateFree; app_hooks_update(&hooks); // Set up the console early, so that debugging // messages can be logged to it. (The console's size // and fonts are set later in InitPs()) g_Console = new CConsole(); CNetHost::Initialize(); new CProfileViewer; new CProfileManager; // before any script code g_ScriptStatsTable = new CScriptStatsTable; g_ProfileViewer.AddRootTable(g_ScriptStatsTable); #if CONFIG2_AUDIO ISoundManager::CreateSoundManager(); #endif // g_ConfigDB, command line args, globals CONFIG_Init(args); // Check if there are mods specified on the command line, // or if we already set the mods (~INIT_MODS), // else check if there are mods that should be loaded specified // in the config and load those (by aborting init and restarting // the engine). if (!args.Has("mod") && (flags & INIT_MODS) == INIT_MODS) { CStr modstring; CFG_GET_VAL("mod.enabledmods", String, modstring); if (!modstring.empty()) { std::vector mods; boost::split(mods, modstring, boost::is_any_of(" "), boost::token_compress_on); std::swap(g_modsLoaded, mods); // Abort init and restart restart_engine(); return false; } } new L10n; // before scripting // JS debugger temporarily disabled during the SpiderMonkey upgrade (check trac ticket #2348 for details) //if (g_JSDebuggerEnabled) // g_DebuggingServer = new CDebuggingServer(); // Optionally start profiler HTTP output automatically // (By default it's only enabled by a hotkey, for security/performance) bool profilerHTTPEnable = false; CFG_GET_VAL("profiler2.autoenable", Bool, profilerHTTPEnable); if (profilerHTTPEnable) g_Profiler2.EnableHTTP(); if (!g_Quickstart) g_UserReporter.Initialize(); // after config PROFILE2_EVENT("Init finished"); return true; } void InitGraphics(const CmdLineArgs& args, int flags) { const bool setup_vmode = (flags & INIT_HAVE_VMODE) == 0; if(setup_vmode) { InitSDL(); if (!g_VideoMode.InitSDL()) throw PSERROR_System_VmodeFailed(); // abort startup #if !SDL_VERSION_ATLEAST(2, 0, 0) SDL_WM_SetCaption("0 A.D.", "0 A.D."); #endif } RunHardwareDetection(); const int quality = SANE_TEX_QUALITY_DEFAULT; // TODO: set value from config file SetTextureQuality(quality); ogl_WarnIfError(); // Optionally start profiler GPU timings automatically // (By default it's only enabled by a hotkey, for performance/compatibility) bool profilerGPUEnable = false; CFG_GET_VAL("profiler2.autoenable", Bool, profilerGPUEnable); if (profilerGPUEnable) g_Profiler2.EnableGPU(); if(!g_Quickstart) { WriteSystemInfo(); // note: no longer vfs_display here. it's dog-slow due to unbuffered // file output and very rarely needed. } if(g_DisableAudio) ISoundManager::SetEnabled(false); g_GUI = new CGUIManager(); // (must come after SetVideoMode, since it calls ogl_Init) if (ogl_HaveExtensions(0, "GL_ARB_vertex_program", "GL_ARB_fragment_program", NULL) != 0 // ARB && ogl_HaveExtensions(0, "GL_ARB_vertex_shader", "GL_ARB_fragment_shader", NULL) != 0) // GLSL { DEBUG_DISPLAY_ERROR( L"Your graphics card doesn't appear to be fully compatible with OpenGL shaders." L" In the future, the game will not support pre-shader graphics cards." L" You are advised to try installing newer drivers and/or upgrade your graphics card." L" For more information, please see http://www.wildfiregames.com/forum/index.php?showtopic=16734" ); // TODO: actually quit once fixed function support is dropped } const char* missing = ogl_HaveExtensions(0, "GL_ARB_multitexture", "GL_EXT_draw_range_elements", "GL_ARB_texture_env_combine", "GL_ARB_texture_env_dot3", NULL); if(missing) { wchar_t buf[500]; swprintf_s(buf, ARRAY_SIZE(buf), L"The %hs extension doesn't appear to be available on your computer." L" The game may still work, though - you are welcome to try at your own risk." L" If not or it doesn't look right, upgrade your graphics card.", missing ); DEBUG_DISPLAY_ERROR(buf); // TODO: i18n } if (!ogl_HaveExtension("GL_ARB_texture_env_crossbar")) { DEBUG_DISPLAY_ERROR( L"The GL_ARB_texture_env_crossbar extension doesn't appear to be available on your computer." L" Shadows are not available and overall graphics quality might suffer." L" You are advised to try installing newer drivers and/or upgrade your graphics card."); g_Shadows = false; } ogl_WarnIfError(); InitRenderer(); InitInput(); ogl_WarnIfError(); try { if (!Autostart(args)) { const bool setup_gui = ((flags & INIT_NO_GUI) == 0); // We only want to display the splash screen at startup shared_ptr scriptInterface = g_GUI->GetScriptInterface(); JSContext* cx = scriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue data(cx); if (g_GUI) { scriptInterface->Eval("({})", &data); scriptInterface->SetProperty(data, "isStartup", true); } InitPs(setup_gui, L"page_pregame.xml", g_GUI->GetScriptInterface().get(), data); } } catch (PSERROR_Game_World_MapLoadFailed& e) { // Map Loading failed // Start the engine so we have a GUI InitPs(true, L"page_pregame.xml", NULL, JS::UndefinedHandleValue); // Call script function to do the actual work // (delete game data, switch GUI page, show error, etc.) CancelLoad(CStr(e.what()).FromUTF8()); } } void RenderGui(bool RenderingState) { g_DoRenderGui = RenderingState; } void RenderLogger(bool RenderingState) { g_DoRenderLogger = RenderingState; } void RenderCursor(bool RenderingState) { g_DoRenderCursor = RenderingState; } /** * Temporarily loads a scenario map and retrieves the "ScriptSettings" JSON * data from it. * The scenario map format is used for scenario and skirmish map types (random * games do not use a "map" (format) but a small JavaScript program which * creates a map on the fly). It contains a section to initialize the game * setup screen. * @param mapPath Absolute path (from VFS root) to the map file to peek in. * @return ScriptSettings in JSON format extracted from the map. */ CStr8 LoadSettingsOfScenarioMap(const VfsPath &mapPath) { CXeromyces mapFile; const char *pathToSettings[] = { "Scenario", "ScriptSettings", "" // Path to JSON data in map }; Status loadResult = mapFile.Load(g_VFS, mapPath); if (INFO::OK != loadResult) { LOGERROR(L"LoadSettingsOfScenarioMap: Unable to load map file '%ls'", mapPath.string().c_str()); throw PSERROR_Game_World_MapLoadFailed("Unable to load map file, check the path for typos."); } XMBElement mapElement = mapFile.GetRoot(); // Select the ScriptSettings node in the map file... for (int i = 0; pathToSettings[i][0]; i++) { int childId = mapFile.GetElementID(pathToSettings[i]); XMBElementList children = mapElement.GetChildNodes(); for (int childIndex = 0; childIndex < children.Count; childIndex++) { XMBElement child = children.Item(childIndex); if (child.GetNodeName() == childId) { mapElement = child; break; } } } // ... they contain a JSON document to initialize the game setup // screen return mapElement.GetText(); } /* * Command line options for autostart (keep synchronized with readme.txt): * - * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random - * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) - * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: easy, 3: very hard) - * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) + * -autostart="TYPEDIR/MAPNAME" enables autostart and sets MAPNAME; TYPEDIR is skirmishes, scenarios, or random + * -autostart-ai=PLAYER:AI sets the AI for PLAYER (e.g. 2:petra) + * -autostart-aidiff=PLAYER:DIFF sets the DIFFiculty of PLAYER's AI (0: sandbox, 4: very hard) + * -autostart-civ=PLAYER:CIV sets PLAYER's civilisation to CIV (skirmish and random maps only) + * -autostart-aiseed=AISEED sets the seed used for the AI random generator (default 0, use -1 for random) * Multiplayer: * -autostart-playername=NAME sets local player NAME (default 'anonymous') - * -autostart-host sets multiplayer host mode - * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) - * -autostart-client=IP sets multiplayer client to join host at given IP address + * -autostart-host sets multiplayer host mode + * -autostart-host-players=NUMBER sets NUMBER of human players for multiplayer game (default 2) + * -autostart-client=IP sets multiplayer client to join host at given IP address * Random maps only: - * -autostart-seed=SEED sets random map SEED value (default 0, use -1 for random) - * -autostart-size=TILES sets random map size in TILES (default 192) - * -autostart-players=NUMBER sets NUMBER of players on random map (default 2) + * -autostart-seed=SEED sets random map SEED value (default 0, use -1 for random) + * -autostart-size=TILES sets random map size in TILES (default 192) + * -autostart-players=NUMBER sets NUMBER of players on random map (default 2) * * Examples: * 1) "Bob" will host a 2 player game on the Arcadia map: * -autostart="scenarios/Arcadia 02" -autostart-host -autostart-host-players=2 -autostart-playername="Bob" * 2) Load Alpine Lakes random map with random seed, 2 players (Athens and Britons), and player 2 is PetraBot: * -autostart="random/alpine_lakes" -autostart-seed=-1 -autostart-players=2 -autostart-civ=1:athen -autostart-civ=2:brit -autostart-ai=2:petra */ bool Autostart(const CmdLineArgs& args) { CStr autoStartName = args.Get("autostart"); #if OS_ANDROID // HACK: currently the most convenient way to test maps on Android; // should find a better solution autoStartName = "scenarios/Arcadia 02"; #endif if (autoStartName.empty()) return false; g_Game = new CGame(); ScriptInterface& scriptInterface = g_Game->GetSimulation2()->GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue attrs(cx); scriptInterface.Eval("({})", &attrs); JS::RootedValue settings(cx); scriptInterface.Eval("({})", &settings); JS::RootedValue playerData(cx); scriptInterface.Eval("([])", &playerData); // The directory in front of the actual map name indicates which type // of map is being loaded. Drawback of this approach is the association // of map types and folders is hard-coded, but benefits are: // - No need to pass the map type via command line separately // - Prevents mixing up of scenarios and skirmish maps to some degree Path mapPath = Path(autoStartName); std::wstring mapDirectory = mapPath.Parent().Filename().string(); std::string mapType; if (mapDirectory == L"random") { // Default seed is 0 u32 seed = 0; CStr seedArg = args.Get("autostart-seed"); if (!seedArg.empty()) { // Random seed value - if (seedArg != "-1") + if (seedArg == "-1") seed = rand(); else seed = seedArg.ToULong(); } // Random map definition will be loaded from JSON file, so we need to parse it std::wstring scriptPath = L"maps/" + autoStartName.FromUTF8() + L".json"; JS::RootedValue scriptData(cx); scriptInterface.ReadJSONFile(scriptPath, &scriptData); if (!scriptData.isUndefined() && scriptInterface.GetProperty(scriptData, "settings", &settings)) { // JSON loaded ok - copy script name over to game attributes std::wstring scriptFile; scriptInterface.GetProperty(settings, "Script", scriptFile); scriptInterface.SetProperty(attrs, "script", scriptFile); // RMS filename } else { // Problem with JSON file LOGERROR(L"Autostart: Error reading random map script '%ls'", scriptPath.c_str()); throw PSERROR_Game_World_MapLoadFailed("Error reading random map script.\nCheck application log for details."); } // Get optional map size argument (default 192) uint mapSize = 192; if (args.Has("autostart-size")) { CStr size = args.Get("autostart-size"); mapSize = size.ToUInt(); } scriptInterface.SetProperty(settings, "Seed", seed); // Random seed scriptInterface.SetProperty(settings, "Size", mapSize); // Random map size (in patches) // Get optional number of players (default 2) size_t numPlayers = 2; if (args.Has("autostart-players")) { CStr num = args.Get("autostart-players"); numPlayers = num.ToUInt(); } // Set up player data for (size_t i = 0; i < numPlayers; ++i) { JS::RootedValue player(cx); scriptInterface.Eval("({})", &player); // We could load player_defaults.json here, but that would complicate the logic // even more and autostart is only intended for developers anyway scriptInterface.SetProperty(player, "Civ", std::string("athen")); scriptInterface.SetPropertyInt(playerData, i, player); } mapType = "random"; } else if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // Initialize general settings from the map data so some values // (e.g. name of map) are always present, even when autostart is // partially configured CStr8 mapSettingsJSON = LoadSettingsOfScenarioMap("maps/" + autoStartName + ".xml"); scriptInterface.ParseJSON(mapSettingsJSON, &settings); // Initialize the playerData array being modified by autostart // with the real map data, so sensible values are present: scriptInterface.GetProperty(settings, "PlayerData", &playerData); if (mapDirectory == L"scenarios") mapType = "scenario"; else mapType = "skirmish"; } else { LOGERROR(L"Autostart: Unrecognized map type '%ls'", mapDirectory.c_str()); throw PSERROR_Game_World_MapLoadFailed("Unrecognized map type.\nConsult readme.txt for the currently supported types."); } scriptInterface.SetProperty(attrs, "mapType", mapType); scriptInterface.SetProperty(attrs, "map", std::string("maps/" + autoStartName)); scriptInterface.SetProperty(settings, "mapType", mapType); + // Set seed for AIs + u32 aiseed = 0; + if (args.Has("autostart-aiseed")) + { + CStr seedArg = args.Get("autostart-aiseed"); + if (seedArg == "-1") + aiseed = rand(); + else + aiseed = seedArg.ToULong(); + } + scriptInterface.SetProperty(settings, "AISeed", aiseed); + // Set player data for AIs // attrs.settings = { PlayerData: [ { AI: ... }, ... ] }: if (args.Has("autostart-ai")) { std::vector aiArgs = args.GetMultiple("autostart-ai"); for (size_t i = 0; i < aiArgs.size(); ++i) { int playerID = aiArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING(L"Autostart: Invalid player %d in autostart-ai option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = aiArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "AI", std::string(name)); scriptInterface.SetProperty(player, "AIDiff", 2); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } // Set AI difficulty if (args.Has("autostart-aidiff")) { std::vector civArgs = args.GetMultiple("autostart-aidiff"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"scenarios" || mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING(L"Autostart: Invalid player %d in autostart-aidiff option", playerID); continue; } scriptInterface.Eval("({})", &player); } int difficulty = civArgs[i].AfterFirst(":").ToInt(); scriptInterface.SetProperty(player, "AIDiff", difficulty); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } // Set player data for Civs if (args.Has("autostart-civ")) { if (mapDirectory != L"scenarios") { std::vector civArgs = args.GetMultiple("autostart-civ"); for (size_t i = 0; i < civArgs.size(); ++i) { int playerID = civArgs[i].BeforeFirst(":").ToInt(); // Instead of overwriting existing player data, modify the array JS::RootedValue player(cx); if (!scriptInterface.GetPropertyInt(playerData, playerID-1, &player) || player.isUndefined()) { if (mapDirectory == L"skirmishes") { // playerID is certainly bigger than this map player number LOGWARNING(L"Autostart: Invalid player %d in autostart-civ option", playerID); continue; } scriptInterface.Eval("({})", &player); } CStr name = civArgs[i].AfterFirst(":"); scriptInterface.SetProperty(player, "Civ", std::string(name)); scriptInterface.SetPropertyInt(playerData, playerID-1, player); } } else LOGWARNING(L"Autostart: Option 'autostart-civ' is invalid for scenarios"); } // Add player data to map settings scriptInterface.SetProperty(settings, "PlayerData", playerData); // Add map settings to game attributes scriptInterface.SetProperty(attrs, "settings", settings); JS::RootedValue mpInitData(cx); scriptInterface.Eval("({isNetworked:true, playerAssignments:{}})", &mpInitData); scriptInterface.SetProperty(mpInitData, "attribs", attrs); // Get optional playername CStrW userName = L"anonymous"; if (args.Has("autostart-playername")) { userName = args.Get("autostart-playername").FromUTF8(); } if (args.Has("autostart-host")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); size_t maxPlayers = 2; if (args.Has("autostart-host-players")) maxPlayers = args.Get("autostart-host-players").ToUInt(); g_NetServer = new CNetServer(maxPlayers); g_NetServer->UpdateGameAttributes(&attrs, scriptInterface); bool ok = g_NetServer->SetupConnection(); ENSURE(ok); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); g_NetClient->SetupConnection("127.0.0.1"); } else if (args.Has("autostart-client")) { InitPs(true, L"page_loading.xml", &scriptInterface, mpInitData); g_NetClient = new CNetClient(g_Game); g_NetClient->SetUserName(userName); CStr ip = args.Get("autostart-client"); if (ip.empty()) ip = "127.0.0.1"; bool ok = g_NetClient->SetupConnection(ip); ENSURE(ok); } else { g_Game->SetPlayerID(1); g_Game->StartGame(CScriptValRooted(cx, attrs), ""); LDR_NonprogressiveLoad(); PSRETURN ret = g_Game->ReallyStartGame(); ENSURE(ret == PSRETURN_OK); InitPs(true, L"page_session.xml", NULL, JS::UndefinedHandleValue); } return true; } void CancelLoad(const CStrW& message) { shared_ptr pScriptInterface = g_GUI->GetActiveGUI()->GetScriptInterface(); JSContext* cx = pScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue global(cx, pScriptInterface->GetGlobalObject()); // Cancel loader LDR_Cancel(); // Call the cancelOnError GUI function, defined in ..gui/common/functions_utility_error.js // So all GUI pages that load games should include this script if (g_GUI && g_GUI->HasPages()) { if (pScriptInterface->HasProperty(global, "cancelOnError" )) pScriptInterface->CallFunctionVoid(global, "cancelOnError", message); } } bool InDevelopmentCopy() { if (!g_CheckedIfInDevelopmentCopy) { g_InDevelopmentCopy = (g_VFS->GetFileInfo(L"config/dev.cfg", NULL) == INFO::OK); g_CheckedIfInDevelopmentCopy = true; } return g_InDevelopmentCopy; } Index: ps/trunk/source/simulation2/components/CCmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 15972) +++ ps/trunk/source/simulation2/components/CCmpAIManager.cpp (revision 15973) @@ -1,1115 +1,1124 @@ /* Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "simulation2/system/Component.h" #include "ICmpAIManager.h" #include "simulation2/MessageTypes.h" #include "graphics/Terrain.h" #include "lib/timer.h" #include "lib/tex/tex.h" #include "lib/allocators/shared_ptr.h" #include "ps/CLogger.h" #include "ps/Filesystem.h" #include "ps/Util.h" #include "simulation2/components/ICmpAIInterface.h" #include "simulation2/components/ICmpCommandQueue.h" #include "simulation2/components/ICmpObstructionManager.h" #include "simulation2/components/ICmpRangeManager.h" #include "simulation2/components/ICmpTemplateManager.h" #include "simulation2/components/ICmpTechnologyTemplateManager.h" #include "simulation2/components/ICmpTerritoryManager.h" #include "simulation2/helpers/Grid.h" #include "simulation2/serialization/DebugSerializer.h" #include "simulation2/serialization/StdDeserializer.h" #include "simulation2/serialization/StdSerializer.h" #include "simulation2/serialization/SerializeTemplates.h" /** * @file * Player AI interface. * AI is primarily scripted, and the CCmpAIManager component defined here * takes care of managing all the scripts. * * To avoid slow AI scripts causing jerky rendering, they are run in a background * thread (maintained by CAIWorker) so that it's okay if they take a whole simulation * turn before returning their results (though preferably they shouldn't use nearly * that much CPU). * * CCmpAIManager grabs the world state after each turn (making use of AIInterface.js * and AIProxy.js to decide what data to include) then passes it to CAIWorker. * The AI scripts will then run asynchronously and return a list of commands to execute. * Any attempts to read the command list (including indirectly via serialization) * will block until it's actually completed, so the rest of the engine should avoid * reading it for as long as possible. * * JS values are passed between the game and AI threads using ScriptInterface::StructuredClone. * * TODO: actually the thread isn't implemented yet, because performance hasn't been * sufficiently problematic to justify the complexity yet, but the CAIWorker interface * is designed to hopefully support threading when we want it. */ /** * Implements worker thread for CCmpAIManager. */ class CAIWorker { private: class CAIPlayer { NONCOPYABLE(CAIPlayer); public: CAIPlayer(CAIWorker& worker, const std::wstring& aiName, player_id_t player, uint8_t difficulty, shared_ptr scriptInterface) : m_Worker(worker), m_AIName(aiName), m_Player(player), m_Difficulty(difficulty), m_ScriptInterface(scriptInterface) { } ~CAIPlayer() { // Clean up rooted objects before destroying their script context m_Obj = CScriptValRooted(); m_Commands.clear(); } bool Initialise() { // LoadScripts will only load each script once even though we call it for each player if (!m_Worker.LoadScripts(m_AIName)) return false; JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); OsPath path = L"simulation/ai/" + m_AIName + L"/data.json"; JS::RootedValue metadata(cx); m_Worker.LoadMetadata(path, &metadata); if (metadata.isUndefined()) { LOGERROR(L"Failed to create AI player: can't find %ls", path.string().c_str()); return false; } // Get the constructor name from the metadata // If the AI doesn't use modules, we look for the constructor in the global object // TODO: All AIs should use modules. Remove the condition if this requirement is met. std::string moduleName; std::string constructor; JS::RootedValue objectWithConstructor(cx); // object that should contain the constructor function JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); JS::RootedValue ctor(cx); if (!m_ScriptInterface->HasProperty(metadata, "moduleName")) { objectWithConstructor.set(m_ScriptInterface->GetGlobalObject()); } else { m_ScriptInterface->GetProperty(metadata, "moduleName", moduleName); if(!m_ScriptInterface->GetProperty(global, moduleName.c_str(), &objectWithConstructor) || objectWithConstructor.isUndefined()) { LOGERROR(L"Failed to create AI player: %ls: can't find the module that should contain the constructor: '%hs'", path.string().c_str(), moduleName.c_str()); return false; } } if (!m_ScriptInterface->GetProperty(metadata, "constructor", constructor)) { LOGERROR(L"Failed to create AI player: %ls: missing 'constructor'", path.string().c_str()); return false; } // Get the constructor function from the loaded scripts if (!m_ScriptInterface->GetProperty(objectWithConstructor, constructor.c_str(), &ctor) || ctor.isNull()) { LOGERROR(L"Failed to create AI player: %ls: can't find constructor '%hs'", path.string().c_str(), constructor.c_str()); return false; } m_ScriptInterface->GetProperty(metadata, "useShared", m_UseSharedComponent); JS::RootedValue obj(cx); // Set up the data to pass as the constructor argument JS::RootedValue settings(cx); m_ScriptInterface->Eval(L"({})", &settings); m_ScriptInterface->SetProperty(settings, "player", m_Player, false); m_ScriptInterface->SetProperty(settings, "difficulty", m_Difficulty, false); ENSURE(m_Worker.m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(settings, "templates", m_Worker.m_EntityTemplates, false); JS::AutoValueVector argv(cx); argv.append(settings.get()); m_ScriptInterface->CallConstructor(ctor, argv, &obj); if (obj.isNull()) { LOGERROR(L"Failed to create AI player: %ls: error calling constructor '%hs'", path.string().c_str(), constructor.c_str()); return false; } m_Obj = CScriptValRooted(cx, obj); return true; } void Run(JS::HandleValue state, int playerID) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue tmpObj(cx, m_Obj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(tmpObj, "HandleMessage", state, playerID); } // overloaded with a sharedAI part. // javascript can handle both natively on the same function. void Run(JS::HandleValue state, int playerID, CScriptValRooted SharedAI) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue tmpObj(cx, m_Obj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(tmpObj, "HandleMessage", state, playerID, SharedAI); } void InitAI(JS::HandleValue state, CScriptValRooted SharedAI) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue tmpObj(cx, m_Obj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_Commands.clear(); m_ScriptInterface->CallFunctionVoid(tmpObj, "Init", state, m_Player, SharedAI); } CAIWorker& m_Worker; std::wstring m_AIName; player_id_t m_Player; uint8_t m_Difficulty; bool m_UseSharedComponent; shared_ptr m_ScriptInterface; CScriptValRooted m_Obj; std::vector > m_Commands; }; public: struct SCommandSets { player_id_t player; std::vector > commands; }; CAIWorker() : m_ScriptInterface(new ScriptInterface("Engine", "AI", g_ScriptRuntime)), m_TurnNum(0), m_CommandsComputed(true), m_HasLoadedEntityTemplates(false), m_HasSharedComponent(false) { - // TODO: ought to seed the RNG (in a network-synchronised way) before we use it m_ScriptInterface->ReplaceNondeterministicRNG(m_RNG); m_ScriptInterface->LoadGlobalScripts(); m_ScriptInterface->SetCallbackData(static_cast (this)); m_ScriptInterface->RegisterFunction("PostCommand"); m_ScriptInterface->RegisterFunction("IncludeModule"); m_ScriptInterface->RegisterFunction("DumpHeap"); m_ScriptInterface->RegisterFunction("ForceGC"); m_ScriptInterface->RegisterFunction, u32, u32, u32, CAIWorker::DumpImage>("DumpImage"); } ~CAIWorker() { // Clear rooted script values before destructing the script interface m_EntityTemplates = CScriptValRooted(); m_PlayerMetadata.clear(); m_Players.clear(); m_GameState.reset(); m_PassabilityMapVal = CScriptValRooted(); m_TerritoryMapVal = CScriptValRooted(); } bool LoadScripts(const std::wstring& moduleName) { // Ignore modules that are already loaded if (m_LoadedModules.find(moduleName) != m_LoadedModules.end()) return true; // Mark this as loaded, to prevent it recursively loading itself m_LoadedModules.insert(moduleName); // Load and execute *.js VfsPaths pathnames; if (vfs::GetPathnames(g_VFS, L"simulation/ai/" + moduleName + L"/", L"*.js", pathnames) < 0) { LOGERROR(L"Failed to load AI scripts for module %ls", moduleName.c_str()); return false; } for (VfsPaths::iterator it = pathnames.begin(); it != pathnames.end(); ++it) { if (!m_ScriptInterface->LoadGlobalScriptFile(*it)) { LOGERROR(L"Failed to load script %ls", it->string().c_str()); return false; } } return true; } static void IncludeModule(ScriptInterface::CxPrivate* pCxPrivate, std::wstring name) { ENSURE(pCxPrivate->pCBData); CAIWorker* self = static_cast (pCxPrivate->pCBData); self->LoadScripts(name); } static void PostCommand(ScriptInterface::CxPrivate* pCxPrivate, int playerid, CScriptValRooted cmd1) { ENSURE(pCxPrivate->pCBData); CAIWorker* self = static_cast (pCxPrivate->pCBData); JSContext* cx = pCxPrivate->pScriptInterface->GetContext(); JSAutoRequest rq(cx); // TODO: Get Handle parameter directly with SpiderMonkey 31 JS::RootedValue cmd(cx, cmd1.get()); self->PostCommand(playerid, cmd); } void PostCommand(int playerid, JS::HandleValue cmd) { for (size_t i=0; im_Player == playerid) { m_Players[i]->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(cmd)); return; } } LOGERROR(L"Invalid playerid in PostCommand!"); } // The next two ought to be implmeneted someday but for now as it returns "null" it can't static void DumpHeap(ScriptInterface::CxPrivate* pCxPrivate) { pCxPrivate->pScriptInterface->DumpHeap(); } static void ForceGC(ScriptInterface::CxPrivate* pCxPrivate) { PROFILE3("AI compute GC"); JS_GC(pCxPrivate->pScriptInterface->GetJSRuntime()); } /** * Debug function for AI scripts to dump 2D array data (e.g. terrain tile weights). */ static void DumpImage(ScriptInterface::CxPrivate* UNUSED(pCxPrivate), std::wstring name, std::vector data, u32 w, u32 h, u32 max) { // TODO: this is totally not threadsafe. VfsPath filename = L"screenshots/aidump/" + name; if (data.size() != w*h) { debug_warn(L"DumpImage: data size doesn't match w*h"); return; } if (max == 0) { debug_warn(L"DumpImage: max must not be 0"); return; } const size_t bpp = 8; int flags = TEX_BOTTOM_UP|TEX_GREY; const size_t img_size = w * h * bpp/8; const size_t hdr_size = tex_hdr_size(filename); shared_ptr buf; AllocateAligned(buf, hdr_size+img_size, maxSectorSize); Tex t; if (t.wrap(w, h, bpp, flags, buf, hdr_size) < 0) return; u8* img = buf.get() + hdr_size; for (size_t i = 0; i < data.size(); ++i) img[i] = (u8)((data[i] * 255) / max); tex_write(&t, filename); } + void SetRNGSeed(uint32_t seed) + { + m_RNG.seed(seed); + } + bool TryLoadSharedComponent(bool hasTechs) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); // we don't need to load it. if (!m_HasSharedComponent) return false; // reset the value so it can be used to determine if we actually initialized it. m_HasSharedComponent = false; if (LoadScripts(L"common-api")) m_HasSharedComponent = true; else return false; // mainly here for the error messages OsPath path = L"simulation/ai/common-api/"; // Constructor name is SharedScript, it's in the module API3 // TODO: Hardcoding this is bad, we need a smarter way. JS::RootedValue AIModule(cx); JS::RootedValue global(cx, m_ScriptInterface->GetGlobalObject()); JS::RootedValue ctor(cx); if (!m_ScriptInterface->GetProperty(global, "API3", &AIModule) || AIModule.isUndefined()) { LOGERROR(L"Failed to create shared AI component: %ls: can't find module '%hs'", path.string().c_str(), "API3"); return false; } if (!m_ScriptInterface->GetProperty(AIModule, "SharedScript", &ctor) || ctor.isUndefined()) { LOGERROR(L"Failed to create shared AI component: %ls: can't find constructor '%hs'", path.string().c_str(), "SharedScript"); return false; } // Set up the data to pass as the constructor argument JS::RootedValue settings(cx); m_ScriptInterface->Eval(L"({})", &settings); JS::RootedValue playersID(cx); m_ScriptInterface->Eval(L"({})", &playersID); for (size_t i = 0; i < m_Players.size(); ++i) { JS::RootedValue val(cx); m_ScriptInterface->ToJSVal(cx, &val, m_Players[i]->m_Player); m_ScriptInterface->SetPropertyInt(playersID, i, val, true); } m_ScriptInterface->SetProperty(settings, "players", playersID); ENSURE(m_HasLoadedEntityTemplates); m_ScriptInterface->SetProperty(settings, "templates", m_EntityTemplates, false); if (hasTechs) { m_ScriptInterface->SetProperty(settings, "techTemplates", m_TechTemplates, false); } else { // won't get the tech templates directly. JS::RootedValue fakeTech(cx); m_ScriptInterface->Eval("({})", &fakeTech); m_ScriptInterface->SetProperty(settings, "techTemplates", fakeTech, false); } JS::AutoValueVector argv(cx); argv.append(settings); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade JS::RootedValue tmpSharedAIObj(cx, m_SharedAIObj.get()); m_ScriptInterface->CallConstructor(ctor, argv, &tmpSharedAIObj); m_SharedAIObj = CScriptValRooted(cx, tmpSharedAIObj); if (tmpSharedAIObj.isNull()) { LOGERROR(L"Failed to create shared AI component: %ls: error calling constructor '%hs'", path.string().c_str(), "SharedScript"); return false; } return true; } bool AddPlayer(const std::wstring& aiName, player_id_t player, uint8_t difficulty) { shared_ptr ai(new CAIPlayer(*this, aiName, player, difficulty, m_ScriptInterface)); if (!ai->Initialise()) return false; // this will be set to true if we need to load the shared Component. if (!m_HasSharedComponent) m_HasSharedComponent = ai->m_UseSharedComponent; m_Players.push_back(ai); return true; } bool RunGamestateInit(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap) { // this will be run last by InitGame.Js, passing the full game representation. // For now it will run for the shared Component. // This is NOT run during deserialization. JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue state(cx); m_ScriptInterface->ReadStructuredClone(gameState, &state); JS::RootedValue tmpVal(cx); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade ScriptInterface::ToJSVal(cx, &tmpVal, passabilityMap); m_PassabilityMapVal = CScriptValRooted(cx, tmpVal.get()); ScriptInterface::ToJSVal(cx, &tmpVal, territoryMap); m_TerritoryMapVal = CScriptValRooted(cx, tmpVal); if (m_HasSharedComponent) { m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); JS::RootedValue tmpSharedAIObj(cx, m_SharedAIObj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_ScriptInterface->CallFunctionVoid(tmpSharedAIObj, "init", state); for (size_t i = 0; i < m_Players.size(); ++i) { if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->InitAI(state, m_SharedAIObj); } } return true; } void StartComputation(const shared_ptr& gameState, const Grid& passabilityMap, const Grid& territoryMap, bool territoryMapDirty) { ENSURE(m_CommandsComputed); m_GameState = gameState; JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); if (passabilityMap.m_DirtyID != m_PassabilityMap.m_DirtyID) { m_PassabilityMap = passabilityMap; JS::RootedValue tmpVal(cx); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade ScriptInterface::ToJSVal(cx, &tmpVal, m_PassabilityMap); m_PassabilityMapVal = CScriptValRooted(cx, tmpVal); } if (territoryMapDirty) { m_TerritoryMap = territoryMap; JS::RootedValue tmpVal(cx); ScriptInterface::ToJSVal(cx, &tmpVal, m_TerritoryMap); m_TerritoryMapVal = CScriptValRooted(cx, tmpVal); } m_CommandsComputed = false; } void WaitToFinishComputation() { if (!m_CommandsComputed) { PerformComputation(); m_CommandsComputed = true; } } void GetCommands(std::vector& commands) { WaitToFinishComputation(); commands.clear(); commands.resize(m_Players.size()); for (size_t i = 0; i < m_Players.size(); ++i) { commands[i].player = m_Players[i]->m_Player; commands[i].commands = m_Players[i]->m_Commands; } } void RegisterTechTemplates(const shared_ptr& techTemplates) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue ret(cx); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_ScriptInterface->ReadStructuredClone(techTemplates, &ret); m_TechTemplates = CScriptValRooted(cx, ret); } void LoadEntityTemplates(const std::vector >& templates) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); m_HasLoadedEntityTemplates = true; JS::RootedValue tmpEntityTemplates(cx); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade m_ScriptInterface->Eval("({})", &tmpEntityTemplates); JS::RootedValue val(cx); for (size_t i = 0; i < templates.size(); ++i) { templates[i].second->ToJSVal(cx, false, &val); m_ScriptInterface->SetProperty(tmpEntityTemplates, templates[i].first.c_str(), val, true); } // Since the template data is shared between AI players, freeze it // to stop any of them changing it and confusing the other players m_EntityTemplates = CScriptValRooted(cx, tmpEntityTemplates); m_ScriptInterface->FreezeObject(tmpEntityTemplates, true); } void Serialize(std::ostream& stream, bool isDebug) { WaitToFinishComputation(); if (isDebug) { CDebugSerializer serializer(*m_ScriptInterface, stream); serializer.Indent(4); SerializeState(serializer); } else { CStdSerializer serializer(*m_ScriptInterface, stream); // TODO: see comment in Deserialize() serializer.SetSerializablePrototypes(m_SerializablePrototypes); SerializeState(serializer); } } void SerializeState(ISerializer& serializer) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); std::stringstream rngStream; rngStream << m_RNG; serializer.StringASCII("rng", rngStream.str(), 0, 32); serializer.NumberU32_Unbounded("turn", m_TurnNum); serializer.NumberU32_Unbounded("num ais", (u32)m_Players.size()); serializer.Bool("useSharedScript", m_HasSharedComponent); if (m_HasSharedComponent) { JS::RootedValue sharedData(cx); JS::RootedValue tmpSharedAIObj(cx, m_SharedAIObj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade if (!m_ScriptInterface->CallFunction(tmpSharedAIObj, "Serialize", &sharedData)) LOGERROR(L"AI shared script Serialize call failed"); serializer.ScriptVal("sharedData", &sharedData); } for (size_t i = 0; i < m_Players.size(); ++i) { serializer.String("name", m_Players[i]->m_AIName, 1, 256); serializer.NumberI32_Unbounded("player", m_Players[i]->m_Player); serializer.NumberU8_Unbounded("difficulty", m_Players[i]->m_Difficulty); serializer.NumberU32_Unbounded("num commands", (u32)m_Players[i]->m_Commands.size()); for (size_t j = 0; j < m_Players[i]->m_Commands.size(); ++j) { JS::RootedValue val(cx); m_ScriptInterface->ReadStructuredClone(m_Players[i]->m_Commands[j], &val); serializer.ScriptVal("command", &val); } JS::RootedValue tmpPlayerObj(cx, m_Players[i]->m_Obj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade bool hasCustomSerialize = m_ScriptInterface->HasProperty(tmpPlayerObj, "Serialize"); if (hasCustomSerialize) { JS::RootedValue scriptData(cx); if (!m_ScriptInterface->CallFunction(tmpPlayerObj, "Serialize", &scriptData)) LOGERROR(L"AI script Serialize call failed"); serializer.ScriptVal("data", &scriptData); } else { serializer.ScriptVal("data", &tmpPlayerObj); } } } void Deserialize(std::istream& stream) { JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); ENSURE(m_CommandsComputed); // deserializing while we're still actively computing would be bad CStdDeserializer deserializer(*m_ScriptInterface, stream); m_PlayerMetadata.clear(); m_Players.clear(); std::string rngString; std::stringstream rngStream; deserializer.StringASCII("rng", rngString, 0, 32); rngStream << rngString; rngStream >> m_RNG; deserializer.NumberU32_Unbounded("turn", m_TurnNum); uint32_t numAis; deserializer.NumberU32_Unbounded("num ais", numAis); deserializer.Bool("useSharedScript", m_HasSharedComponent); TryLoadSharedComponent(false); if (m_HasSharedComponent) { JS::RootedValue sharedData(cx); JS::RootedValue tmpSharedAIObj(cx, m_SharedAIObj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 deserializer.ScriptVal("sharedData", &sharedData); if (!m_ScriptInterface->CallFunctionVoid(tmpSharedAIObj, "Deserialize", sharedData)) LOGERROR(L"AI shared script Deserialize call failed"); } for (size_t i = 0; i < numAis; ++i) { std::wstring name; player_id_t player; uint8_t difficulty; deserializer.String("name", name, 1, 256); deserializer.NumberI32_Unbounded("player", player); deserializer.NumberU8_Unbounded("difficulty",difficulty); if (!AddPlayer(name, player, difficulty)) throw PSERROR_Deserialize_ScriptError(); uint32_t numCommands; deserializer.NumberU32_Unbounded("num commands", numCommands); m_Players.back()->m_Commands.reserve(numCommands); for (size_t j = 0; j < numCommands; ++j) { JS::RootedValue val(cx); deserializer.ScriptVal("command", &val); m_Players.back()->m_Commands.push_back(m_ScriptInterface->WriteStructuredClone(val)); } // TODO: this is yucky but necessary while the AIs are sharing data between contexts; // ideally a new (de)serializer instance would be created for each player // so they would have a single, consistent script context to use and serializable // prototypes could be stored in their ScriptInterface deserializer.SetSerializablePrototypes(m_DeserializablePrototypes); JS::RootedValue tmpPlayerObj(cx, m_Players.back()->m_Obj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 upgrade bool hasCustomDeserialize = m_ScriptInterface->HasProperty(tmpPlayerObj, "Deserialize"); if (hasCustomDeserialize) { JS::RootedValue scriptData(cx); deserializer.ScriptVal("data", &scriptData); if (m_Players[i]->m_UseSharedComponent) { if (!m_ScriptInterface->CallFunctionVoid(tmpPlayerObj, "Deserialize", scriptData, m_SharedAIObj)) LOGERROR(L"AI script Deserialize call failed"); } else if (!m_ScriptInterface->CallFunctionVoid(tmpPlayerObj, "Deserialize", scriptData)) { LOGERROR(L"AI script deserialize() call failed"); } } else { deserializer.ScriptVal("data", &tmpPlayerObj); m_Players.back()->m_Obj = CScriptValRooted(cx, tmpPlayerObj); } } } int getPlayerSize() { return m_Players.size(); } void RegisterSerializablePrototype(std::wstring name, CScriptVal proto) { // Require unique prototype and name (for reverse lookup) // TODO: this is yucky - see comment in Deserialize() JSObject* obj = JSVAL_TO_OBJECT(proto.get()); std::pair::iterator, bool> ret1 = m_SerializablePrototypes.insert(std::make_pair(obj, name)); std::pair::iterator, bool> ret2 = m_DeserializablePrototypes.insert(std::make_pair(name, obj)); if (!ret1.second || !ret2.second) LOGERROR(L"RegisterSerializablePrototype called with same prototype multiple times: p=%p n='%ls'", obj, name.c_str()); } private: void LoadMetadata(const VfsPath& path, JS::MutableHandleValue out) { if (m_PlayerMetadata.find(path) == m_PlayerMetadata.end()) { // Load and cache the AI player metadata m_ScriptInterface->ReadJSONFile(path, out); m_PlayerMetadata[path] = CScriptValRooted(m_ScriptInterface->GetContext(), out); } out.set(m_PlayerMetadata[path].get()); } void PerformComputation() { // Deserialize the game state, to pass to the AI's HandleMessage JSContext* cx = m_ScriptInterface->GetContext(); JSAutoRequest rq(cx); JS::RootedValue state(cx); { PROFILE3("AI compute read state"); m_ScriptInterface->ReadStructuredClone(m_GameState, &state); m_ScriptInterface->SetProperty(state, "passabilityMap", m_PassabilityMapVal, true); m_ScriptInterface->SetProperty(state, "territoryMap", m_TerritoryMapVal, true); } // It would be nice to do // m_ScriptInterface->FreezeObject(state.get(), true); // to prevent AI scripts accidentally modifying the state and // affecting other AI scripts they share it with. But the performance // cost is far too high, so we won't do that. // If there is a shared component, run it if (m_HasSharedComponent) { PROFILE3("AI run shared component"); JS::RootedValue tmpSharedAIObj(cx, m_SharedAIObj.get()); // TODO: Check if this temporary root can be removed after SpiderMonkey 31 m_ScriptInterface->CallFunctionVoid(tmpSharedAIObj, "onUpdate", state); } for (size_t i = 0; i < m_Players.size(); ++i) { PROFILE3("AI script"); PROFILE2_ATTR("player: %d", m_Players[i]->m_Player); PROFILE2_ATTR("script: %ls", m_Players[i]->m_AIName.c_str()); if (m_HasSharedComponent && m_Players[i]->m_UseSharedComponent) m_Players[i]->Run(state, m_Players[i]->m_Player, m_SharedAIObj); else m_Players[i]->Run(state, m_Players[i]->m_Player); } } shared_ptr m_ScriptRuntime; shared_ptr m_ScriptInterface; boost::rand48 m_RNG; u32 m_TurnNum; CScriptValRooted m_EntityTemplates; bool m_HasLoadedEntityTemplates; CScriptValRooted m_TechTemplates; std::map m_PlayerMetadata; std::vector > m_Players; // use shared_ptr just to avoid copying bool m_HasSharedComponent; CScriptValRooted m_SharedAIObj; std::vector m_Commands; std::set m_LoadedModules; shared_ptr m_GameState; Grid m_PassabilityMap; CScriptValRooted m_PassabilityMapVal; Grid m_TerritoryMap; CScriptValRooted m_TerritoryMapVal; bool m_CommandsComputed; std::map m_SerializablePrototypes; std::map m_DeserializablePrototypes; }; /** * Implementation of ICmpAIManager. */ class CCmpAIManager : public ICmpAIManager { public: static void ClassInit(CComponentManager& componentManager) { componentManager.SubscribeToMessageType(MT_ProgressiveLoad); } DEFAULT_COMPONENT_ALLOCATOR(AIManager) static std::string GetSchema() { return ""; } virtual void Init(const CParamNode& UNUSED(paramNode)) { m_TerritoriesDirtyID = 0; m_JustDeserialized = false; StartLoadEntityTemplates(); } virtual void Deinit() { } virtual void Serialize(ISerializer& serialize) { // Because the AI worker uses its own ScriptInterface, we can't use the // ISerializer (which was initialised with the simulation ScriptInterface) // directly. So we'll just grab the ISerializer's stream and write to it // with an independent serializer. m_Worker.Serialize(serialize.GetStream(), serialize.IsDebug()); } virtual void Deserialize(const CParamNode& paramNode, IDeserializer& deserialize) { Init(paramNode); ForceLoadEntityTemplates(); m_Worker.Deserialize(deserialize.GetStream()); m_JustDeserialized = true; } virtual void HandleMessage(const CMessage& msg, bool UNUSED(global)) { switch (msg.GetType()) { case MT_ProgressiveLoad: { const CMessageProgressiveLoad& msgData = static_cast (msg); *msgData.total += (int)m_TemplateNames.size(); if (*msgData.progressed) break; if (ContinueLoadEntityTemplates()) *msgData.progressed = true; *msgData.progress += (int)m_TemplateLoadedIdx; break; } } } virtual void AddPlayer(std::wstring id, player_id_t player, uint8_t difficulty) { m_Worker.AddPlayer(id, player, difficulty); // AI players can cheat and see through FoW/SoD, since that greatly simplifies // their implementation. // (TODO: maybe cleverer AIs should be able to optionally retain FoW/SoD) CmpPtr cmpRangeManager(GetSystemEntity()); if (cmpRangeManager) cmpRangeManager->SetLosRevealAll(player, true); } + virtual void SetRNGSeed(uint32_t seed) + { + m_Worker.SetRNGSeed(seed); + } + virtual void TryLoadSharedComponent() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); // load the technology templates CmpPtr cmpTechTemplateManager(GetSystemEntity()); ENSURE(cmpTechTemplateManager); // Get the game state from AIInterface JS::RootedValue techTemplates(cx, cmpTechTemplateManager->GetAllTechs().get()); m_Worker.RegisterTechTemplates(scriptInterface.WriteStructuredClone(techTemplates)); m_Worker.TryLoadSharedComponent(true); } virtual void RunGamestateInit() { ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface // We flush events from the initialization so we get a clean state now. JS::RootedValue state(cx, cmpAIInterface->GetFullRepresentation(true).get()); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdate first Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID)) { territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); } LoadPathfinderClasses(state); m_Worker.RunGamestateInit(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap); } virtual void StartComputation() { PROFILE("AI setup"); ForceLoadEntityTemplates(); ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); if (m_Worker.getPlayerSize() == 0) return; CmpPtr cmpAIInterface(GetSystemEntity()); ENSURE(cmpAIInterface); // Get the game state from AIInterface JS::RootedValue state(cx); if (m_JustDeserialized) state.set(cmpAIInterface->GetFullRepresentation(false).get()); else state.set(cmpAIInterface->GetRepresentation().get()); // Get the passability data Grid dummyGrid; const Grid* passabilityMap = &dummyGrid; CmpPtr cmpPathfinder(GetSystemEntity()); if (cmpPathfinder) passabilityMap = &cmpPathfinder->GetPassabilityGrid(); // Get the territory data // Since getting the territory grid can trigger a recalculation, we check NeedUpdate first bool territoryMapDirty = false; Grid dummyGrid2; const Grid* territoryMap = &dummyGrid2; CmpPtr cmpTerritoryManager(GetSystemEntity()); if (cmpTerritoryManager && cmpTerritoryManager->NeedUpdate(&m_TerritoriesDirtyID)) { territoryMap = &cmpTerritoryManager->GetTerritoryGrid(); territoryMapDirty = true; } LoadPathfinderClasses(state); m_Worker.StartComputation(scriptInterface.WriteStructuredClone(state), *passabilityMap, *territoryMap, territoryMapDirty); m_JustDeserialized = false; } virtual void PushCommands() { std::vector commands; m_Worker.GetCommands(commands); CmpPtr cmpCommandQueue(GetSystemEntity()); if (!cmpCommandQueue) return; ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue clonedCommandVal(cx); for (size_t i = 0; i < commands.size(); ++i) { for (size_t j = 0; j < commands[i].commands.size(); ++j) { scriptInterface.ReadStructuredClone(commands[i].commands[j], &clonedCommandVal); cmpCommandQueue->PushLocalCommand(commands[i].player, CScriptVal(clonedCommandVal)); } } } private: std::vector m_TemplateNames; size_t m_TemplateLoadedIdx; std::vector > m_Templates; size_t m_TerritoriesDirtyID; bool m_JustDeserialized; void StartLoadEntityTemplates() { CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); m_TemplateNames = cmpTemplateManager->FindAllTemplates(false); m_TemplateLoadedIdx = 0; m_Templates.reserve(m_TemplateNames.size()); } // Tries to load the next entity template. Returns true if we did some work. bool ContinueLoadEntityTemplates() { if (m_TemplateLoadedIdx >= m_TemplateNames.size()) return false; CmpPtr cmpTemplateManager(GetSystemEntity()); ENSURE(cmpTemplateManager); const CParamNode* node = cmpTemplateManager->GetTemplateWithoutValidation(m_TemplateNames[m_TemplateLoadedIdx]); if (node) m_Templates.push_back(std::make_pair(m_TemplateNames[m_TemplateLoadedIdx], node)); m_TemplateLoadedIdx++; // If this was the last template, send the data to the worker if (m_TemplateLoadedIdx == m_TemplateNames.size()) m_Worker.LoadEntityTemplates(m_Templates); return true; } void ForceLoadEntityTemplates() { while (ContinueLoadEntityTemplates()) { } } void LoadPathfinderClasses(JS::HandleValue state) { CmpPtr cmpPathfinder(GetSystemEntity()); if (!cmpPathfinder) return; ScriptInterface& scriptInterface = GetSimContext().GetScriptInterface(); JSContext* cx = scriptInterface.GetContext(); JSAutoRequest rq(cx); JS::RootedValue classesVal(cx); scriptInterface.Eval("({ pathfinderObstruction: 1, foundationObstruction: 2 })", &classesVal); std::map classes = cmpPathfinder->GetPassabilityClasses(); for (std::map::iterator it = classes.begin(); it != classes.end(); ++it) scriptInterface.SetProperty(classesVal, it->first.c_str(), it->second, true); scriptInterface.SetProperty(state, "passabilityClasses", classesVal, true); } CAIWorker m_Worker; }; REGISTER_COMPONENT_TYPE(AIManager) Index: ps/trunk/source/simulation2/components/ICmpAIManager.cpp =================================================================== --- ps/trunk/source/simulation2/components/ICmpAIManager.cpp (revision 15972) +++ ps/trunk/source/simulation2/components/ICmpAIManager.cpp (revision 15973) @@ -1,82 +1,83 @@ /* Copyright (C) 2012 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #include "precompiled.h" #include "ICmpAIManager.h" #include "simulation2/system/InterfaceScripted.h" #include "lib/file/vfs/vfs_util.h" #include "ps/Filesystem.h" BEGIN_INTERFACE_WRAPPER(AIManager) DEFINE_INTERFACE_METHOD_3("AddPlayer", void, ICmpAIManager, AddPlayer, std::wstring, player_id_t, uint8_t) +DEFINE_INTERFACE_METHOD_1("SetRNGSeed", void, ICmpAIManager, SetRNGSeed, uint32_t) DEFINE_INTERFACE_METHOD_0("TryLoadSharedComponent", void, ICmpAIManager, TryLoadSharedComponent) DEFINE_INTERFACE_METHOD_0("RunGamestateInit", void, ICmpAIManager, RunGamestateInit) END_INTERFACE_WRAPPER(AIManager) // Implement the static method that finds all AI scripts // that can be loaded via AddPlayer: struct GetAIsHelper { NONCOPYABLE(GetAIsHelper); public: GetAIsHelper(ScriptInterface& scriptInterface) : m_ScriptInterface(scriptInterface) { } void Run() { vfs::ForEachFile(g_VFS, L"simulation/ai/", Callback, (uintptr_t)this, L"*.json", vfs::DIR_RECURSIVE); } static Status Callback(const VfsPath& pathname, const CFileInfo& UNUSED(fileInfo), const uintptr_t cbData) { GetAIsHelper* self = (GetAIsHelper*)cbData; JSContext* cx = self->m_ScriptInterface.GetContext(); JSAutoRequest rq(cx); // Extract the 3rd component of the path (i.e. the directory after simulation/ai/) fs::wpath components = pathname.string(); fs::wpath::iterator it = components.begin(); std::advance(it, 2); std::wstring dirname = GetWstringFromWpath(*it); JS::RootedValue ai(cx); JS::RootedValue data(cx); self->m_ScriptInterface.ReadJSONFile(pathname, &data); self->m_ScriptInterface.Eval("({})", &ai); self->m_ScriptInterface.SetProperty(ai, "id", dirname, true); self->m_ScriptInterface.SetProperty(ai, "data", data, true); self->m_AIs.push_back(CScriptValRooted(cx, ai)); return INFO::OK; } std::vector m_AIs; ScriptInterface& m_ScriptInterface; }; std::vector ICmpAIManager::GetAIs(ScriptInterface& scriptInterface) { GetAIsHelper helper(scriptInterface); helper.Run(); return helper.m_AIs; } Index: ps/trunk/source/simulation2/components/ICmpAIManager.h =================================================================== --- ps/trunk/source/simulation2/components/ICmpAIManager.h (revision 15972) +++ ps/trunk/source/simulation2/components/ICmpAIManager.h (revision 15973) @@ -1,58 +1,59 @@ /* Copyright (C) 2011 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . */ #ifndef INCLUDED_ICMPAIMANAGER #define INCLUDED_ICMPAIMANAGER #include "simulation2/system/Interface.h" #include "simulation2/helpers/Player.h" class ICmpAIManager : public IComponent { public: /** * Add a new AI player into the world, based on the AI script identified * by @p id (corresponding to a subdirectory in simulation/ai/), * to control player @p player. */ virtual void AddPlayer(std::wstring id, player_id_t player, uint8_t difficulty) = 0; + virtual void SetRNGSeed(uint32_t seed) = 0; virtual void TryLoadSharedComponent() = 0; virtual void RunGamestateInit() = 0; /** * Call this at the end of a turn, to trigger AI computation which will be * ready for the next turn. */ virtual void StartComputation() = 0; /** * Call this at the start of a turn, to push the computed AI commands into * the command queue. */ virtual void PushCommands() = 0; /** * Returns a vector of {"id":"value-for-AddPlayer", "name":"Human readable name"} * objects, based on all the available AI scripts. */ static std::vector GetAIs(ScriptInterface& scriptInterface); DECLARE_INTERFACE_TYPE(AIManager) }; #endif // INCLUDED_ICMPAIMANAGER