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