Index: ps/trunk/binaries/data/mods/public/gui/loadgame/SavegamePage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loadgame/SavegamePage.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/loadgame/SavegamePage.js (revision 24979)
@@ -1,48 +1,44 @@
/**
- * This class architecture is an example of how to use classes
- * to encapsulate and to avoid fragmentation and globals.
- */
-var g_SavegamePage;
-
-function init(data)
-{
- g_SavegamePage = new SavegamePage(data);
-}
-
-/**
* This class is responsible for loading the affected GUI control classes,
* and setting them up to communicate with each other.
*/
class SavegamePage
{
constructor(data)
{
- this.savegameList = new SavegameList();
+ this.savegameList = new SavegameList(data && data.campaignRun || null);
this.savegameDetails = new SavegameDetails();
this.savegameList.registerSelectionChangeHandler(this.savegameDetails);
this.savegameDeleter = new SavegameDeleter();
this.savegameDeleter.registerSavegameListChangeHandler(this.savegameList);
this.savegameList.registerSelectionChangeHandler(this.savegameDeleter);
let savePage = Engine.IsGameStarted();
if (savePage)
{
this.savegameWriter = new SavegameWriter(data && data.savedGameData || {});
this.savegameList.registerSelectionChangeHandler(this.savegameWriter);
let size = this.savegameList.gameSelection.size;
size.bottom -= 24;
this.savegameList.gameSelection.size = size;
}
else
{
this.savegameLoader = new SavegameLoader();
this.savegameList.registerSelectionChangeHandler(this.savegameLoader);
this.savegameList.selectFirst();
}
Engine.GetGUIObjectByName("title").caption = savePage ? translate("Save Game") : translate("Load Game");
Engine.GetGUIObjectByName("cancel").onPress = () => { Engine.PopGuiPage(); };
}
}
+
+var g_SavegamePage;
+
+function init(data)
+{
+ g_SavegamePage = new SavegamePage(data);
+}
Index: ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItems.js (revision 24979)
@@ -1,232 +1,265 @@
var g_MainMenuItems = [
{
"caption": translate("Learn to Play"),
"tooltip": translate("Learn how to play, start the tutorial, discover the technology trees, and the history behind the civilizations."),
"submenu": [
{
"caption": translate("Manual"),
"tooltip": translate("Open the 0 A.D. Game Manual."),
"onPress": () => {
Engine.PushGuiPage("page_manual.xml");
}
},
{
"caption": translate("Tutorial"),
"tooltip": translate("Start the economic tutorial."),
"onPress": () => {
Engine.SwitchGuiPage("page_gamesetup.xml", {
+ "autostart": true,
"mapType": "scenario",
"map": "maps/tutorials/starting_economy_walkthrough"
});
}
},
{
"caption": translate("Structure Tree"),
"tooltip": colorizeHotkey(translate("%(hotkey)s: View the structure tree of civilizations featured in 0 A.D."), "structree"),
"hotkey": "structree",
"onPress": () => {
let callback = data => {
if (data.nextPage)
Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback);
};
Engine.PushGuiPage("page_structree.xml", {}, callback);
},
},
{
"caption": translate("Civilization Overview"),
"tooltip": colorizeHotkey(translate("%(hotkey)s: Learn about the civilizations featured in 0 A.D."), "civinfo"),
"hotkey": "civinfo",
"onPress": () => {
let callback = data => {
if (data.nextPage)
Engine.PushGuiPage(data.nextPage, { "civ": data.civ }, callback);
};
Engine.PushGuiPage("page_civinfo.xml", {}, callback);
}
},
{
"caption": translate("Catafalque Overview"),
"tooltip": translate("Compare the bonuses of catafalques featured in 0 A.D."),
"onPress": () => {
Engine.PushGuiPage("page_catafalque.xml");
}
},
{
"caption": translate("Map Overview"),
"tooltip": translate("View the different maps featured in 0 A.D."),
"onPress": () => {
Engine.PushGuiPage("page_mapbrowser.xml");
},
}
]
},
{
+ "caption": translate("Continue Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage(CampaignRun.getCurrentRun().getMenuPath(), {
+ "filename": CampaignRun.getCurrentRun().filename
+ });
+ },
+ "enabled": () => !!CampaignRun.getCurrentRun()
+ },
+ {
"caption": translate("Single-player"),
"tooltip": translate("Start, load, or replay a single-player game."),
"submenu": [
{
"caption": translate("Matches"),
"tooltip": translate("Start a new single-player game."),
"onPress": () => {
Engine.SwitchGuiPage("page_gamesetup.xml");
}
},
{
- "caption": translate("Campaigns"),
- "tooltip": translate("Relive history through historical military campaigns. \\[NOT YET IMPLEMENTED]"),
- "enabled": false
- },
- {
"caption": translate("Load Game"),
"tooltip": translate("Load a saved game."),
"onPress": () => {
Engine.PushGuiPage("page_loadgame.xml");
}
},
{
+ "caption": translate("Continue Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage(CampaignRun.getCurrentRun().getMenuPath(), {
+ "filename": CampaignRun.getCurrentRun().filename
+ });
+ },
+ "enabled": () => !!CampaignRun.getCurrentRun()
+ },
+ {
+ "caption": translate("New Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ Engine.SwitchGuiPage("campaigns/setup/page.xml");
+ }
+ },
+ {
+ "caption": translate("Load Campaign"),
+ "tooltip": translate("Relive history through historical military campaigns."),
+ "onPress": () => {
+ // Switch instead of push, otherwise the 'continue'
+ // button might remain enabled.
+ // TODO: find a better solution.
+ Engine.SwitchGuiPage("campaigns/load_modal/page.xml");
+ }
+ },
+ {
"caption": translate("Replays"),
"tooltip": translate("Playback previous games."),
"onPress": () => {
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Single-player"
}
}
});
}
}
]
},
{
"caption": translate("Multiplayer"),
"tooltip": translate("Fight against one or more human players in a multiplayer game."),
"submenu": [
{
// Translation: Join a game by specifying the host's IP address.
"caption": translate("Join Game"),
"tooltip": translate("Joining an existing multiplayer game."),
"onPress": () => {
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "join"
});
}
},
{
"caption": translate("Host Game"),
"tooltip": translate("Host a multiplayer game."),
"onPress": () => {
Engine.PushGuiPage("page_gamesetup_mp.xml", {
"multiplayerGameType": "host"
});
}
},
{
"caption": translate("Game Lobby"),
"tooltip":
colorizeHotkey(translate("%(hotkey)s: Launch the multiplayer lobby to join and host publicly visible games and chat with other players."), "lobby") +
(Engine.StartXmppClient ? "" : translate("Launch the multiplayer lobby. \\[DISABLED BY BUILD]")),
- "enabled": !!Engine.StartXmppClient,
+ "enabled": () => !!Engine.StartXmppClient,
"hotkey": "lobby",
"onPress": () => {
if (Engine.StartXmppClient)
Engine.PushGuiPage("page_prelobby_entrance.xml");
}
},
{
"caption": translate("Replays"),
"tooltip": translate("Playback previous games."),
"onPress": () => {
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": {
"filters": {
"singleplayer": "Multiplayer"
}
}
});
}
}
]
},
{
"caption": translate("Settings"),
"tooltip": translate("Change game options."),
"submenu": [
{
"caption": translate("Options"),
"tooltip": translate("Adjust game settings."),
"onPress": () => {
Engine.PushGuiPage(
"page_options.xml",
{},
fireConfigChangeHandlers);
}
},
{
"caption": translate("Hotkeys"),
"tooltip": translate("Adjust hotkeys."),
"onPress": () => {
Engine.PushGuiPage("hotkeys/page_hotkeys.xml");
}
},
{
"caption": translate("Language"),
"tooltip": translate("Choose the language of the game."),
"onPress": () => {
Engine.PushGuiPage("page_locale.xml");
}
},
{
"caption": translate("Mod Selection"),
"tooltip": translate("Select and download mods for the game."),
"onPress": () => {
Engine.SwitchGuiPage("page_modmod.xml");
}
},
{
"caption": translate("Welcome Screen"),
"tooltip": translate("Show the Welcome Screen again. Useful if you hid it by mistake."),
"onPress": () => {
Engine.PushGuiPage("page_splashscreen.xml");
}
}
]
},
{
"caption": translate("Scenario Editor"),
"tooltip": translate('Open the Atlas Scenario Editor in a new window. You can run this more reliably by starting the game with the command-line argument "-editor".'),
"onPress": () => {
if (Engine.AtlasIsAvailable())
messageBox(
400, 200,
translate("Are you sure you want to quit 0 A.D. and open the Scenario Editor?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[null, Engine.RestartInAtlas]);
else
messageBox(
400, 200,
translate("The scenario editor is not available or failed to load. See the game logs for additional information."),
translate("Error"));
}
},
{
"caption": translate("Credits"),
"tooltip": translate("Show the 0 A.D. credits."),
"onPress": () => {
Engine.PushGuiPage("page_credits.xml");
}
},
{
"caption": translate("Exit"),
"tooltip": translate("Exit the game."),
"onPress": () => {
messageBox(
400, 200,
translate("Are you sure you want to quit 0 A.D.?"),
translate("Confirmation"),
[translate("No"), translate("Yes")],
[null, Engine.Exit]);
}
}
];
Index: ps/trunk/binaries/data/mods/public/gui/session/MenuButtons.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/MenuButtons.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/session/MenuButtons.js (revision 24979)
@@ -1,272 +1,275 @@
/**
* This class is extended in subclasses.
* Each subclass represents one button in the session menu.
* All subclasses store the button member so that mods can change it easily.
*/
class MenuButtons
{
}
MenuButtons.prototype.Manual = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate(translate("Manual"));
this.pauseControl = pauseControl;
}
onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage("page_manual.xml", {}, resumeGame);
}
};
MenuButtons.prototype.Chat = class
{
constructor(button, pauseControl, playerViewControl, chat)
{
this.button = button;
this.button.caption = translate("Chat");
this.chat = chat;
registerHotkeyChangeHandler(this.rebuild.bind(this));
}
rebuild()
{
this.button.tooltip = this.chat.getOpenHotkeyTooltip().trim();
}
onPress()
{
this.chat.openPage();
}
};
MenuButtons.prototype.Save = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Save");
this.pauseControl = pauseControl;
}
onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"page_loadgame.xml",
- { "savedGameData": getSavedGameData() },
+ {
+ "savedGameData": getSavedGameData(),
+ "campaignRun": g_CampaignSession ? g_CampaignSession.run.filename : null
+ },
resumeGame);
}
};
MenuButtons.prototype.Summary = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Summary");
this.button.hotkey = "summary";
// TODO: Atlas should pass g_GameAttributes.settings
this.button.enabled = !Engine.IsAtlasRunning();
this.pauseControl = pauseControl;
this.selectedData = undefined;
registerHotkeyChangeHandler(this.rebuild.bind(this));
}
rebuild()
{
this.button.tooltip = sprintf(translate("Press %(hotkey)s to open the summary screen."), {
"hotkey": colorizeHotkey("%(hotkey)s", this.button.hotkey),
});
}
onPress()
{
if (Engine.IsAtlasRunning())
return;
closeOpenDialogs();
this.pauseControl.implicitPause();
// Allows players to see their own summary.
// If they have shared ally vision researched, they are able to see the summary of there allies too.
let simState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
Engine.PushGuiPage(
"page_summary.xml",
{
"sim": {
"mapSettings": g_GameAttributes.settings,
"playerStates": simState.players.filter((state, player) =>
g_IsObserver || g_ViewedPlayer == 0 || player == 0 || player == g_ViewedPlayer ||
simState.players[g_ViewedPlayer].hasSharedLos && g_Players[player].isMutualAlly[g_ViewedPlayer]),
"timeElapsed": simState.timeElapsed
},
"gui": {
"dialog": true,
"isInGame": true,
"summarySelection": this.summarySelection
},
},
data => {
this.summarySelection = data.summarySelection;
this.pauseControl.implicitResume();
});
}
};
MenuButtons.prototype.Lobby = class
{
constructor(button)
{
this.button = button;
this.button.caption = translate("Lobby");
this.button.hotkey = "lobby";
this.button.enabled = Engine.HasXmppClient();
registerHotkeyChangeHandler(this.rebuild.bind(this));
}
rebuild()
{
this.button.tooltip = sprintf(translate("Press %(hotkey)s to open the multiplayer lobby page without leaving the game."), {
"hotkey": colorizeHotkey("%(hotkey)s", this.button.hotkey),
});
}
onPress()
{
if (!Engine.HasXmppClient())
return;
closeOpenDialogs();
Engine.PushGuiPage("page_lobby.xml", { "dialog": true });
}
};
MenuButtons.prototype.Options = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Options");
this.pauseControl = pauseControl;
}
onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"page_options.xml",
{},
changes => {
fireConfigChangeHandlers(changes);
resumeGame();
});
}
};
MenuButtons.prototype.Hotkeys = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Hotkeys");
this.pauseControl = pauseControl;
}
onPress()
{
closeOpenDialogs();
this.pauseControl.implicitPause();
Engine.PushGuiPage(
"hotkeys/page_hotkeys.xml",
{},
() => { resumeGame(); });
}
};
MenuButtons.prototype.Pause = class
{
constructor(button, pauseControl, playerViewControl)
{
this.button = button;
this.button.hotkey = "pause";
this.pauseControl = pauseControl;
registerPlayersInitHandler(this.rebuild.bind(this));
registerPlayersFinishedHandler(this.rebuild.bind(this));
playerViewControl.registerPlayerIDChangeHandler(this.rebuild.bind(this));
pauseControl.registerPauseHandler(this.rebuild.bind(this));
registerHotkeyChangeHandler(this.rebuild.bind(this));
registerNetworkStatusChangeHandler(this.rebuild.bind(this));
}
rebuild()
{
this.button.enabled = this.pauseControl.canPause(true);
this.button.caption = this.pauseControl.explicitPause ? translate("Resume") : translate("Pause");
this.button.tooltip = sprintf(translate("Press %(hotkey)s to pause or resume the game."), {
"hotkey": colorizeHotkey("%(hotkey)s", this.button.hotkey),
});
}
onPress()
{
this.pauseControl.setPaused(!g_PauseControl.explicitPause, true);
}
};
MenuButtons.prototype.Resign = class
{
constructor(button, pauseControl, playerViewControl)
{
this.button = button;
this.button.caption = translate("Resign");
this.pauseControl = pauseControl;
registerPlayersInitHandler(this.rebuild.bind(this));
registerPlayersFinishedHandler(this.rebuild.bind(this));
playerViewControl.registerPlayerIDChangeHandler(this.rebuild.bind(this));
}
rebuild()
{
this.button.enabled = !g_IsObserver;
}
onPress()
{
(new ResignConfirmation()).display();
}
};
MenuButtons.prototype.Exit = class
{
constructor(button, pauseControl)
{
this.button = button;
this.button.caption = translate("Exit");
this.button.enabled = !Engine.IsAtlasRunning();
this.pauseControl = pauseControl;
}
onPress()
{
for (let name in QuitConfirmationMenu.prototype)
{
let quitConfirmation = new QuitConfirmationMenu.prototype[name]();
if (quitConfirmation.enabled())
quitConfirmation.display();
}
}
};
Index: ps/trunk/binaries/data/mods/public/gui/session/session.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 24979)
@@ -1,815 +1,832 @@
const g_IsReplay = Engine.IsVisualReplay();
const g_CivData = loadCivData(false, true);
const g_MapSizes = prepareForDropdown(g_Settings && g_Settings.MapSizes);
const g_MapTypes = prepareForDropdown(g_Settings && g_Settings.MapTypes);
const g_PopulationCapacities = prepareForDropdown(g_Settings && g_Settings.PopulationCapacities);
const g_WorldPopulationCapacities = prepareForDropdown(g_Settings && g_Settings.WorldPopulationCapacities);
const g_StartingResources = prepareForDropdown(g_Settings && g_Settings.StartingResources);
const g_VictoryConditions = g_Settings && g_Settings.VictoryConditions;
var g_Ambient;
var g_AutoFormation;
var g_Chat;
var g_Cheats;
var g_DeveloperOverlay;
var g_DiplomacyColors;
var g_DiplomacyDialog;
var g_GameSpeedControl;
var g_Menu;
var g_MiniMapPanel;
var g_NetworkStatusOverlay;
var g_ObjectivesDialog;
var g_OutOfSyncNetwork;
var g_OutOfSyncReplay;
var g_PanelEntityManager;
var g_PauseControl;
var g_PauseOverlay;
var g_PlayerViewControl;
var g_QuitConfirmationDefeat;
var g_QuitConfirmationReplay;
var g_RangeOverlayManager;
var g_ResearchProgress;
var g_TimeNotificationOverlay;
var g_TopPanel;
var g_TradeDialog;
/**
* Map, player and match settings set in game setup.
*/
const g_GameAttributes = deepfreeze(Engine.GuiInterfaceCall("GetInitAttributes"));
/**
* True if this is a multiplayer game.
*/
const g_IsNetworked = Engine.HasNetClient();
/**
* Is this user in control of game settings (i.e. is a network server, or offline player).
*/
var g_IsController = !g_IsNetworked || Engine.IsNetController();
/**
* Whether we have finished the synchronization and
* can start showing simulation related message boxes.
*/
var g_IsNetworkedActive = false;
/**
* True if the connection to the server has been lost.
*/
var g_Disconnected = false;
/**
* True if the current user has observer capabilities.
*/
var g_IsObserver = false;
/**
* True if the current user has rejoined (or joined the game after it started).
*/
var g_HasRejoined = false;
/**
* The playerID selected in the change perspective tool.
*/
var g_ViewedPlayer = Engine.GetPlayerID();
/**
* True if the camera should focus on attacks and player commands
* and select the affected units.
*/
var g_FollowPlayer = false;
/**
* Cache the basic player data (name, civ, color).
*/
var g_Players = [];
/**
* Last time when onTick was called().
* Used for animating the main menu.
*/
var g_LastTickTime = Date.now();
/**
* Recalculate which units have their status bars shown with this frequency in milliseconds.
*/
var g_StatusBarUpdate = 200;
/**
* For restoring selection, order and filters when returning to the replay menu
*/
var g_ReplaySelectionData;
/**
* Remembers which clients are assigned to which player slots.
* The keys are GUIDs or "local" in single-player.
*/
var g_PlayerAssignments;
/**
* Whether the entire UI should be hidden (useful for promotional screenshots).
* Can be toggled with a hotkey.
*/
var g_ShowGUI = true;
/**
* Whether status bars should be shown for all of the player's units.
*/
var g_ShowAllStatusBars = false;
/**
* Cache of simulation state and template data (apart from TechnologyData, updated on every simulation update).
*/
var g_SimState;
var g_EntityStates = {};
var g_TemplateData = {};
var g_TechnologyData = {};
var g_ResourceData = new Resources();
/**
* These handlers are called each time a new turn was simulated.
* Use this as sparely as possible.
*/
var g_SimulationUpdateHandlers = new Set();
/**
* These handlers are called after the player states have been initialized.
*/
var g_PlayersInitHandlers = new Set();
/**
* These handlers are called when a player has been defeated or won the game.
*/
var g_PlayerFinishedHandlers = new Set();
/**
* These events are fired whenever the player added or removed entities from the selection.
*/
var g_EntitySelectionChangeHandlers = new Set();
/**
* These events are fired when the user has performed a hotkey assignment change.
* Currently only fired on init, but to be fired from any hotkey editor dialog.
*/
var g_HotkeyChangeHandlers = new Set();
/**
* List of additional entities to highlight.
*/
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];
/**
* Order in which the panel entities are shown.
*/
var g_PanelEntityOrder = ["Hero", "Relic"];
/**
* Unit classes to be checked for the idle-worker-hotkey.
*/
var g_WorkerTypes = ["FemaleCitizen", "Trader", "FishingBoat", "Citizen"];
/**
* Unit classes to be checked for the military-only-selection modifier and for the idle-warrior-hotkey.
*/
var g_MilitaryTypes = ["Melee", "Ranged"];
function GetSimState()
{
if (!g_SimState)
g_SimState = deepfreeze(Engine.GuiInterfaceCall("GetSimulationState"));
return g_SimState;
}
function GetMultipleEntityStates(ents)
{
if (!ents.length)
return null;
let entityStates = Engine.GuiInterfaceCall("GetMultipleEntityStates", ents);
for (let item of entityStates)
g_EntityStates[item.entId] = item.state && deepfreeze(item.state);
return entityStates;
}
function GetEntityState(entId)
{
if (!g_EntityStates[entId])
{
let entityState = Engine.GuiInterfaceCall("GetEntityState", entId);
g_EntityStates[entId] = entityState && deepfreeze(entityState);
}
return g_EntityStates[entId];
}
/**
* Returns template data calling GetTemplateData defined in GuiInterface.js
* and deepfreezing returned object.
* @param {string} templateName - Data of this template will be returned.
* @param {number|undefined} player - Modifications of this player will be applied to the template.
* If undefined, id of player calling this method will be used.
*/
function GetTemplateData(templateName, player)
{
if (!(templateName in g_TemplateData))
{
let template = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": player });
translateObjectKeys(template, ["specific", "generic", "tooltip"]);
g_TemplateData[templateName] = deepfreeze(template);
}
return g_TemplateData[templateName];
}
function GetTechnologyData(technologyName, civ)
{
if (!g_TechnologyData[civ])
g_TechnologyData[civ] = {};
if (!(technologyName in g_TechnologyData[civ]))
{
let template = GetTechnologyDataHelper(TechnologyTemplates.Get(technologyName), civ, g_ResourceData);
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
g_TechnologyData[civ][technologyName] = deepfreeze(template);
}
return g_TechnologyData[civ][technologyName];
}
function init(initData, hotloadData)
{
if (!g_Settings)
{
Engine.EndGame();
Engine.SwitchGuiPage("page_pregame.xml");
return;
}
// Fallback used by atlas
g_PlayerAssignments = initData ? initData.playerAssignments : { "local": { "player": 1 } };
// Fallback used by atlas and autostart games
if (g_PlayerAssignments.local && !g_PlayerAssignments.local.name)
g_PlayerAssignments.local.name = singleplayerName();
if (initData)
{
g_ReplaySelectionData = initData.replaySelectionData;
g_HasRejoined = initData.isRejoining;
if (initData.savedGUIData)
restoreSavedGameData(initData.savedGUIData);
}
+ if (g_GameAttributes.campaignData)
+ g_CampaignSession = new CampaignSession(g_GameAttributes.campaignData);
+
let mapCache = new MapCache();
g_Cheats = new Cheats();
g_DiplomacyColors = new DiplomacyColors();
g_PlayerViewControl = new PlayerViewControl();
g_PlayerViewControl.registerViewedPlayerChangeHandler(g_DiplomacyColors.updateDisplayedPlayerColors.bind(g_DiplomacyColors));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(g_PlayerViewControl.rebuild.bind(g_PlayerViewControl));
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updateGUIObjects);
g_PauseControl = new PauseControl();
g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay);
g_PlayerViewControl.registerViewedPlayerChangeHandler(resetTemplates);
g_Ambient = new Ambient();
g_AutoFormation = new AutoFormation();
g_Chat = new Chat(g_PlayerViewControl, g_Cheats);
g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);
g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl);
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat);
g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
g_NetworkStatusOverlay = new NetworkStatusOverlay();
g_ObjectivesDialog = new ObjectivesDialog(g_PlayerViewControl, mapCache);
g_OutOfSyncNetwork = new OutOfSyncNetwork();
g_OutOfSyncReplay = new OutOfSyncReplay();
g_PanelEntityManager = new PanelEntityManager(g_PlayerViewControl, g_Selection, g_PanelEntityOrder);
g_PauseOverlay = new PauseOverlay(g_PauseControl);
g_QuitConfirmationDefeat = new QuitConfirmationDefeat();
g_QuitConfirmationReplay = new QuitConfirmationReplay();
g_RangeOverlayManager = new RangeOverlayManager(g_Selection);
g_ResearchProgress = new ResearchProgress(g_PlayerViewControl, g_Selection);
g_TradeDialog = new TradeDialog(g_PlayerViewControl);
g_TopPanel = new TopPanel(g_PlayerViewControl, g_DiplomacyDialog, g_TradeDialog, g_ObjectivesDialog, g_GameSpeedControl);
g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl);
initBatchTrain();
initSelectionPanels();
LoadModificationTemplates();
updatePlayerData();
initializeMusic(); // before changing the perspective
Engine.SetBoundingBoxDebugOverlay(false);
for (let handler of g_PlayersInitHandlers)
handler();
for (let handler of g_HotkeyChangeHandlers)
handler();
if (hotloadData)
{
g_Selection.selected = hotloadData.selection;
g_PlayerAssignments = hotloadData.playerAssignments;
g_Players = hotloadData.player;
}
// TODO: use event instead
onSimulationUpdate();
setTimeout(displayGamestateNotifications, 1000);
}
function registerPlayersInitHandler(handler)
{
g_PlayersInitHandlers.add(handler);
}
function registerPlayersFinishedHandler(handler)
{
g_PlayerFinishedHandlers.add(handler);
}
function registerSimulationUpdateHandler(handler)
{
g_SimulationUpdateHandlers.add(handler);
}
function unregisterSimulationUpdateHandler(handler)
{
g_SimulationUpdateHandlers.delete(handler);
}
function registerEntitySelectionChangeHandler(handler)
{
g_EntitySelectionChangeHandlers.add(handler);
}
function unregisterEntitySelectionChangeHandler(handler)
{
g_EntitySelectionChangeHandlers.delete(handler);
}
function registerHotkeyChangeHandler(handler)
{
g_HotkeyChangeHandlers.add(handler);
}
function updatePlayerData()
{
let simState = GetSimState();
if (!simState)
return;
let playerData = [];
for (let i = 0; i < simState.players.length; ++i)
{
let playerState = simState.players[i];
playerData.push({
"name": playerState.name,
"civ": playerState.civ,
"color": {
"r": playerState.color.r * 255,
"g": playerState.color.g * 255,
"b": playerState.color.b * 255,
"a": playerState.color.a * 255
},
"team": playerState.team,
"teamsLocked": playerState.teamsLocked,
"cheatsEnabled": playerState.cheatsEnabled,
"state": playerState.state,
"isAlly": playerState.isAlly,
"isMutualAlly": playerState.isMutualAlly,
"isNeutral": playerState.isNeutral,
"isEnemy": playerState.isEnemy,
"guid": undefined, // network guid for players controlled by hosts
"offline": g_Players[i] && !!g_Players[i].offline
});
}
for (let guid in g_PlayerAssignments)
{
let playerID = g_PlayerAssignments[guid].player;
if (!playerData[playerID])
continue;
playerData[playerID].guid = guid;
playerData[playerID].name = g_PlayerAssignments[guid].name;
}
g_Players = playerData;
}
/**
* @param {number} ent - The entity to get its ID for.
* @return {number} - The entity ID of the entity or of its garrisonHolder.
*/
function getEntityOrHolder(ent)
{
let entState = GetEntityState(ent);
if (entState && !entState.position && entState.garrisonable && entState.garrisonable.holder != INVALID_ENTITY)
return getEntityOrHolder(entState.garrisonable.holder);
return ent;
}
function initializeMusic()
{
initMusic();
if (g_ViewedPlayer != -1 && g_CivData[g_Players[g_ViewedPlayer].civ].Music)
global.music.storeTracks(g_CivData[g_Players[g_ViewedPlayer].civ].Music);
global.music.setState(global.music.states.PEACE);
}
function resetTemplates()
{
// Update GUI and clear player-dependent cache
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
// TODO: do this more selectively
onSimulationUpdate();
}
/**
* Returns true if the player with that ID is in observermode.
*/
function isPlayerObserver(playerID)
{
let playerStates = GetSimState().players;
return !playerStates[playerID] || playerStates[playerID].state != "active";
}
/**
* Returns true if the current user can issue commands for that player.
*/
function controlsPlayer(playerID)
{
let playerStates = GetSimState().players;
return !!playerStates[Engine.GetPlayerID()] &&
playerStates[Engine.GetPlayerID()].controlsAll ||
Engine.GetPlayerID() == playerID &&
!!playerStates[playerID] &&
playerStates[playerID].state != "defeated";
}
/**
* Called when one or more players have won or were defeated.
*
* @param {array} - IDs of the players who have won or were defeated.
* @param {Object} - a plural string stating the victory reason.
* @param {boolean} - whether these players have won or lost.
*/
function playersFinished(players, victoryString, won)
{
addChatMessage({
"type": "playerstate",
"message": victoryString,
"players": players
});
updatePlayerData();
// TODO: The other calls in this function should move too
for (let handler of g_PlayerFinishedHandlers)
handler(players, won);
if (players.indexOf(Engine.GetPlayerID()) == -1 || Engine.IsAtlasRunning())
return;
global.music.setState(
won ?
global.music.states.VICTORY :
global.music.states.DEFEAT
);
}
function resumeGame()
{
g_PauseControl.implicitResume();
}
function closeOpenDialogs()
{
g_Menu.close();
g_Chat.closePage();
g_DiplomacyDialog.close();
g_ObjectivesDialog.close();
g_TradeDialog.close();
}
function endGame()
{
// Before ending the game
let replayDirectory = Engine.GetCurrentReplayDirectory();
let simData = Engine.GuiInterfaceCall("GetReplayMetadata");
let playerID = Engine.GetPlayerID();
Engine.EndGame();
// After the replay file was closed in EndGame
// Done here to keep EndGame small
if (!g_IsReplay)
Engine.AddReplayToCache(replayDirectory);
if (g_IsController && Engine.HasXmppClient())
Engine.SendUnregisterGame();
- Engine.SwitchGuiPage("page_summary.xml", {
+ let summaryData = {
"sim": simData,
"gui": {
"dialog": false,
"assignedPlayer": playerID,
"disconnected": g_Disconnected,
"isReplay": g_IsReplay,
"replayDirectory": !g_HasRejoined && replayDirectory,
"replaySelectionData": g_ReplaySelectionData
}
- });
+ };
+
+ if (g_GameAttributes.campaignData)
+ {
+ let menu = g_CampaignSession.getMenu();
+ if (g_GameAttributes.campaignData.skipSummary)
+ {
+ Engine.SwitchGuiPage(menu);
+ return;
+ }
+ summaryData.campaignData = { "filename": g_GameAttributes.campaignData.run };
+ summaryData.nextPage = menu;
+ }
+
+ Engine.SwitchGuiPage("page_summary.xml", summaryData);
}
// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
return {
"selection": g_Selection.selected,
"playerAssignments": g_PlayerAssignments,
"player": g_Players,
};
}
function getSavedGameData()
{
return {
"groups": g_Groups.groups
};
}
function restoreSavedGameData(data)
{
// Restore camera if any
if (data.camera)
Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
data.camera.RotX, data.camera.RotY, data.camera.Zoom);
// Clear selection when loading a game
g_Selection.reset();
// Restore control groups
for (let groupNumber in data.groups)
{
g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
}
updateGroups();
}
/**
* Called every frame.
*/
function onTick()
{
if (!g_Settings)
return;
let now = Date.now();
let tickLength = now - g_LastTickTime;
g_LastTickTime = now;
handleNetMessages();
updateCursorAndTooltip();
if (g_Selection.dirty)
{
g_Selection.dirty = false;
// When selection changed, get the entityStates of new entities
GetMultipleEntityStates(g_Selection.toList().filter(entId => !g_EntityStates[entId]));
for (let handler of g_EntitySelectionChangeHandlers)
handler();
updateGUIObjects();
// Display rally points for selected structures.
if (Engine.GetPlayerID() != -1)
Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
}
else if (g_ShowAllStatusBars && now % g_StatusBarUpdate <= tickLength)
recalculateStatusBarDisplay();
updateTimers();
Engine.GuiInterfaceCall("ClearRenamedEntities");
}
function onSimulationUpdate()
{
// Templates change depending on technologies and auras, so they have to be reloaded after such a change.
// g_TechnologyData data never changes, so it shouldn't be deleted.
g_EntityStates = {};
if (Engine.GuiInterfaceCall("IsTemplateModified"))
{
g_TemplateData = {};
Engine.GuiInterfaceCall("ResetTemplateModified");
}
g_SimState = undefined;
// Some changes may require re-rendering the selection.
if (Engine.GuiInterfaceCall("IsSelectionDirty"))
{
g_Selection.onChange();
Engine.GuiInterfaceCall("ResetSelectionDirty");
}
if (!GetSimState())
return;
GetMultipleEntityStates(g_Selection.toList());
for (let handler of g_SimulationUpdateHandlers)
handler();
// TODO: Move to handlers
updateCinemaPath();
handleNotifications();
updateGUIObjects();
}
function toggleGUI()
{
g_ShowGUI = !g_ShowGUI;
updateCinemaPath();
}
function updateCinemaPath()
{
let isPlayingCinemaPath = GetSimState().cinemaPlaying && !g_Disconnected;
Engine.GetGUIObjectByName("session").hidden = !g_ShowGUI || isPlayingCinemaPath;
Engine.ConfigDB_CreateValue("user", "silhouettes", !isPlayingCinemaPath && Engine.ConfigDB_GetValue("user", "silhouettes") == "true" ? "true" : "false");
}
// TODO: Use event subscription onSimulationUpdate, onEntitySelectionChange, onPlayerViewChange, ... instead
function updateGUIObjects()
{
g_Selection.update();
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay();
if (g_ShowGuarding || g_ShowGuarded)
updateAdditionalHighlight();
updateGroups();
updateSelectionDetails();
updateBuildingPlacementPreview();
if (!g_IsObserver)
{
// Update music state on basis of battle state.
let battleState = Engine.GuiInterfaceCall("GetBattleState", g_ViewedPlayer);
if (battleState)
global.music.setState(global.music.states[battleState]);
}
}
function updateGroups()
{
g_Groups.update();
// Determine the sum of the costs of a given template
let getCostSum = (ent) => {
let cost = GetTemplateData(GetEntityState(ent).template).cost;
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
};
for (let i in Engine.GetGUIObjectByName("unitGroupPanel").children)
{
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = i;
let button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]");
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
button.onPress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); }; })(i);
button.onDoublePress = (function(i) { return function() { performGroup("snap", i); }; })(i);
button.onPressRight = (function(i) { return function() { performGroup("breakUp", i); }; })(i);
// Choose the icon of the most common template (or the most costly if it's not unique)
if (g_Groups.groups[i].getTotalCount() > 0)
{
let icon = GetTemplateData(GetEntityState(g_Groups.groups[i].getEntsGrouped().reduce((pre, cur) => {
if (pre.ents.length == cur.ents.length)
return getCostSum(pre.ents[0]) > getCostSum(cur.ents[0]) ? pre : cur;
return pre.ents.length > cur.ents.length ? pre : cur;
}).ents[0]).template).icon;
Engine.GetGUIObjectByName("unitGroupIcon[" + i + "]").sprite =
icon ? ("stretched:session/portraits/" + icon) : "groupsIcon";
}
setPanelObjectPosition(button, i, 1);
}
}
/**
* Toggles the display of status bars for all of the player's entities.
*
* @param {boolean} remove - Whether to hide all previously shown status bars.
*/
function recalculateStatusBarDisplay(remove = false)
{
let entities;
if (g_ShowAllStatusBars && !remove)
entities = g_ViewedPlayer == -1 ?
Engine.PickNonGaiaEntitiesOnScreen() :
Engine.PickPlayerEntitiesOnScreen(g_ViewedPlayer);
else
{
let selected = g_Selection.toList();
for (let ent in g_Selection.highlighted)
selected.push(g_Selection.highlighted[ent]);
// Remove selected entities from the 'all entities' array,
// to avoid disabling their status bars.
entities = Engine.GuiInterfaceCall(
g_ViewedPlayer == -1 ? "GetNonGaiaEntities" : "GetPlayerEntities", {
"viewedPlayer": g_ViewedPlayer
}).filter(idx => selected.indexOf(idx) == -1);
}
Engine.GuiInterfaceCall("SetStatusBars", {
"entities": entities,
"enabled": g_ShowAllStatusBars && !remove,
"showRank": Engine.ConfigDB_GetValue("user", "gui.session.rankabovestatusbar") == "true",
"showExperience": Engine.ConfigDB_GetValue("user", "gui.session.experiencestatusbar") == "true"
});
}
function removeStatusBarDisplay()
{
if (g_ShowAllStatusBars)
recalculateStatusBarDisplay(true);
}
/**
* Inverts the given configuration boolean and returns the current state.
* For example "silhouettes".
*/
function toggleConfigBool(configName)
{
let enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
Engine.ConfigDB_CreateAndWriteValueToFile("user", configName, String(enabled), "config/user.cfg");
return enabled;
}
// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
let entsAdd = []; // list of entities units to be highlighted
let entsRemove = [];
let highlighted = g_Selection.toList();
for (let ent in g_Selection.highlighted)
highlighted.push(g_Selection.highlighted[ent]);
if (g_ShowGuarding)
// flag the guarding entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.guard || !state.guard.entities.length)
continue;
for (let ent of state.guard.entities)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
if (g_ShowGuarded)
// flag the guarded entities to add in this additional highlight
for (let sel in g_Selection.selected)
{
let state = GetEntityState(g_Selection.selected[sel]);
if (!state.unitAI || !state.unitAI.isGuarding)
continue;
let ent = state.unitAI.isGuarding;
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
entsAdd.push(ent);
}
// flag the entities to remove (from the previously added) from this additional highlight
for (let ent of g_AdditionalHighlight)
if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
entsRemove.push(ent);
_setHighlight(entsAdd, g_HighlightedAlpha, true);
_setHighlight(entsRemove, 0, false);
g_AdditionalHighlight = entsAdd;
}
Index: ps/trunk/binaries/data/mods/public/gui/summary/summary.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/summary/summary.js (revision 24979)
@@ -1,594 +1,596 @@
const g_CivData = loadCivData(false, false);
var g_ScorePanelsData;
var g_MaxHeadingTitle = 9;
var g_LongHeadingWidth = 250;
var g_PlayerBoxYSize = 40;
var g_PlayerBoxGap = 2;
var g_PlayerBoxAlpha = 50;
var g_TeamsBoxYStart = 40;
var g_TypeColors = {
"blue": "196 198 255",
"green": "201 255 200",
"red": "255 213 213",
"yellow": "255 255 157"
};
/**
* Colors, captions and format used for units, structures, etc. types
*/
var g_SummaryTypes = {
"percent": {
"color": "",
"caption": "%",
"postfix": "%"
},
"trained": {
"color": g_TypeColors.green,
"caption": translate("Trained"),
"postfix": " / "
},
"constructed": {
"color": g_TypeColors.green,
"caption": translate("Constructed"),
"postfix": " / "
},
"gathered": {
"color": g_TypeColors.green,
"caption": translate("Gathered"),
"postfix": " / "
},
"count": {
"caption": translate("Count"),
"hideInSummary": true
},
"sent": {
"color": g_TypeColors.green,
"caption": translate("Sent"),
"postfix": " / "
},
"bought": {
"color": g_TypeColors.green,
"caption": translate("Bought"),
"postfix": " / "
},
"income": {
"color": g_TypeColors.green,
"caption": translate("Income"),
"postfix": " / "
},
"captured": {
"color": g_TypeColors.yellow,
"caption": translate("Captured"),
"postfix": " / "
},
"succeeded": {
"color": g_TypeColors.green,
"caption": translate("Succeeded"),
"postfix": " / "
},
"destroyed": {
"color": g_TypeColors.blue,
"caption": translate("Destroyed"),
"postfix": "\n"
},
"killed": {
"color": g_TypeColors.blue,
"caption": translate("Killed"),
"postfix": "\n"
},
"lost": {
"color": g_TypeColors.red,
"caption": translate("Lost"),
"postfix": ""
},
"used": {
"color": g_TypeColors.red,
"caption": translate("Used"),
"postfix": ""
},
"received": {
"color": g_TypeColors.red,
"caption": translate("Received"),
"postfix": ""
},
"population": {
"color": g_TypeColors.red,
"caption": translate("Population"),
"postfix": ""
},
"sold": {
"color": g_TypeColors.red,
"caption": translate("Sold"),
"postfix": ""
},
"outcome": {
"color": g_TypeColors.red,
"caption": translate("Outcome"),
"postfix": ""
},
"failed": {
"color": g_TypeColors.red,
"caption": translate("Failed"),
"postfix": ""
}
};
// Translation: Unicode encoded infinity symbol indicating a division by zero in the summary screen.
var g_InfinitySymbol = translate("\u221E");
var g_Teams = [];
var g_PlayerCount;
var g_GameData;
var g_ResourceData = new Resources();
/**
* Selected chart indexes.
*/
var g_SelectedChart = {
"category": [0, 0],
"value": [0, 1],
"type": [0, 0]
};
function init(data)
{
initSummaryData(data);
initGUISummary();
}
function initSummaryData(data)
{
g_GameData = data;
g_ScorePanelsData = getScorePanelsData();
let teamCharts = false;
if (data && data.gui && data.gui.summarySelection)
{
g_TabCategorySelected = data.gui.summarySelection.panel;
g_SelectedChart = data.gui.summarySelection.charts;
teamCharts = data.gui.summarySelection.teamCharts;
}
Engine.GetGUIObjectByName("toggleTeamBox").checked = g_Teams && teamCharts;
initTeamData();
calculateTeamCounterDataHelper();
}
function initGUISummary()
{
initGUIWindow();
initPlayerBoxPositions();
initGUICharts();
initGUILabels();
initGUIButtons();
}
/**
* Sets the style and title of the page.
*/
function initGUIWindow()
{
let summaryWindow = Engine.GetGUIObjectByName("summaryWindow");
summaryWindow.sprite = g_GameData.gui.dialog ? "ModernDialog" : "ModernWindow";
summaryWindow.size = g_GameData.gui.dialog ? "16 24 100%-16 100%-24" : "0 0 100% 100%";
Engine.GetGUIObjectByName("summaryWindowTitle").size = g_GameData.gui.dialog ? "50%-128 -16 50%+128 16" : "50%-128 4 50%+128 36";
}
function selectPanelGUI(panel)
{
adjustTabDividers(Engine.GetGUIObjectByName("tabButton[" + panel + "]").size);
let generalPanel = Engine.GetGUIObjectByName("generalPanel");
let chartsPanel = Engine.GetGUIObjectByName("chartsPanel");
// We assume all scorePanels come before the charts.
let chartsHidden = panel < g_ScorePanelsData.length;
generalPanel.hidden = !chartsHidden;
chartsPanel.hidden = chartsHidden;
if (chartsHidden)
updatePanelData(g_ScorePanelsData[panel]);
else
[0, 1].forEach(updateCategoryDropdown);
}
function constructPlayersWithColor(color, playerListing)
{
return sprintf(translateWithContext("Player listing with color indicator",
"%(colorIndicator)s %(playerListing)s"),
{
"colorIndicator": setStringTags(translateWithContext(
"Charts player color indicator", "■"), { "color": color }),
"playerListing": playerListing
});
}
function updateChartColorAndLegend()
{
let playerColors = [];
for (let i = 1; i <= g_PlayerCount; ++i)
{
let playerState = g_GameData.sim.playerStates[i];
playerColors.push(
Math.floor(playerState.color.r * 255) + " " +
Math.floor(playerState.color.g * 255) + " " +
Math.floor(playerState.color.b * 255)
);
}
for (let i = 0; i < 2; ++i)
Engine.GetGUIObjectByName("chart[" + i + "]").series_color =
Engine.GetGUIObjectByName("toggleTeamBox").checked ?
g_Teams.filter(el => el !== null).map(players => playerColors[players[0] - 1]) :
playerColors;
let chartLegend = Engine.GetGUIObjectByName("chartLegend");
chartLegend.caption = (Engine.GetGUIObjectByName("toggleTeamBox").checked ?
g_Teams.filter(el => el !== null).map(players =>
constructPlayersWithColor(playerColors[players[0] - 1], players.map(player =>
g_GameData.sim.playerStates[player].name
).join(translateWithContext("Player listing", ", ")))
) :
g_GameData.sim.playerStates.slice(1).map((state, index) =>
constructPlayersWithColor(playerColors[index], state.name))
).join(" ");
}
function initGUICharts()
{
updateChartColorAndLegend();
let chart1Part = Engine.GetGUIObjectByName("chart[1]Part");
let chart1PartSize = chart1Part.size;
chart1PartSize.rright += 50;
chart1PartSize.rleft += 50;
chart1PartSize.right -= 5;
chart1PartSize.left -= 5;
chart1Part.size = chart1PartSize;
Engine.GetGUIObjectByName("toggleTeam").hidden = !g_Teams;
}
function resizeDropdown(dropdown)
{
let size = dropdown.size;
size.bottom = dropdown.size.top +
(Engine.GetTextWidth(dropdown.font, dropdown.list[dropdown.selected]) >
dropdown.size.right - dropdown.size.left - 32 ? 42 : 27);
dropdown.size = size;
}
function updateCategoryDropdown(number)
{
let chartCategory = Engine.GetGUIObjectByName("chart[" + number + "]CategorySelection");
chartCategory.list_data = g_ScorePanelsData.map((panel, idx) => idx);
chartCategory.list = g_ScorePanelsData.map(panel => panel.label);
chartCategory.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
if (g_SelectedChart.category[number] != this.selected)
{
g_SelectedChart.category[number] = this.selected;
g_SelectedChart.value[number] = 0;
g_SelectedChart.type[number] = 0;
}
resizeDropdown(this);
updateValueDropdown(number, this.list_data[this.selected]);
};
chartCategory.selected = g_SelectedChart.category[number];
}
function updateValueDropdown(number, category)
{
let chartValue = Engine.GetGUIObjectByName("chart[" + number + "]ValueSelection");
let list = g_ScorePanelsData[category].headings.map(heading => heading.caption);
list.shift();
chartValue.list = list;
let list_data = g_ScorePanelsData[category].headings.map(heading => heading.identifier);
list_data.shift();
chartValue.list_data = list_data;
chartValue.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
if (g_SelectedChart.value[number] != this.selected)
{
g_SelectedChart.value[number] = this.selected;
g_SelectedChart.type[number] = 0;
}
resizeDropdown(this);
updateTypeDropdown(number, category, this.list_data[this.selected], this.selected);
};
chartValue.selected = g_SelectedChart.value[number];
}
function updateTypeDropdown(number, category, item, itemNumber)
{
let testValue = g_ScorePanelsData[category].counters[itemNumber].fn(g_GameData.sim.playerStates[1], 0, item);
let hide = !g_ScorePanelsData[category].counters[itemNumber].fn ||
typeof testValue != "object" || Object.keys(testValue).length < 2;
Engine.GetGUIObjectByName("chart[" + number + "]TypeLabel").hidden = hide;
let chartType = Engine.GetGUIObjectByName("chart[" + number + "]TypeSelection");
chartType.hidden = hide;
if (hide)
{
updateChart(number, category, item, itemNumber, Object.keys(testValue)[0] || undefined);
return;
}
chartType.list = Object.keys(testValue).map(type => g_SummaryTypes[type].caption);
chartType.list_data = Object.keys(testValue);
chartType.onSelectionChange = function() {
if (!this.list_data[this.selected])
return;
g_SelectedChart.type[number] = this.selected;
resizeDropdown(this);
updateChart(number, category, item, itemNumber, this.list_data[this.selected]);
};
chartType.selected = g_SelectedChart.type[number];
}
function updateChart(number, category, item, itemNumber, type)
{
if (!g_ScorePanelsData[category].counters[itemNumber].fn)
return;
let chart = Engine.GetGUIObjectByName("chart[" + number + "]");
chart.format_y = g_ScorePanelsData[category].headings[itemNumber + 1].format || "INTEGER";
Engine.GetGUIObjectByName("chart[" + number + "]XAxisLabel").caption = translate("Time elapsed");
let series = [];
if (Engine.GetGUIObjectByName("toggleTeamBox").checked)
for (let team in g_Teams)
{
let data = [];
for (let index in g_GameData.sim.playerStates[1].sequences.time)
{
let value = g_ScorePanelsData[category].teamCounterFn(team, index, item,
g_ScorePanelsData[category].counters, g_ScorePanelsData[category].headings);
if (type)
value = value[type];
data.push([g_GameData.sim.playerStates[1].sequences.time[index], value]);
}
series.push(data);
}
else
for (let j = 1; j <= g_PlayerCount; ++j)
{
let playerState = g_GameData.sim.playerStates[j];
let data = [];
for (let index in playerState.sequences.time)
{
let value = g_ScorePanelsData[category].counters[itemNumber].fn(playerState, index, item);
if (type)
value = value[type];
data.push([playerState.sequences.time[index], value]);
}
series.push(data);
}
chart.series = series;
}
function adjustTabDividers(tabSize)
{
let tabButtonsLeft = Engine.GetGUIObjectByName("tabButtonsFrame").size.left;
let leftSpacer = Engine.GetGUIObjectByName("tabDividerLeft");
let leftSpacerSize = leftSpacer.size;
leftSpacerSize.right = tabSize.left + tabButtonsLeft + 2;
leftSpacer.size = leftSpacerSize;
let rightSpacer = Engine.GetGUIObjectByName("tabDividerRight");
let rightSpacerSize = rightSpacer.size;
rightSpacerSize.left = tabSize.right + tabButtonsLeft - 2;
rightSpacer.size = rightSpacerSize;
}
function updatePanelData(panelInfo)
{
resetGeneralPanel();
updateGeneralPanelHeadings(panelInfo.headings);
updateGeneralPanelTitles(panelInfo.titleHeadings);
let rowPlayerObjectWidth = updateGeneralPanelCounter(panelInfo.counters);
updateGeneralPanelTeams();
let index = g_GameData.sim.playerStates[1].sequences.time.length - 1;
let playerBoxesCounts = [];
for (let i = 0; i < g_PlayerCount; ++i)
{
let playerState = g_GameData.sim.playerStates[i + 1];
if (!playerBoxesCounts[playerState.team + 1])
playerBoxesCounts[playerState.team + 1] = 1;
else
playerBoxesCounts[playerState.team + 1] += 1;
let positionObject = playerBoxesCounts[playerState.team + 1] - 1;
let rowPlayer = "playerBox[" + positionObject + "]";
let playerOutcome = "playerOutcome[" + positionObject + "]";
let playerNameColumn = "playerName[" + positionObject + "]";
let playerCivicBoxColumn = "civIcon[" + positionObject + "]";
let playerCounterValue = "valueData[" + positionObject + "]";
if (playerState.team != -1)
{
rowPlayer = "playerBoxt[" + playerState.team + "][" + positionObject + "]";
playerOutcome = "playerOutcomet[" + playerState.team + "][" + positionObject + "]";
playerNameColumn = "playerNamet[" + playerState.team + "][" + positionObject + "]";
playerCivicBoxColumn = "civIcont[" + playerState.team + "][" + positionObject + "]";
playerCounterValue = "valueDataTeam[" + playerState.team + "][" + positionObject + "]";
}
let colorString = "color: " +
Math.floor(playerState.color.r * 255) + " " +
Math.floor(playerState.color.g * 255) + " " +
Math.floor(playerState.color.b * 255);
let rowPlayerObject = Engine.GetGUIObjectByName(rowPlayer);
rowPlayerObject.hidden = false;
rowPlayerObject.sprite = colorString + " " + g_PlayerBoxAlpha;
let boxSize = rowPlayerObject.size;
boxSize.right = rowPlayerObjectWidth;
rowPlayerObject.size = boxSize;
setOutcomeIcon(playerState.state, Engine.GetGUIObjectByName(playerOutcome));
playerNameColumn = Engine.GetGUIObjectByName(playerNameColumn);
playerNameColumn.caption = g_GameData.sim.playerStates[i + 1].name;
playerNameColumn.tooltip = translateAISettings(g_GameData.sim.mapSettings.PlayerData[i + 1]);
let civIcon = Engine.GetGUIObjectByName(playerCivicBoxColumn);
civIcon.sprite = "stretched:" + g_CivData[playerState.civ].Emblem;
civIcon.tooltip = g_CivData[playerState.civ].Name;
updateCountersPlayer(playerState, panelInfo.counters, panelInfo.headings, playerCounterValue, index);
}
let teamCounterFn = panelInfo.teamCounterFn;
if (g_Teams && teamCounterFn)
updateCountersTeam(teamCounterFn, panelInfo.counters, panelInfo.headings, index);
}
function continueButton()
{
let summarySelection = {
"panel": g_TabCategorySelected,
"charts": g_SelectedChart,
"teamCharts": Engine.GetGUIObjectByName("toggleTeamBox").checked
};
if (g_GameData.gui.isInGame)
Engine.PopGuiPage({
"summarySelection": summarySelection
});
else if (g_GameData.gui.dialog)
Engine.PopGuiPage();
else if (Engine.HasXmppClient())
Engine.SwitchGuiPage("page_lobby.xml", { "dialog": false });
else if (g_GameData.gui.isReplay)
Engine.SwitchGuiPage("page_replaymenu.xml", {
"replaySelectionData": g_GameData.gui.replaySelectionData,
"summarySelection": summarySelection
});
+ else if (g_GameData.campaignData)
+ Engine.SwitchGuiPage(g_GameData.nextPage, g_GameData.campaignData);
else
Engine.SwitchGuiPage("page_pregame.xml");
}
function startReplay()
{
if (!Engine.StartVisualReplay(g_GameData.gui.replayDirectory))
{
warn("Replay file not found!");
return;
}
Engine.SwitchGuiPage("page_loading.xml", {
"attribs": Engine.GetReplayAttributes(g_GameData.gui.replayDirectory),
"playerAssignments": {
"local": {
"name": singleplayerName(),
"player": -1
}
},
"savedGUIData": "",
"isReplay": true,
"replaySelectionData": g_GameData.gui.replaySelectionData
});
}
function initGUILabels()
{
let assignedState = g_GameData.sim.playerStates[g_GameData.gui.assignedPlayer || -1];
Engine.GetGUIObjectByName("summaryText").caption =
g_GameData.gui.isInGame ?
translate("Current Scores") :
g_GameData.gui.isReplay ?
translate("Scores at the end of the game.") :
g_GameData.gui.disconnected ?
translate("You have been disconnected.") :
!assignedState ?
translate("You have left the game.") :
assignedState.state == "won" ?
translate("You have won the battle!") :
assignedState.state == "defeated" ?
translate("You have been defeated…") :
translate("You have abandoned the game.");
Engine.GetGUIObjectByName("timeElapsed").caption = sprintf(
translate("Game time elapsed: %(time)s"), {
"time": timeToString(g_GameData.sim.timeElapsed)
});
let mapType = g_Settings.MapTypes.find(type => type.Name == g_GameData.sim.mapSettings.mapType);
let mapSize = g_Settings.MapSizes.find(size => size.Tiles == g_GameData.sim.mapSettings.Size || 0);
Engine.GetGUIObjectByName("mapName").caption = sprintf(
translate("%(mapName)s - %(mapType)s"), {
"mapName": translate(g_GameData.sim.mapSettings.Name),
"mapType": mapSize ? mapSize.Name : (mapType ? mapType.Title : "")
});
}
function initGUIButtons()
{
let replayButton = Engine.GetGUIObjectByName("replayButton");
replayButton.hidden = g_GameData.gui.isInGame || !g_GameData.gui.replayDirectory;
let lobbyButton = Engine.GetGUIObjectByName("lobbyButton");
lobbyButton.tooltip = colorizeHotkey(translate("%(hotkey)s: Toggle the multiplayer lobby in a dialog window."), "lobby");
lobbyButton.hidden = g_GameData.gui.isInGame || !Engine.HasXmppClient();
// Right-align lobby button
let lobbyButtonSize = lobbyButton.size;
let lobbyButtonWidth = lobbyButtonSize.right - lobbyButtonSize.left;
lobbyButtonSize.right = (replayButton.hidden ? Engine.GetGUIObjectByName("continueButton").size.left : replayButton.size.left) - 10;
lobbyButtonSize.left = lobbyButtonSize.right - lobbyButtonWidth;
lobbyButton.size = lobbyButtonSize;
let allPanelsData = g_ScorePanelsData.concat(g_ChartPanelsData);
for (let tab in allPanelsData)
allPanelsData[tab].tooltip =
sprintf(translate("Toggle the %(name)s summary tab."), { "name": allPanelsData[tab].label }) +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a summary tab right."), "tab.next") +
colorizeHotkey("\n" + translate("Use %(hotkey)s to move a summary tab left."), "tab.prev");
placeTabButtons(
allPanelsData,
true,
g_TabButtonWidth,
g_TabButtonDist,
selectPanel,
selectPanelGUI);
}
function initTeamData()
{
// Panels
g_PlayerCount = g_GameData.sim.playerStates.length - 1;
if (g_GameData.sim.mapSettings.LockTeams)
{
// Count teams
for (let player = 1; player <= g_PlayerCount; ++player)
{
let playerTeam = g_GameData.sim.playerStates[player].team;
if (!g_Teams[playerTeam])
g_Teams[playerTeam] = [];
g_Teams[playerTeam].push(player);
}
if (g_Teams.every(team => team && team.length < 2))
g_Teams = false; // Each player has his own team. Displaying teams makes no sense.
}
else
g_Teams = false;
// Erase teams data if teams are not displayed
if (!g_Teams)
for (let p = 0; p < g_PlayerCount; ++p)
g_GameData.sim.playerStates[p+1].team = -1;
}
Index: ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js
===================================================================
--- ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/simulation/components/GuiInterface.js (revision 24979)
@@ -1,2081 +1,2093 @@
function GuiInterface() {}
GuiInterface.prototype.Schema =
"";
GuiInterface.prototype.Serialize = function()
{
// This component isn't network-synchronized for the biggest part,
// so most of the attributes shouldn't be serialized.
// Return an object with a small selection of deterministic data.
return {
"timeNotifications": this.timeNotifications,
"timeNotificationID": this.timeNotificationID
};
};
GuiInterface.prototype.Deserialize = function(data)
{
this.Init();
this.timeNotifications = data.timeNotifications;
this.timeNotificationID = data.timeNotificationID;
};
GuiInterface.prototype.Init = function()
{
this.placementEntity = undefined; // = undefined or [templateName, entityID]
this.placementWallEntities = undefined;
this.placementWallLastAngle = 0;
this.notifications = [];
this.renamedEntities = [];
this.miragedEntities = [];
this.timeNotificationID = 1;
this.timeNotifications = [];
this.entsRallyPointsDisplayed = [];
this.entsWithAuraAndStatusBars = new Set();
this.enabledVisualRangeOverlayTypes = {};
this.templateModified = {};
this.selectionDirty = {};
this.obstructionSnap = new ObstructionSnap();
};
/*
* All of the functions defined below are called via Engine.GuiInterfaceCall(name, arg)
* from GUI scripts, and executed here with arguments (player, arg).
*
* CAUTION: The input to the functions in this module is not network-synchronised, so it
* mustn't affect the simulation state (i.e. the data that is serialised and can affect
* the behaviour of the rest of the simulation) else it'll cause out-of-sync errors.
*/
/**
* Returns global information about the current game state.
* This is used by the GUI and also by AI scripts.
*/
GuiInterface.prototype.GetSimulationState = function()
{
let ret = {
"players": []
};
let cmpPlayerManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager);
let numPlayers = cmpPlayerManager.GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i);
let cmpPlayerEntityLimits = QueryPlayerIDInterface(i, IID_EntityLimits);
// Work out which phase we are in.
let phase = "";
let cmpTechnologyManager = QueryPlayerIDInterface(i, IID_TechnologyManager);
if (cmpTechnologyManager)
{
if (cmpTechnologyManager.IsTechnologyResearched("phase_city"))
phase = "city";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_town"))
phase = "town";
else if (cmpTechnologyManager.IsTechnologyResearched("phase_village"))
phase = "village";
}
let allies = [];
let mutualAllies = [];
let neutrals = [];
let enemies = [];
for (let j = 0; j < numPlayers; ++j)
{
allies[j] = cmpPlayer.IsAlly(j);
mutualAllies[j] = cmpPlayer.IsMutualAlly(j);
neutrals[j] = cmpPlayer.IsNeutral(j);
enemies[j] = cmpPlayer.IsEnemy(j);
}
ret.players.push({
"name": cmpPlayer.GetName(),
"civ": cmpPlayer.GetCiv(),
"color": cmpPlayer.GetColor(),
"controlsAll": cmpPlayer.CanControlAllUnits(),
"popCount": cmpPlayer.GetPopulationCount(),
"popLimit": cmpPlayer.GetPopulationLimit(),
"popMax": cmpPlayer.GetMaxPopulation(),
"panelEntities": cmpPlayer.GetPanelEntities(),
"resourceCounts": cmpPlayer.GetResourceCounts(),
"resourceGatherers": cmpPlayer.GetResourceGatherers(),
"trainingBlocked": cmpPlayer.IsTrainingBlocked(),
"state": cmpPlayer.GetState(),
"team": cmpPlayer.GetTeam(),
"teamsLocked": cmpPlayer.GetLockTeams(),
"cheatsEnabled": cmpPlayer.GetCheatsEnabled(),
"disabledTemplates": cmpPlayer.GetDisabledTemplates(),
"disabledTechnologies": cmpPlayer.GetDisabledTechnologies(),
"hasSharedDropsites": cmpPlayer.HasSharedDropsites(),
"hasSharedLos": cmpPlayer.HasSharedLos(),
"spyCostMultiplier": cmpPlayer.GetSpyCostMultiplier(),
"phase": phase,
"isAlly": allies,
"isMutualAlly": mutualAllies,
"isNeutral": neutrals,
"isEnemy": enemies,
"entityLimits": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimits() : null,
"entityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetCounts() : null,
"matchEntityCounts": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetMatchCounts() : null,
"entityLimitChangers": cmpPlayerEntityLimits ? cmpPlayerEntityLimits.GetLimitChangers() : null,
"researchQueued": cmpTechnologyManager ? cmpTechnologyManager.GetQueuedResearch() : null,
"researchStarted": cmpTechnologyManager ? cmpTechnologyManager.GetStartedTechs() : null,
"researchedTechs": cmpTechnologyManager ? cmpTechnologyManager.GetResearchedTechs() : null,
"classCounts": cmpTechnologyManager ? cmpTechnologyManager.GetClassCounts() : null,
"typeCountsByClass": cmpTechnologyManager ? cmpTechnologyManager.GetTypeCountsByClass() : null,
"canBarter": cmpPlayer.CanBarter(),
"barterPrices": Engine.QueryInterface(SYSTEM_ENTITY, IID_Barter).GetPrices(cmpPlayer)
});
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (cmpRangeManager)
ret.circularMap = cmpRangeManager.GetLosCircular();
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (cmpTerrain)
ret.mapSize = cmpTerrain.GetMapSize();
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
ret.timeElapsed = cmpTimer.GetTime();
let cmpCeasefireManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CeasefireManager);
if (cmpCeasefireManager)
{
ret.ceasefireActive = cmpCeasefireManager.IsCeasefireActive();
ret.ceasefireTimeRemaining = ret.ceasefireActive ? cmpCeasefireManager.GetCeasefireStartedTime() + cmpCeasefireManager.GetCeasefireTime() - ret.timeElapsed : 0;
}
let cmpCinemaManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_CinemaManager);
if (cmpCinemaManager)
ret.cinemaPlaying = cmpCinemaManager.IsPlaying();
let cmpEndGameManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_EndGameManager);
ret.victoryConditions = cmpEndGameManager.GetVictoryConditions();
ret.alliedVictory = cmpEndGameManager.GetAlliedVictory();
ret.maxWorldPopulation = cmpPlayerManager.GetMaxWorldPopulation();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].statistics = cmpPlayerStatisticsTracker.GetBasicStatistics();
}
return ret;
};
/**
* Returns global information about the current game state, plus statistics.
* This is used by the GUI at the end of a game, in the summary screen.
* Note: Amongst statistics, the team exploration map percentage is computed from
* scratch, so the extended simulation state should not be requested too often.
*/
GuiInterface.prototype.GetExtendedSimulationState = function()
{
let ret = this.GetSimulationState();
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 0; i < numPlayers; ++i)
{
let cmpPlayerStatisticsTracker = QueryPlayerIDInterface(i, IID_StatisticsTracker);
if (cmpPlayerStatisticsTracker)
ret.players[i].sequences = cmpPlayerStatisticsTracker.GetSequences();
}
return ret;
};
/**
* Returns the gamesettings that were chosen at the time the match started.
*/
GuiInterface.prototype.GetInitAttributes = function()
{
return InitAttributes;
};
/**
* This data will be stored in the replay metadata file after a match has been finished recording.
*/
GuiInterface.prototype.GetReplayMetadata = function()
{
let extendedSimState = this.GetExtendedSimulationState();
return {
"timeElapsed": extendedSimState.timeElapsed,
"playerStates": extendedSimState.players,
"mapSettings": InitAttributes.settings
};
};
+/**
+ * Called when the game ends if the current game is part of a campaign run.
+ */
+GuiInterface.prototype.GetCampaignGameEndData = function(player)
+{
+ let cmpTrigger = Engine.QueryInterface(SYSTEM_ENTITY, IID_Trigger);
+ if (Trigger.prototype.OnCampaignGameEnd)
+ return Trigger.prototype.OnCampaignGameEnd();
+ return {};
+};
+
GuiInterface.prototype.GetRenamedEntities = function(player)
{
if (this.miragedEntities[player])
return this.renamedEntities.concat(this.miragedEntities[player]);
return this.renamedEntities;
};
GuiInterface.prototype.ClearRenamedEntities = function()
{
this.renamedEntities = [];
this.miragedEntities = [];
};
GuiInterface.prototype.AddMiragedEntity = function(player, entity, mirage)
{
if (!this.miragedEntities[player])
this.miragedEntities[player] = [];
this.miragedEntities[player].push({ "entity": entity, "newentity": mirage });
};
/**
* Get common entity info, often used in the gui.
*/
GuiInterface.prototype.GetEntityState = function(player, ent)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
// All units must have a template; if not then it's a nonexistent entity id.
let template = cmpTemplateManager.GetCurrentTemplateName(ent);
if (!template)
return null;
let ret = {
"id": ent,
"player": INVALID_PLAYER,
"template": template
};
let cmpMirage = Engine.QueryInterface(ent, IID_Mirage);
if (cmpMirage)
ret.mirage = true;
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
if (cmpIdentity)
ret.identity = {
"rank": cmpIdentity.GetRank(),
"classes": cmpIdentity.GetClassesList(),
"selectionGroupName": cmpIdentity.GetSelectionGroupName(),
"canDelete": !cmpIdentity.IsUndeletable(),
"hasSomeFormation": cmpIdentity.HasSomeFormation(),
"formations": cmpIdentity.GetFormationsList(),
"controllable": cmpIdentity.IsControllable()
};
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition && cmpPosition.IsInWorld())
ret.position = cmpPosition.GetPosition();
let cmpHealth = QueryMiragedInterface(ent, IID_Health);
if (cmpHealth)
{
ret.hitpoints = cmpHealth.GetHitpoints();
ret.maxHitpoints = cmpHealth.GetMaxHitpoints();
ret.needsRepair = cmpHealth.IsRepairable() && cmpHealth.IsInjured();
ret.needsHeal = !cmpHealth.IsUnhealable();
}
let cmpCapturable = QueryMiragedInterface(ent, IID_Capturable);
if (cmpCapturable)
{
ret.capturePoints = cmpCapturable.GetCapturePoints();
ret.maxCapturePoints = cmpCapturable.GetMaxCapturePoints();
}
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (cmpBuilder)
ret.builder = true;
let cmpMarket = QueryMiragedInterface(ent, IID_Market);
if (cmpMarket)
ret.market = {
"land": cmpMarket.HasType("land"),
"naval": cmpMarket.HasType("naval")
};
let cmpPack = Engine.QueryInterface(ent, IID_Pack);
if (cmpPack)
ret.pack = {
"packed": cmpPack.IsPacked(),
"progress": cmpPack.GetProgress()
};
let cmpPopulation = Engine.QueryInterface(ent, IID_Population);
if (cmpPopulation)
ret.population = {
"bonus": cmpPopulation.GetPopBonus()
};
let cmpUpgrade = Engine.QueryInterface(ent, IID_Upgrade);
if (cmpUpgrade)
ret.upgrade = {
"upgrades": cmpUpgrade.GetUpgrades(),
"progress": cmpUpgrade.GetProgress(),
"template": cmpUpgrade.GetUpgradingTo(),
"isUpgrading": cmpUpgrade.IsUpgrading()
};
let cmpStatusEffects = Engine.QueryInterface(ent, IID_StatusEffectsReceiver);
if (cmpStatusEffects)
ret.statusEffects = cmpStatusEffects.GetActiveStatuses();
let cmpProductionQueue = Engine.QueryInterface(ent, IID_ProductionQueue);
if (cmpProductionQueue)
ret.production = {
"entities": cmpProductionQueue.GetEntitiesList(),
"technologies": cmpProductionQueue.GetTechnologiesList(),
"techCostMultiplier": cmpProductionQueue.GetTechCostMultiplier(),
"queue": cmpProductionQueue.GetQueue()
};
let cmpTrader = Engine.QueryInterface(ent, IID_Trader);
if (cmpTrader)
ret.trader = {
"goods": cmpTrader.GetGoods()
};
let cmpFoundation = QueryMiragedInterface(ent, IID_Foundation);
if (cmpFoundation)
ret.foundation = {
"numBuilders": cmpFoundation.GetNumBuilders(),
"buildTime": cmpFoundation.GetBuildTime()
};
let cmpRepairable = QueryMiragedInterface(ent, IID_Repairable);
if (cmpRepairable)
ret.repairable = {
"numBuilders": cmpRepairable.GetNumBuilders(),
"buildTime": cmpRepairable.GetBuildTime()
};
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
ret.player = cmpOwnership.GetOwner();
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (cmpRallyPoint)
ret.rallyPoint = { "position": cmpRallyPoint.GetPositions()[0] }; // undefined or {x,z} object
let cmpGarrisonHolder = Engine.QueryInterface(ent, IID_GarrisonHolder);
if (cmpGarrisonHolder)
ret.garrisonHolder = {
"entities": cmpGarrisonHolder.GetEntities(),
"buffHeal": cmpGarrisonHolder.GetHealRate(),
"allowedClasses": cmpGarrisonHolder.GetAllowedClasses(),
"capacity": cmpGarrisonHolder.GetCapacity(),
"occupiedSlots": cmpGarrisonHolder.OccupiedSlots()
};
let cmpTurretHolder = Engine.QueryInterface(ent, IID_TurretHolder);
if (cmpTurretHolder)
ret.turretHolder = {
"turretPoints": cmpTurretHolder.GetTurretPoints()
};
let cmpGarrisonable = Engine.QueryInterface(ent, IID_Garrisonable);
if (cmpGarrisonable)
ret.garrisonable = {
"holder": cmpGarrisonable.HolderID(),
"size": cmpGarrisonable.UnitSize()
};
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI)
ret.unitAI = {
"state": cmpUnitAI.GetCurrentState(),
"orders": cmpUnitAI.GetOrders(),
"hasWorkOrders": cmpUnitAI.HasWorkOrders(),
"canGuard": cmpUnitAI.CanGuard(),
"isGuarding": cmpUnitAI.IsGuardOf(),
"canPatrol": cmpUnitAI.CanPatrol(),
"selectableStances": cmpUnitAI.GetSelectableStances(),
"isIdle": cmpUnitAI.IsIdle()
};
let cmpGuard = Engine.QueryInterface(ent, IID_Guard);
if (cmpGuard)
ret.guard = {
"entities": cmpGuard.GetEntities()
};
let cmpResourceGatherer = Engine.QueryInterface(ent, IID_ResourceGatherer);
if (cmpResourceGatherer)
{
ret.resourceCarrying = cmpResourceGatherer.GetCarryingStatus();
ret.resourceGatherRates = cmpResourceGatherer.GetGatherRates();
}
let cmpGate = Engine.QueryInterface(ent, IID_Gate);
if (cmpGate)
ret.gate = {
"locked": cmpGate.IsLocked()
};
let cmpAlertRaiser = Engine.QueryInterface(ent, IID_AlertRaiser);
if (cmpAlertRaiser)
ret.alertRaiser = true;
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
ret.visibility = cmpRangeManager.GetLosVisibility(ent, player);
let cmpAttack = Engine.QueryInterface(ent, IID_Attack);
if (cmpAttack)
{
let types = cmpAttack.GetAttackTypes();
if (types.length)
ret.attack = {};
for (let type of types)
{
ret.attack[type] = {};
Object.assign(ret.attack[type], cmpAttack.GetAttackEffectsData(type));
ret.attack[type].attackName = cmpAttack.GetAttackName(type);
ret.attack[type].splash = cmpAttack.GetSplashData(type);
if (ret.attack[type].splash)
Object.assign(ret.attack[type].splash, cmpAttack.GetAttackEffectsData(type, true));
let range = cmpAttack.GetRange(type);
ret.attack[type].minRange = range.min;
ret.attack[type].maxRange = range.max;
let timers = cmpAttack.GetTimers(type);
ret.attack[type].prepareTime = timers.prepare;
ret.attack[type].repeatTime = timers.repeat;
if (type != "Ranged")
{
// Not a ranged attack, set some defaults.
ret.attack[type].elevationBonus = 0;
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
continue;
}
ret.attack[type].elevationBonus = range.elevationBonus;
if (cmpPosition && cmpPosition.IsInWorld())
// For units, take the range in front of it, no spread, so angle = 0,
// else, take the average elevation around it: angle = 2 * pi.
ret.attack[type].elevationAdaptedRange = cmpRangeManager.GetElevationAdaptedRange(cmpPosition.GetPosition(), cmpPosition.GetRotation(), range.max, range.elevationBonus, cmpUnitAI ? 0 : 2 * Math.PI);
else
// Not in world, set a default?
ret.attack[type].elevationAdaptedRange = ret.attack.maxRange;
}
}
let cmpResistance = Engine.QueryInterface(ent, IID_Resistance);
if (cmpResistance)
ret.resistance = cmpResistance.GetResistanceOfForm(cmpFoundation ? "Foundation" : "Entity");
let cmpBuildingAI = Engine.QueryInterface(ent, IID_BuildingAI);
if (cmpBuildingAI)
ret.buildingAI = {
"defaultArrowCount": cmpBuildingAI.GetDefaultArrowCount(),
"maxArrowCount": cmpBuildingAI.GetMaxArrowCount(),
"garrisonArrowMultiplier": cmpBuildingAI.GetGarrisonArrowMultiplier(),
"garrisonArrowClasses": cmpBuildingAI.GetGarrisonArrowClasses(),
"arrowCount": cmpBuildingAI.GetArrowCount()
};
if (cmpPosition && cmpPosition.GetTurretParent() != INVALID_ENTITY)
ret.turretParent = cmpPosition.GetTurretParent();
let cmpResourceSupply = QueryMiragedInterface(ent, IID_ResourceSupply);
if (cmpResourceSupply)
ret.resourceSupply = {
"isInfinite": cmpResourceSupply.IsInfinite(),
"max": cmpResourceSupply.GetMaxAmount(),
"amount": cmpResourceSupply.GetCurrentAmount(),
"type": cmpResourceSupply.GetType(),
"killBeforeGather": cmpResourceSupply.GetKillBeforeGather(),
"maxGatherers": cmpResourceSupply.GetMaxGatherers(),
"numGatherers": cmpResourceSupply.GetNumGatherers()
};
let cmpResourceDropsite = Engine.QueryInterface(ent, IID_ResourceDropsite);
if (cmpResourceDropsite)
ret.resourceDropsite = {
"types": cmpResourceDropsite.GetTypes(),
"sharable": cmpResourceDropsite.IsSharable(),
"shared": cmpResourceDropsite.IsShared()
};
let cmpPromotion = Engine.QueryInterface(ent, IID_Promotion);
if (cmpPromotion)
ret.promotion = {
"curr": cmpPromotion.GetCurrentXp(),
"req": cmpPromotion.GetRequiredXp()
};
if (!cmpFoundation && cmpIdentity && cmpIdentity.HasClass("Barter"))
ret.isBarterMarket = true;
let cmpHeal = Engine.QueryInterface(ent, IID_Heal);
if (cmpHeal)
ret.heal = {
"health": cmpHeal.GetHealth(),
"range": cmpHeal.GetRange().max,
"interval": cmpHeal.GetInterval(),
"unhealableClasses": cmpHeal.GetUnhealableClasses(),
"healableClasses": cmpHeal.GetHealableClasses()
};
let cmpLoot = Engine.QueryInterface(ent, IID_Loot);
if (cmpLoot)
{
ret.loot = cmpLoot.GetResources();
ret.loot.xp = cmpLoot.GetXp();
}
let cmpResourceTrickle = Engine.QueryInterface(ent, IID_ResourceTrickle);
if (cmpResourceTrickle)
ret.resourceTrickle = {
"interval": cmpResourceTrickle.GetInterval(),
"rates": cmpResourceTrickle.GetRates()
};
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
ret.speed = {
"walk": cmpUnitMotion.GetWalkSpeed(),
"run": cmpUnitMotion.GetWalkSpeed() * cmpUnitMotion.GetRunMultiplier()
};
return ret;
};
GuiInterface.prototype.GetMultipleEntityStates = function(player, ents)
{
return ents.map(ent => ({ "entId": ent, "state": this.GetEntityState(player, ent) }));
};
GuiInterface.prototype.GetAverageRangeForBuildings = function(player, cmd)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
let rot = { "x": 0, "y": 0, "z": 0 };
let pos = {
"x": cmd.x,
"y": cmpTerrain.GetGroundLevel(cmd.x, cmd.z),
"z": cmd.z
};
let elevationBonus = cmd.elevationBonus || 0;
let range = cmd.range;
return cmpRangeManager.GetElevationAdaptedRange(pos, rot, range, elevationBonus, 2 * Math.PI);
};
GuiInterface.prototype.GetTemplateData = function(player, data)
{
let templateName = data.templateName;
let owner = data.player !== undefined ? data.player : player;
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(templateName);
if (!template)
return null;
let aurasTemplate = {};
if (!template.Auras)
return GetTemplateDataHelper(template, owner, aurasTemplate);
let auraNames = template.Auras._string.split(/\s+/);
for (let name of auraNames)
{
let auraTemplate = AuraTemplates.Get(name);
if (!auraTemplate)
error("Template " + templateName + " has undefined aura " + name);
else
aurasTemplate[name] = auraTemplate;
}
return GetTemplateDataHelper(template, owner, aurasTemplate);
};
GuiInterface.prototype.IsTechnologyResearched = function(player, data)
{
if (!data.tech)
return true;
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.IsTechnologyResearched(data.tech);
};
/**
* Checks whether the requirements for this technology have been met.
*/
GuiInterface.prototype.CheckTechnologyRequirements = function(player, data)
{
let cmpTechnologyManager = QueryPlayerIDInterface(data.player !== undefined ? data.player : player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return false;
return cmpTechnologyManager.CanResearch(data.tech);
};
/**
* Returns technologies that are being actively researched, along with
* which entity is researching them and how far along the research is.
*/
GuiInterface.prototype.GetStartedResearch = function(player)
{
let cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
if (!cmpTechnologyManager)
return {};
let ret = {};
for (let tech of cmpTechnologyManager.GetStartedTechs())
{
ret[tech] = { "researcher": cmpTechnologyManager.GetResearcher(tech) };
let cmpProductionQueue = Engine.QueryInterface(ret[tech].researcher, IID_ProductionQueue);
if (cmpProductionQueue)
{
ret[tech].progress = cmpProductionQueue.GetQueue()[0].progress;
ret[tech].timeRemaining = cmpProductionQueue.GetQueue()[0].timeRemaining;
}
else
{
ret[tech].progress = 0;
ret[tech].timeRemaining = 0;
}
}
return ret;
};
/**
* Returns the battle state of the player.
*/
GuiInterface.prototype.GetBattleState = function(player)
{
let cmpBattleDetection = QueryPlayerIDInterface(player, IID_BattleDetection);
if (!cmpBattleDetection)
return false;
return cmpBattleDetection.GetState();
};
/**
* Returns a list of ongoing attacks against the player.
*/
GuiInterface.prototype.GetIncomingAttacks = function(player)
{
let cmpAttackDetection = QueryPlayerIDInterface(player, IID_AttackDetection);
if (!cmpAttackDetection)
return [];
return cmpAttackDetection.GetIncomingAttacks();
};
/**
* Used to show a red square over GUI elements you can't yet afford.
*/
GuiInterface.prototype.GetNeededResources = function(player, data)
{
let cmpPlayer = QueryPlayerIDInterface(data.player !== undefined ? data.player : player);
return cmpPlayer ? cmpPlayer.GetNeededResources(data.cost) : {};
};
/**
* State of the templateData (player dependent): true when some template values have been modified
* and need to be reloaded by the gui.
*/
GuiInterface.prototype.OnTemplateModification = function(msg)
{
this.templateModified[msg.player] = true;
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.IsTemplateModified = function(player)
{
return this.templateModified[player] || false;
};
GuiInterface.prototype.ResetTemplateModified = function()
{
this.templateModified = {};
};
/**
* Some changes may require an update to the selection panel,
* which is cached for efficiency. Inform the GUI it needs reloading.
*/
GuiInterface.prototype.OnDisabledTemplatesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.OnDisabledTechnologiesChanged = function(msg)
{
this.selectionDirty[msg.player] = true;
};
GuiInterface.prototype.SetSelectionDirty = function(player)
{
this.selectionDirty[player] = true;
};
GuiInterface.prototype.IsSelectionDirty = function(player)
{
return this.selectionDirty[player] || false;
};
GuiInterface.prototype.ResetSelectionDirty = function()
{
this.selectionDirty = {};
};
/**
* Add a timed notification.
* Warning: timed notifacations are serialised
* (to also display them on saved games or after a rejoin)
* so they should allways be added and deleted in a deterministic way.
*/
GuiInterface.prototype.AddTimeNotification = function(notification, duration = 10000)
{
let cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
notification.endTime = duration + cmpTimer.GetTime();
notification.id = ++this.timeNotificationID;
// Let all players and observers receive the notification by default.
if (!notification.players)
{
notification.players = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetAllPlayers();
notification.players[0] = -1;
}
this.timeNotifications.push(notification);
this.timeNotifications.sort((n1, n2) => n2.endTime - n1.endTime);
cmpTimer.SetTimeout(this.entity, IID_GuiInterface, "DeleteTimeNotification", duration, this.timeNotificationID);
return this.timeNotificationID;
};
GuiInterface.prototype.DeleteTimeNotification = function(notificationID)
{
this.timeNotifications = this.timeNotifications.filter(n => n.id != notificationID);
};
GuiInterface.prototype.GetTimeNotifications = function(player)
{
let time = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer).GetTime();
// Filter on players and time, since the delete timer might be executed with a delay.
return this.timeNotifications.filter(n => n.players.indexOf(player) != -1 && n.endTime > time);
};
GuiInterface.prototype.PushNotification = function(notification)
{
if (!notification.type || notification.type == "text")
this.AddTimeNotification(notification);
else
this.notifications.push(notification);
};
GuiInterface.prototype.GetNotifications = function()
{
let n = this.notifications;
this.notifications = [];
return n;
};
GuiInterface.prototype.GetAvailableFormations = function(player, wantedPlayer)
{
let cmpPlayer = QueryPlayerIDInterface(wantedPlayer);
if (!cmpPlayer)
return [];
return cmpPlayer.GetFormations();
};
GuiInterface.prototype.GetFormationRequirements = function(player, data)
{
return GetFormationRequirements(data.formationTemplate);
};
GuiInterface.prototype.CanMoveEntsIntoFormation = function(player, data)
{
return CanMoveEntsIntoFormation(data.ents, data.formationTemplate);
};
GuiInterface.prototype.GetFormationInfoFromTemplate = function(player, data)
{
let cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
let template = cmpTemplateManager.GetTemplate(data.templateName);
if (!template || !template.Formation)
return {};
return {
"name": template.Formation.FormationName,
"tooltip": template.Formation.DisabledTooltip || "",
"icon": template.Formation.Icon
};
};
GuiInterface.prototype.IsFormationSelected = function(player, data)
{
return data.ents.some(ent => {
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
return cmpUnitAI && cmpUnitAI.GetFormationTemplate() == data.formationTemplate;
});
};
GuiInterface.prototype.IsStanceSelected = function(player, data)
{
for (let ent of data.ents)
{
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (cmpUnitAI && cmpUnitAI.GetStanceName() == data.stance)
return true;
}
return false;
};
GuiInterface.prototype.GetAllBuildableEntities = function(player, cmd)
{
let buildableEnts = [];
for (let ent of cmd.entities)
{
let cmpBuilder = Engine.QueryInterface(ent, IID_Builder);
if (!cmpBuilder)
continue;
for (let building of cmpBuilder.GetEntitiesList())
if (buildableEnts.indexOf(building) == -1)
buildableEnts.push(building);
}
return buildableEnts;
};
GuiInterface.prototype.UpdateDisplayedPlayerColors = function(player, data)
{
let updateEntityColor = (iids, entities) => {
for (let ent of entities)
for (let iid of iids)
{
let cmp = Engine.QueryInterface(ent, iid);
if (cmp)
cmp.UpdateColor();
}
};
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let numPlayers = Engine.QueryInterface(SYSTEM_ENTITY, IID_PlayerManager).GetNumPlayers();
for (let i = 1; i < numPlayers; ++i)
{
let cmpPlayer = QueryPlayerIDInterface(i, IID_Player);
if (!cmpPlayer)
continue;
cmpPlayer.SetDisplayDiplomacyColor(data.displayDiplomacyColors);
if (data.displayDiplomacyColors)
cmpPlayer.SetDiplomacyColor(data.displayedPlayerColors[i]);
updateEntityColor(data.showAllStatusBars && (i == player || player == -1) ?
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer, IID_StatusBars] :
[IID_Minimap, IID_RangeOverlayRenderer, IID_RallyPointRenderer],
cmpRangeManager.GetEntitiesByPlayer(i));
}
updateEntityColor([IID_Selectable, IID_StatusBars], data.selected);
Engine.QueryInterface(SYSTEM_ENTITY, IID_TerritoryManager).UpdateColors();
};
GuiInterface.prototype.SetSelectionHighlight = function(player, cmd)
{
// Cache of owner -> color map
let playerColors = {};
for (let ent of cmd.entities)
{
let cmpSelectable = Engine.QueryInterface(ent, IID_Selectable);
if (!cmpSelectable)
continue;
// Find the entity's owner's color.
let owner = INVALID_PLAYER;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (cmpOwnership)
owner = cmpOwnership.GetOwner();
let color = playerColors[owner];
if (!color)
{
color = { "r": 1, "g": 1, "b": 1 };
let cmpPlayer = QueryPlayerIDInterface(owner);
if (cmpPlayer)
color = cmpPlayer.GetDisplayedColor();
playerColors[owner] = color;
}
cmpSelectable.SetSelectionHighlight({ "r": color.r, "g": color.g, "b": color.b, "a": cmd.alpha }, cmd.selected);
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (!cmpRangeOverlayManager || player != owner && player != INVALID_PLAYER)
continue;
cmpRangeOverlayManager.SetEnabled(cmd.selected, this.enabledVisualRangeOverlayTypes, false);
}
};
GuiInterface.prototype.EnableVisualRangeOverlayType = function(player, data)
{
this.enabledVisualRangeOverlayTypes[data.type] = data.enabled;
};
GuiInterface.prototype.GetEntitiesWithStatusBars = function()
{
return Array.from(this.entsWithAuraAndStatusBars);
};
GuiInterface.prototype.SetStatusBars = function(player, cmd)
{
let affectedEnts = new Set();
for (let ent of cmd.entities)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (!cmpStatusBars)
continue;
cmpStatusBars.SetEnabled(cmd.enabled, cmd.showRank, cmd.showExperience);
let cmpAuras = Engine.QueryInterface(ent, IID_Auras);
if (!cmpAuras)
continue;
for (let name of cmpAuras.GetAuraNames())
{
if (!cmpAuras.GetOverlayIcon(name))
continue;
for (let e of cmpAuras.GetAffectedEntities(name))
affectedEnts.add(e);
if (cmd.enabled)
this.entsWithAuraAndStatusBars.add(ent);
else
this.entsWithAuraAndStatusBars.delete(ent);
}
}
for (let ent of affectedEnts)
{
let cmpStatusBars = Engine.QueryInterface(ent, IID_StatusBars);
if (cmpStatusBars)
cmpStatusBars.RegenerateSprites();
}
};
GuiInterface.prototype.SetRangeOverlays = function(player, cmd)
{
for (let ent of cmd.entities)
{
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(cmd.enabled, this.enabledVisualRangeOverlayTypes, true);
}
};
GuiInterface.prototype.GetPlayerEntities = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetEntitiesByPlayer(player);
};
GuiInterface.prototype.GetNonGaiaEntities = function()
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetNonGaiaEntities();
};
/**
* Displays the rally points of a given list of entities (carried in cmd.entities).
*
* The 'cmd' object may carry its own x/z coordinate pair indicating the location where the rally point should
* be rendered, in order to support instantaneously rendering a rally point marker at a specified location
* instead of incurring a delay while PostNetworkCommand processes the set-rallypoint command (see input.js).
* If cmd doesn't carry a custom location, then the position to render the marker at will be read from the
* RallyPoint component.
*/
GuiInterface.prototype.DisplayRallyPoint = function(player, cmd)
{
let cmpPlayer = QueryPlayerIDInterface(player);
// If there are some rally points already displayed, first hide them.
for (let ent of this.entsRallyPointsDisplayed)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (cmpRallyPointRenderer)
cmpRallyPointRenderer.SetDisplayed(false);
}
this.entsRallyPointsDisplayed = [];
// Show the rally points for the passed entities.
for (let ent of cmd.entities)
{
let cmpRallyPointRenderer = Engine.QueryInterface(ent, IID_RallyPointRenderer);
if (!cmpRallyPointRenderer)
continue;
// Entity must have a rally point component to display a rally point marker
// (regardless of whether cmd specifies a custom location).
let cmpRallyPoint = Engine.QueryInterface(ent, IID_RallyPoint);
if (!cmpRallyPoint)
continue;
// Verify the owner.
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
if (!(cmpPlayer && cmpPlayer.CanControlAllUnits()))
if (!cmpOwnership || cmpOwnership.GetOwner() != player)
continue;
// If the command was passed an explicit position, use that and
// override the real rally point position; otherwise use the real position.
let pos;
if (cmd.x && cmd.z)
pos = cmd;
else
// May return undefined if no rally point is set.
pos = cmpRallyPoint.GetPositions()[0];
if (pos)
{
// Only update the position if we changed it (cmd.queued is set).
// Note that Add-/SetPosition take a CFixedVector2D which has X/Y components, not X/Z.
if ("queued" in cmd)
{
if (cmd.queued == true)
cmpRallyPointRenderer.AddPosition(new Vector2D(pos.x, pos.z));
else
cmpRallyPointRenderer.SetPosition(new Vector2D(pos.x, pos.z));
}
else if (!cmpRallyPointRenderer.IsSet())
// Rebuild the renderer when not set (when reading saved game or in case of building update).
for (let posi of cmpRallyPoint.GetPositions())
cmpRallyPointRenderer.AddPosition(new Vector2D(posi.x, posi.z));
cmpRallyPointRenderer.SetDisplayed(true);
// Remember which entities have their rally points displayed so we can hide them again.
this.entsRallyPointsDisplayed.push(ent);
}
}
};
GuiInterface.prototype.AddTargetMarker = function(player, cmd)
{
let ent = Engine.AddLocalEntity(cmd.template);
if (!ent)
return;
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
cmpPosition.JumpTo(cmd.x, cmd.z);
};
/**
* Display the building placement preview.
* cmd.template is the name of the entity template, or "" to disable the preview.
* cmd.x, cmd.z, cmd.angle give the location.
*
* Returns result object from CheckPlacement:
* {
* "success": true iff the placement is valid, else false
* "message": message to display in UI for invalid placement, else ""
* "parameters": parameters to use in the message
* "translateMessage": localisation info
* "translateParameters": localisation info
* "pluralMessage": we might return a plural translation instead (optional)
* "pluralCount": localisation info (optional)
* }
*/
GuiInterface.prototype.SetBuildingPlacementPreview = function(player, cmd)
{
let result = {
"success": false,
"message": "",
"parameters": {},
"translateMessage": false,
"translateParameters": []
};
if (!this.placementEntity || this.placementEntity[0] != cmd.template)
{
if (this.placementEntity)
Engine.DestroyEntity(this.placementEntity[1]);
if (cmd.template == "")
this.placementEntity = undefined;
else
this.placementEntity = [cmd.template, Engine.AddLocalEntity("preview|" + cmd.template)];
}
if (this.placementEntity)
{
let ent = this.placementEntity[1];
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
{
pos.JumpTo(cmd.x, cmd.z);
pos.SetYRotation(cmd.angle);
}
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
error("cmpBuildRestrictions not defined");
else
result = cmpBuildRestrictions.CheckPlacement();
let cmpRangeOverlayManager = Engine.QueryInterface(ent, IID_RangeOverlayManager);
if (cmpRangeOverlayManager)
cmpRangeOverlayManager.SetEnabled(true, this.enabledVisualRangeOverlayTypes);
// Set it to a red shade if this is an invalid location.
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (cmd.actorSeed !== undefined)
cmpVisual.SetActorSeed(cmd.actorSeed);
if (!result.success)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
}
return result;
};
/**
* Previews the placement of a wall between cmd.start and cmd.end, or just the starting piece of a wall if cmd.end is not
* specified. Returns an object with information about the list of entities that need to be newly constructed to complete
* at least a part of the wall, or false if there are entities required to build at least part of the wall but none of
* them can be validly constructed.
*
* It's important to distinguish between three lists of entities that are at play here, because they may be subsets of one
* another depending on things like snapping and whether some of the entities inside them can be validly positioned.
* We have:
* - The list of entities that previews the wall. This list is usually equal to the entities required to construct the
* entire wall. However, if there is snapping to an incomplete tower (i.e. a foundation), it includes extra entities
* to preview the completed tower on top of its foundation.
*
* - The list of entities that need to be newly constructed to build the entire wall. This list is regardless of whether
* any of them can be validly positioned. The emphasishere here is on 'newly'; this list does not include any existing
* towers at either side of the wall that we snapped to. Or, more generally; it does not include any _entities_ that we
* snapped to; we might still snap to e.g. terrain, in which case the towers on either end will still need to be newly
* constructed.
*
* - The list of entities that need to be newly constructed to build at least a part of the wall. This list is the same
* as the one above, except that it is truncated at the first entity that cannot be validly positioned. This happens
* e.g. if the player tries to build a wall straight through an obstruction. Note that any entities that can be validly
* constructed but come after said first invalid entity are also truncated away.
*
* With this in mind, this method will return false if the second list is not empty, but the third one is. That is, if there
* were entities that are needed to build the wall, but none of them can be validly constructed. False is also returned in
* case of unexpected errors (typically missing components), and when clearing the preview by passing an empty wallset
* argument (see below). Otherwise, it will return an object with the following information:
*
* result: {
* 'startSnappedEnt': ID of the entity that we snapped to at the starting side of the wall. Currently only supports towers.
* 'endSnappedEnt': ID of the entity that we snapped to at the (possibly truncated) ending side of the wall. Note that this
* can only be set if no truncation of the second list occurs; if we snapped to an entity at the ending side
* but the wall construction was truncated before we could reach it, it won't be set here. Currently only
* supports towers.
* 'pieces': Array with the following data for each of the entities in the third list:
* [{
* 'template': Template name of the entity.
* 'x': X coordinate of the entity's position.
* 'z': Z coordinate of the entity's position.
* 'angle': Rotation around the Y axis of the entity (in radians).
* },
* ...]
* 'cost': { The total cost required for constructing all the pieces as listed above.
* 'food': ...,
* 'wood': ...,
* 'stone': ...,
* 'metal': ...,
* 'population': ...,
* }
* }
*
* @param cmd.wallSet Object holding the set of wall piece template names. Set to an empty value to clear the preview.
* @param cmd.start Starting point of the wall segment being created.
* @param cmd.end (Optional) Ending point of the wall segment being created. If not defined, it is understood that only
* the starting point of the wall is available at this time (e.g. while the player is still in the process
* of picking a starting point), and that therefore only the first entity in the wall (a tower) should be
* previewed.
* @param cmd.snapEntities List of candidate entities to snap the start and ending positions to.
*/
GuiInterface.prototype.SetWallPlacementPreview = function(player, cmd)
{
let wallSet = cmd.wallSet;
// Did the start position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let start = {
"pos": cmd.start,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// Did the end position snap to anything?
// If we snapped, was it to an entity? If yes, hold that entity's ID.
let end = {
"pos": cmd.end,
"angle": 0,
"snapped": false,
"snappedEnt": INVALID_ENTITY
};
// --------------------------------------------------------------------------------
// Do some entity cache management and check for snapping.
if (!this.placementWallEntities)
this.placementWallEntities = {};
if (!wallSet)
{
// We're clearing the preview, clear the entity cache and bail.
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
Engine.DestroyEntity(ent);
this.placementWallEntities[tpl].numUsed = 0;
this.placementWallEntities[tpl].entities = [];
// Keep template data around.
}
return false;
}
for (let tpl in this.placementWallEntities)
{
for (let ent of this.placementWallEntities[tpl].entities)
{
let pos = Engine.QueryInterface(ent, IID_Position);
if (pos)
pos.MoveOutOfWorld();
}
this.placementWallEntities[tpl].numUsed = 0;
}
// Create cache entries for templates we haven't seen before.
for (let type in wallSet.templates)
{
if (type == "curves")
continue;
let tpl = wallSet.templates[type];
if (!(tpl in this.placementWallEntities))
{
this.placementWallEntities[tpl] = {
"numUsed": 0,
"entities": [],
"templateData": this.GetTemplateData(player, { "templateName": tpl }),
};
if (!this.placementWallEntities[tpl].templateData.wallPiece)
{
error("[SetWallPlacementPreview] No WallPiece component found for wall set template '" + tpl + "'");
return false;
}
}
}
// Prevent division by zero errors further on if the start and end positions are the same.
if (end.pos && (start.pos.x === end.pos.x && start.pos.z === end.pos.z))
end.pos = undefined;
// See if we need to snap the start and/or end coordinates to any of our list of snap entities. Note that, despite the list
// of snapping candidate entities, it might still snap to e.g. terrain features. Use the "ent" key in the returned snapping
// data to determine whether it snapped to an entity (if any), and to which one (see GetFoundationSnapData).
if (cmd.snapEntities)
{
// Value of 0.5 was determined through trial and error.
let snapRadius = this.placementWallEntities[wallSet.templates.tower].templateData.wallPiece.length * 0.5;
let startSnapData = this.GetFoundationSnapData(player, {
"x": start.pos.x,
"z": start.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (startSnapData)
{
start.pos.x = startSnapData.x;
start.pos.z = startSnapData.z;
start.angle = startSnapData.angle;
start.snapped = true;
if (startSnapData.ent)
start.snappedEnt = startSnapData.ent;
}
if (end.pos)
{
let endSnapData = this.GetFoundationSnapData(player, {
"x": end.pos.x,
"z": end.pos.z,
"template": wallSet.templates.tower,
"snapEntities": cmd.snapEntities,
"snapRadius": snapRadius,
});
if (endSnapData)
{
end.pos.x = endSnapData.x;
end.pos.z = endSnapData.z;
end.angle = endSnapData.angle;
end.snapped = true;
if (endSnapData.ent)
end.snappedEnt = endSnapData.ent;
}
}
}
// Clear the single-building preview entity (we'll be rolling our own).
this.SetBuildingPlacementPreview(player, { "template": "" });
// --------------------------------------------------------------------------------
// Calculate wall placement and position preview entities.
let result = {
"pieces": [],
"cost": { "population": 0, "time": 0 }
};
for (let res of Resources.GetCodes())
result.cost[res] = 0;
let previewEntities = [];
if (end.pos)
// See helpers/Walls.js.
previewEntities = GetWallPlacement(this.placementWallEntities, wallSet, start, end);
// For wall placement, we may (and usually do) need to have wall pieces overlap each other more than would
// otherwise be allowed by their obstruction shapes. However, during this preview phase, this is not so much of
// an issue, because all preview entities have their obstruction components deactivated, meaning that their
// obstruction shapes do not register in the simulation and hence cannot affect it. This implies that the preview
// entities cannot be found to obstruct each other, which largely solves the issue of overlap between wall pieces.
// Note that they will still be obstructed by existing shapes in the simulation (that have the BLOCK_FOUNDATION
// flag set), which is what we want. The only exception to this is when snapping to existing towers (or
// foundations thereof); the wall segments that connect up to these will be found to be obstructed by the
// existing tower/foundation, and be shaded red to indicate that they cannot be placed there. To prevent this,
// we manually set the control group of the outermost wall pieces equal to those of the snapped-to towers, so
// that they are free from mutual obstruction (per definition of obstruction control groups). This is done by
// assigning them an extra "controlGroup" field, which we'll then set during the placement loop below.
// Additionally, in the situation that we're snapping to merely a foundation of a tower instead of a fully
// constructed one, we'll need an extra preview entity for the starting tower, which also must not be obstructed
// by the foundation it snaps to.
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
{
let startEntObstruction = Engine.QueryInterface(start.snappedEnt, IID_Obstruction);
if (previewEntities.length && startEntObstruction)
previewEntities[0].controlGroups = [startEntObstruction.GetControlGroup()];
// If we're snapping to merely a foundation, add an extra preview tower and also set it to the same control group.
let startEntState = this.GetEntityState(player, start.snappedEnt);
if (startEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(start.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [startEntObstruction ? startEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true // Preview only, must not appear in the result.
});
}
}
else
{
// Didn't snap to an existing entity, add the starting tower manually. To prevent odd-looking rotation jumps
// when shift-clicking to build a wall, reuse the placement angle that was last seen on a validly positioned
// wall piece.
// To illustrate the last point, consider what happens if we used some constant instead, say, 0. Issuing the
// build command for a wall is asynchronous, so when the preview updates after shift-clicking, the wall piece
// foundations are not registered yet in the simulation. This means they cannot possibly be picked in the list
// of candidate entities for snapping. In the next preview update, we therefore hit this case, and would rotate
// the preview to 0 radians. Then, after one or two simulation updates or so, the foundations register and
// onSimulationUpdate in session.js updates the preview again. It first grabs a new list of snapping candidates,
// which this time does include the new foundations; so we snap to the entity, and rotate the preview back to
// the foundation's angle.
// The result is a noticeable rotation to 0 and back, which is undesirable. So, for a split second there until
// the simulation updates, we fake it by reusing the last angle and hope the player doesn't notice.
previewEntities.unshift({
"template": wallSet.templates.tower,
"pos": start.pos,
"angle": previewEntities.length ? previewEntities[0].angle : this.placementWallLastAngle
});
}
if (end.pos)
{
// Analogous to the starting side case above.
if (end.snappedEnt && end.snappedEnt != INVALID_ENTITY)
{
let endEntObstruction = Engine.QueryInterface(end.snappedEnt, IID_Obstruction);
// Note that it's possible for the last entity in previewEntities to be the same as the first, i.e. the
// same wall piece snapping to both a starting and an ending tower. And it might be more common than you would
// expect; the allowed overlap between wall segments and towers facilitates this to some degree. To deal with
// the possibility of dual initial control groups, we use a '.controlGroups' array rather than a single
// '.controlGroup' property. Note that this array can only ever have 0, 1 or 2 elements (checked at a later time).
if (previewEntities.length > 0 && endEntObstruction)
{
previewEntities[previewEntities.length - 1].controlGroups = previewEntities[previewEntities.length - 1].controlGroups || [];
previewEntities[previewEntities.length - 1].controlGroups.push(endEntObstruction.GetControlGroup());
}
// If we're snapping to a foundation, add an extra preview tower and also set it to the same control group.
let endEntState = this.GetEntityState(player, end.snappedEnt);
if (endEntState.foundation)
{
let cmpPosition = Engine.QueryInterface(end.snappedEnt, IID_Position);
if (cmpPosition)
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": cmpPosition.GetRotation().y,
"controlGroups": [endEntObstruction ? endEntObstruction.GetControlGroup() : undefined],
"excludeFromResult": true
});
}
}
else
previewEntities.push({
"template": wallSet.templates.tower,
"pos": end.pos,
"angle": previewEntities.length ? previewEntities[previewEntities.length - 1].angle : this.placementWallLastAngle
});
}
let cmpTerrain = Engine.QueryInterface(SYSTEM_ENTITY, IID_Terrain);
if (!cmpTerrain)
{
error("[SetWallPlacementPreview] System Terrain component not found");
return false;
}
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
if (!cmpRangeManager)
{
error("[SetWallPlacementPreview] System RangeManager component not found");
return false;
}
// Loop through the preview entities, and construct the subset of them that need to be, and can be, validly constructed
// to build at least a part of the wall (meaning that the subset is truncated after the first entity that needs to be,
// but cannot validly be, constructed). See method-level documentation for more details.
let allPiecesValid = true;
// Number of entities that are required to build the entire wall, regardless of validity.
let numRequiredPieces = 0;
for (let i = 0; i < previewEntities.length; ++i)
{
let entInfo = previewEntities[i];
let ent = null;
let tpl = entInfo.template;
let tplData = this.placementWallEntities[tpl].templateData;
let entPool = this.placementWallEntities[tpl];
if (entPool.numUsed >= entPool.entities.length)
{
ent = Engine.AddLocalEntity("preview|" + tpl);
entPool.entities.push(ent);
}
else
ent = entPool.entities[entPool.numUsed];
if (!ent)
{
error("[SetWallPlacementPreview] Failed to allocate or reuse preview entity of template '" + tpl + "'");
continue;
}
// Move piece to right location.
// TODO: Consider reusing SetBuildingPlacementReview for this, enhanced to be able to deal with multiple entities.
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (cmpPosition)
{
cmpPosition.JumpTo(entInfo.pos.x, entInfo.pos.z);
cmpPosition.SetYRotation(entInfo.angle);
// If this piece is a tower, then it should have a Y position that is at least as high as its surrounding pieces.
if (tpl === wallSet.templates.tower)
{
let terrainGroundPrev = null;
let terrainGroundNext = null;
if (i > 0)
terrainGroundPrev = cmpTerrain.GetGroundLevel(previewEntities[i - 1].pos.x, previewEntities[i - 1].pos.z);
if (i < previewEntities.length - 1)
terrainGroundNext = cmpTerrain.GetGroundLevel(previewEntities[i + 1].pos.x, previewEntities[i + 1].pos.z);
if (terrainGroundPrev != null || terrainGroundNext != null)
{
let targetY = Math.max(terrainGroundPrev, terrainGroundNext);
cmpPosition.SetHeightFixed(targetY);
}
}
}
let cmpObstruction = Engine.QueryInterface(ent, IID_Obstruction);
if (!cmpObstruction)
{
error("[SetWallPlacementPreview] Preview entity of template '" + tpl + "' does not have an Obstruction component");
continue;
}
// Assign any predefined control groups. Note that there can only be 0, 1 or 2 predefined control groups; if there are
// more, we've made a programming error. The control groups are assigned from the entInfo.controlGroups array on a
// first-come first-served basis; the first value in the array is always assigned as the primary control group, and
// any second value as the secondary control group.
// By default, we reset the control groups to their standard values. Remember that we're reusing entities; if we don't
// reset them, then an ending wall segment that was e.g. at one point snapped to an existing tower, and is subsequently
// reused as a non-snapped ending wall segment, would no longer be capable of being obstructed by the same tower it was
// once snapped to.
let primaryControlGroup = ent;
let secondaryControlGroup = INVALID_ENTITY;
if (entInfo.controlGroups && entInfo.controlGroups.length > 0)
{
if (entInfo.controlGroups.length > 2)
{
error("[SetWallPlacementPreview] Encountered preview entity of template '" + tpl + "' with more than 2 initial control groups");
break;
}
primaryControlGroup = entInfo.controlGroups[0];
if (entInfo.controlGroups.length > 1)
secondaryControlGroup = entInfo.controlGroups[1];
}
cmpObstruction.SetControlGroup(primaryControlGroup);
cmpObstruction.SetControlGroup2(secondaryControlGroup);
let validPlacement = false;
let cmpOwnership = Engine.QueryInterface(ent, IID_Ownership);
cmpOwnership.SetOwner(player);
// Check whether it's in a visible or fogged region.
// TODO: Should definitely reuse SetBuildingPlacementPreview, this is just straight up copy/pasta.
let visible = cmpRangeManager.GetLosVisibility(ent, player) != "hidden";
if (visible)
{
let cmpBuildRestrictions = Engine.QueryInterface(ent, IID_BuildRestrictions);
if (!cmpBuildRestrictions)
{
error("[SetWallPlacementPreview] cmpBuildRestrictions not defined for preview entity of template '" + tpl + "'");
continue;
}
// TODO: Handle results of CheckPlacement.
validPlacement = cmpBuildRestrictions && cmpBuildRestrictions.CheckPlacement().success;
// If a wall piece has two control groups, it's likely a segment that spans
// between two existing towers. To avoid placing a duplicate wall segment,
// check for collisions with entities that share both control groups.
if (validPlacement && entInfo.controlGroups && entInfo.controlGroups.length > 1)
validPlacement = cmpObstruction.CheckDuplicateFoundation();
}
allPiecesValid = allPiecesValid && validPlacement;
// The requirement below that all pieces so far have to have valid positions, rather than only this single one,
// ensures that no more foundations will be placed after a first invalidly-positioned piece. (It is possible
// for pieces past some invalidly-positioned ones to still have valid positions, e.g. if you drag a wall
// through and past an existing building).
// Additionally, the excludeFromResult flag is set for preview entities that were manually added to be placed
// on top of foundations of incompleted towers that we snapped to; they must not be part of the result.
if (!entInfo.excludeFromResult)
++numRequiredPieces;
if (allPiecesValid && !entInfo.excludeFromResult)
{
result.pieces.push({
"template": tpl,
"x": entInfo.pos.x,
"z": entInfo.pos.z,
"angle": entInfo.angle,
});
this.placementWallLastAngle = entInfo.angle;
// Grab the cost of this wall piece and add it up (note; preview entities don't have their Cost components
// copied over, so we need to fetch it from the template instead).
// TODO: We should really use a Cost object or at least some utility functions for this, this is mindless
// boilerplate that's probably duplicated in tons of places.
for (let res of Resources.GetCodes().concat(["population", "time"]))
result.cost[res] += tplData.cost[res];
}
let canAfford = true;
let cmpPlayer = QueryPlayerIDInterface(player, IID_Player);
if (cmpPlayer && cmpPlayer.GetNeededResources(result.cost))
canAfford = false;
let cmpVisual = Engine.QueryInterface(ent, IID_Visual);
if (cmpVisual)
{
if (!allPiecesValid || !canAfford)
cmpVisual.SetShadingColor(1.4, 0.4, 0.4, 1);
else
cmpVisual.SetShadingColor(1, 1, 1, 1);
}
++entPool.numUsed;
}
// If any were entities required to build the wall, but none of them could be validly positioned, return failure
// (see method-level documentation).
if (numRequiredPieces > 0 && result.pieces.length == 0)
return false;
if (start.snappedEnt && start.snappedEnt != INVALID_ENTITY)
result.startSnappedEnt = start.snappedEnt;
// We should only return that we snapped to an entity if all pieces up until that entity can be validly constructed,
// i.e. are included in result.pieces (see docs for the result object).
if (end.pos && end.snappedEnt && end.snappedEnt != INVALID_ENTITY && allPiecesValid)
result.endSnappedEnt = end.snappedEnt;
return result;
};
/**
* Given the current position {data.x, data.z} of an foundation of template data.template, returns the position and angle to snap
* it to (if necessary/useful).
*
* @param data.x The X position of the foundation to snap.
* @param data.z The Z position of the foundation to snap.
* @param data.template The template to get the foundation snapping data for.
* @param data.snapEntities Optional; list of entity IDs to snap to if {data.x, data.z} is within a circle of radius data.snapRadius
* around the entity. Only takes effect when used in conjunction with data.snapRadius.
* When this option is used and the foundation is found to snap to one of the entities passed in this list
* (as opposed to e.g. snapping to terrain features), then the result will contain an additional key "ent",
* holding the ID of the entity that was snapped to.
* @param data.snapRadius Optional; when used in conjunction with data.snapEntities, indicates the circle radius around an entity that
* {data.x, data.z} must be located within to have it snap to that entity.
*/
GuiInterface.prototype.GetFoundationSnapData = function(player, data)
{
let template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate(data.template);
if (!template)
{
warn("[GetFoundationSnapData] Failed to load template '" + data.template + "'");
return false;
}
if (data.snapEntities && data.snapRadius && data.snapRadius > 0)
{
// See if {data.x, data.z} is inside the snap radius of any of the snap entities; and if so, to which it is closest.
// (TODO: Break unlikely ties by choosing the lowest entity ID.)
let minDist2 = -1;
let minDistEntitySnapData = null;
let radius2 = data.snapRadius * data.snapRadius;
for (let ent of data.snapEntities)
{
let cmpPosition = Engine.QueryInterface(ent, IID_Position);
if (!cmpPosition || !cmpPosition.IsInWorld())
continue;
let pos = cmpPosition.GetPosition();
let dist2 = (data.x - pos.x) * (data.x - pos.x) + (data.z - pos.z) * (data.z - pos.z);
if (dist2 > radius2)
continue;
if (minDist2 < 0 || dist2 < minDist2)
{
minDist2 = dist2;
minDistEntitySnapData = {
"x": pos.x,
"z": pos.z,
"angle": cmpPosition.GetRotation().y,
"ent": ent
};
}
}
if (minDistEntitySnapData != null)
return minDistEntitySnapData;
}
if (data.snapToEdges)
{
let position = this.obstructionSnap.getPosition(data, template);
if (position)
return position;
}
if (template.BuildRestrictions.PlacementType == "shore")
{
let angle = GetDockAngle(template, data.x, data.z);
if (angle !== undefined)
return {
"x": data.x,
"z": data.z,
"angle": angle
};
}
return false;
};
GuiInterface.prototype.PlaySound = function(player, data)
{
if (!data.entity)
return;
PlaySound(data.name, data.entity);
};
/**
* Find any idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.prevUnit The previous idle unit, if calling a second time to iterate through units. May be left undefined.
* @param data.limit The number of idle units to return. May be left undefined (will return all idle units).
* @param data.excludeUnits Array of units to exclude.
*
* Returns an array of idle units.
* If multiple classes were supplied, and multiple items will be returned, the items will be sorted by class.
*/
GuiInterface.prototype.FindIdleUnits = function(player, data)
{
let idleUnits = [];
// The general case is that only the 'first' idle unit is required; filtering would examine every unit.
// This loop imitates a grouping/aggregation on the first matching idle class.
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
for (let entity of cmpRangeManager.GetEntitiesByPlayer(player))
{
let filtered = this.IdleUnitFilter(entity, data.idleClasses, data.excludeUnits);
if (!filtered.idle)
continue;
// If the entity is in the 'current' (first, 0) bucket on a resumed search, it must be after the "previous" unit, if any.
// By adding to the 'end', there is no pause if the series of units loops.
let bucket = filtered.bucket;
if (bucket == 0 && data.prevUnit && entity <= data.prevUnit)
bucket = data.idleClasses.length;
if (!idleUnits[bucket])
idleUnits[bucket] = [];
idleUnits[bucket].push(entity);
// If enough units have been collected in the first bucket, go ahead and return them.
if (data.limit && bucket == 0 && idleUnits[0].length == data.limit)
return idleUnits[0];
}
let reduced = idleUnits.reduce((prev, curr) => prev.concat(curr), []);
if (data.limit && reduced.length > data.limit)
return reduced.slice(0, data.limit);
return reduced;
};
/**
* Discover if the player has idle units.
*
* @param data.idleClasses Array of class names to include.
* @param data.excludeUnits Array of units to exclude.
*
* Returns a boolean of whether the player has any idle units
*/
GuiInterface.prototype.HasIdleUnits = function(player, data)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
return cmpRangeManager.GetEntitiesByPlayer(player).some(unit => this.IdleUnitFilter(unit, data.idleClasses, data.excludeUnits).idle);
};
/**
* Whether to filter an idle unit
*
* @param unit The unit to filter.
* @param idleclasses Array of class names to include.
* @param excludeUnits Array of units to exclude.
*
* Returns an object with the following fields:
* - idle - true if the unit is considered idle by the filter, false otherwise.
* - bucket - if idle, set to the index of the first matching idle class, undefined otherwise.
*/
GuiInterface.prototype.IdleUnitFilter = function(unit, idleClasses, excludeUnits)
{
let cmpUnitAI = Engine.QueryInterface(unit, IID_UnitAI);
if (!cmpUnitAI || !cmpUnitAI.IsIdle() || cmpUnitAI.IsGarrisoned())
return { "idle": false };
let cmpIdentity = Engine.QueryInterface(unit, IID_Identity);
if (!cmpIdentity)
return { "idle": false };
let bucket = idleClasses.findIndex(elem => MatchesClassList(cmpIdentity.GetClassesList(), elem));
if (bucket == -1 || excludeUnits.indexOf(unit) > -1)
return { "idle": false };
return { "idle": true, "bucket": bucket };
};
GuiInterface.prototype.GetTradingRouteGain = function(player, data)
{
if (!data.firstMarket || !data.secondMarket)
return null;
return CalculateTraderGain(data.firstMarket, data.secondMarket, data.template);
};
GuiInterface.prototype.GetTradingDetails = function(player, data)
{
let cmpEntityTrader = Engine.QueryInterface(data.trader, IID_Trader);
if (!cmpEntityTrader || !cmpEntityTrader.CanTrade(data.target))
return null;
let firstMarket = cmpEntityTrader.GetFirstMarket();
let secondMarket = cmpEntityTrader.GetSecondMarket();
let result = null;
if (data.target === firstMarket)
{
result = {
"type": "is first",
"hasBothMarkets": cmpEntityTrader.HasBothMarkets()
};
if (cmpEntityTrader.HasBothMarkets())
result.gain = cmpEntityTrader.GetGoods().amount;
}
else if (data.target === secondMarket)
result = {
"type": "is second",
"gain": cmpEntityTrader.GetGoods().amount,
};
else if (!firstMarket)
result = { "type": "set first" };
else if (!secondMarket)
result = {
"type": "set second",
"gain": cmpEntityTrader.CalculateGain(firstMarket, data.target),
};
else
result = { "type": "set first" };
return result;
};
GuiInterface.prototype.CanAttack = function(player, data)
{
let cmpAttack = Engine.QueryInterface(data.entity, IID_Attack);
return cmpAttack && cmpAttack.CanAttack(data.target, data.types || undefined);
};
/*
* Returns batch build time.
*/
GuiInterface.prototype.GetBatchTime = function(player, data)
{
let cmpProductionQueue = Engine.QueryInterface(data.entity, IID_ProductionQueue);
if (!cmpProductionQueue)
return 0;
return cmpProductionQueue.GetBatchTime(data.batchSize);
};
GuiInterface.prototype.IsMapRevealed = function(player)
{
return Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).GetLosRevealAll(player);
};
GuiInterface.prototype.SetPathfinderDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetPathfinderHierDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_Pathfinder).SetHierDebugOverlay(enabled);
};
GuiInterface.prototype.SetObstructionDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_ObstructionManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.SetMotionDebugOverlay = function(player, data)
{
for (let ent of data.entities)
{
let cmpUnitMotion = Engine.QueryInterface(ent, IID_UnitMotion);
if (cmpUnitMotion)
cmpUnitMotion.SetDebugOverlay(data.enabled);
}
};
GuiInterface.prototype.SetRangeDebugOverlay = function(player, enabled)
{
Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager).SetDebugOverlay(enabled);
};
GuiInterface.prototype.GetTraderNumber = function(player)
{
let cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
let traders = cmpRangeManager.GetEntitiesByPlayer(player).filter(e => Engine.QueryInterface(e, IID_Trader));
let landTrader = { "total": 0, "trading": 0, "garrisoned": 0 };
let shipTrader = { "total": 0, "trading": 0 };
for (let ent of traders)
{
let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
let cmpUnitAI = Engine.QueryInterface(ent, IID_UnitAI);
if (!cmpIdentity || !cmpUnitAI)
continue;
if (cmpIdentity.HasClass("Ship"))
{
++shipTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++shipTrader.trading;
}
else
{
++landTrader.total;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Trade")
++landTrader.trading;
if (cmpUnitAI.order && cmpUnitAI.order.type == "Garrison")
{
let holder = cmpUnitAI.order.data.target;
let cmpHolderUnitAI = Engine.QueryInterface(holder, IID_UnitAI);
if (cmpHolderUnitAI && cmpHolderUnitAI.order && cmpHolderUnitAI.order.type == "Trade")
++landTrader.garrisoned;
}
}
}
return { "landTrader": landTrader, "shipTrader": shipTrader };
};
GuiInterface.prototype.GetTradingGoods = function(player)
{
let cmpPlayer = QueryPlayerIDInterface(player);
if (!cmpPlayer)
return [];
return cmpPlayer.GetTradingGoods();
};
GuiInterface.prototype.OnGlobalEntityRenamed = function(msg)
{
this.renamedEntities.push(msg);
};
/**
* List the GuiInterface functions that can be safely called by GUI scripts.
* (GUI scripts are non-deterministic and untrusted, so these functions must be
* appropriately careful. They are called with a first argument "player", which is
* trusted and indicates the player associated with the current client; no data should
* be returned unless this player is meant to be able to see it.)
*/
let exposedFunctions = {
"GetSimulationState": 1,
"GetExtendedSimulationState": 1,
"GetInitAttributes": 1,
"GetReplayMetadata": 1,
+ "GetCampaignGameEndData": 1,
"GetRenamedEntities": 1,
"ClearRenamedEntities": 1,
"GetEntityState": 1,
"GetMultipleEntityStates": 1,
"GetAverageRangeForBuildings": 1,
"GetTemplateData": 1,
"IsTechnologyResearched": 1,
"CheckTechnologyRequirements": 1,
"GetStartedResearch": 1,
"GetBattleState": 1,
"GetIncomingAttacks": 1,
"GetNeededResources": 1,
"GetNotifications": 1,
"GetTimeNotifications": 1,
"GetAvailableFormations": 1,
"GetFormationRequirements": 1,
"CanMoveEntsIntoFormation": 1,
"IsFormationSelected": 1,
"GetFormationInfoFromTemplate": 1,
"IsStanceSelected": 1,
"UpdateDisplayedPlayerColors": 1,
"SetSelectionHighlight": 1,
"GetAllBuildableEntities": 1,
"SetStatusBars": 1,
"GetPlayerEntities": 1,
"GetNonGaiaEntities": 1,
"DisplayRallyPoint": 1,
"AddTargetMarker": 1,
"SetBuildingPlacementPreview": 1,
"SetWallPlacementPreview": 1,
"GetFoundationSnapData": 1,
"PlaySound": 1,
"FindIdleUnits": 1,
"HasIdleUnits": 1,
"GetTradingRouteGain": 1,
"GetTradingDetails": 1,
"CanAttack": 1,
"GetBatchTime": 1,
"IsMapRevealed": 1,
"SetPathfinderDebugOverlay": 1,
"SetPathfinderHierDebugOverlay": 1,
"SetObstructionDebugOverlay": 1,
"SetMotionDebugOverlay": 1,
"SetRangeDebugOverlay": 1,
"EnableVisualRangeOverlayType": 1,
"SetRangeOverlays": 1,
"GetTraderNumber": 1,
"GetTradingGoods": 1,
"IsTemplateModified": 1,
"ResetTemplateModified": 1,
"IsSelectionDirty": 1,
"ResetSelectionDirty": 1
};
GuiInterface.prototype.ScriptCall = function(player, name, args)
{
if (exposedFunctions[name])
return this[name](player, args);
throw new Error("Invalid GuiInterface Call name \"" + name + "\"");
};
Engine.RegisterSystemComponentType(IID_GuiInterface, "GuiInterface", GuiInterface);
Index: ps/trunk/binaries/data/mods/mod/gui/common/modern/styles.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/common/modern/styles.xml (revision 24978)
+++ ps/trunk/binaries/data/mods/mod/gui/common/modern/styles.xml (revision 24979)
@@ -1,193 +1,192 @@
Index: ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml
===================================================================
--- ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24978)
+++ ps/trunk/binaries/data/mods/mod/gui/modmod/modmod.xml (revision 24979)
@@ -1,206 +1,206 @@
Index: ps/trunk/binaries/data/mods/public/campaigns/tutorial.json
===================================================================
--- ps/trunk/binaries/data/mods/public/campaigns/tutorial.json (nonexistent)
+++ ps/trunk/binaries/data/mods/public/campaigns/tutorial.json (revision 24979)
@@ -0,0 +1,23 @@
+{
+ "Name": "Tutorial",
+ "Description": "Learn how to play 0 A.D.",
+ "Image": "session/icons/mappreview/Introductory_Tutorial.png",
+ "Levels": {
+ "introduction": {
+ "Name": "Introductory Tutorial",
+ "Map": "tutorials/introductory_tutorial.xml",
+ "MapType": "scenario",
+ "Description": "This is a basic tutorial to get you started playing 0 A.D.",
+ "Preview": "session/icons/mappreview/Introductory_Tutorial.png"
+ },
+ "eco_walkthrough": {
+ "Name": "Economy Walkthrough",
+ "Map": "tutorials/starting_economy_walkthrough.xml",
+ "MapType": "scenario",
+ "Description": "This map will give a rough guide for starting the game effectively. Early in the game the most important thing is to gather resources as fast as possible so you are able to build enough troops later. Warning: This is very fast at the start, be prepared to run through the initial bit several times.",
+ "Requires": "introduction"
+ }
+ },
+ "Order": ["introduction", "eco_walkthrough"],
+ "ShowUnavailable": true
+}
Property changes on: ps/trunk/binaries/data/mods/public/campaigns/tutorial.json
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/common_scripts.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/common_scripts.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/common_scripts.xml (revision 24979)
@@ -0,0 +1,5 @@
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/common_scripts.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js (revision 24979)
@@ -0,0 +1,162 @@
+/**
+ * This is the main menu screen of the campaign.
+ * It shows you the currently available scenarios, scenarios you've already completed, etc.
+ * This particular variant is extremely simple and shows a list similar to Age 1's campaigns,
+ * but conceptually nothing really prevents more complex systems.
+ */
+class CampaignMenu extends AutoWatcher
+{
+ constructor(campaignRun)
+ {
+ super("render");
+
+ this.run = campaignRun;
+
+ this.selectedLevel = -1;
+ this.levelSelection = Engine.GetGUIObjectByName("levelSelection");
+ this.levelSelection.onSelectionChange = () => { this.selectedLevel = this.levelSelection.selected; };
+
+ this.levelSelection.onMouseLeftDoubleClickItem = () => this.startScenario();
+ Engine.GetGUIObjectByName('startButton').onPress = () => this.startScenario();
+ Engine.GetGUIObjectByName('backToMain').onPress = () => this.goBackToMainMenu();
+ Engine.GetGUIObjectByName('savedGamesButton').onPress = () => Engine.PushGuiPage('page_loadgame.xml', {
+ 'campaignRun': this.run.filename
+ });
+
+ this.mapCache = new MapCache();
+
+ this._ready = true;
+ }
+
+ goBackToMainMenu()
+ {
+ this.run.save();
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+ }
+
+ startScenario()
+ {
+ let level = this.getSelectedLevelData();
+ if (!meetsRequirements(this.run, level))
+ return;
+ Engine.SwitchGuiPage("page_gamesetup.xml", {
+ "mapType": level.MapType,
+ "map": "maps/" + level.Map,
+ "autostart": true,
+ "campaignData": {
+ "run": this.run.filename,
+ "levelID": this.levelSelection.list_data[this.selectedLevel],
+ "data": this.run.data
+ }
+ });
+ }
+
+ getSelectedLevelData()
+ {
+ if (this.selectedLevel === -1)
+ return undefined;
+ return this.run.template.Levels[this.levelSelection.list_data[this.selectedLevel]];
+ }
+
+ shouldShowLevel(levelData)
+ {
+ if (this.run.template.ShowUnavailable)
+ return true;
+
+ return meetsRequirements(this.run, levelData);
+ }
+
+ getLevelName(levelData)
+ {
+ if (levelData.Name)
+ return translateWithContext("Campaign Template", levelData.Name);
+ return translate(this.mapCache.getTranslatableMapName(levelData.MapType, "maps/" + levelData.Map));
+ }
+
+ getLevelDescription(levelData)
+ {
+ if (levelData.Description)
+ return translateWithContext("Campaign Template", levelData.Description);
+ return this.mapCache.getTranslatedMapDescription(levelData.MapType, "maps/" + levelData.Map);
+
+ }
+
+ displayLevelsList()
+ {
+ let list = [];
+ for (let key in this.run.template.Levels)
+ {
+ let level = this.run.template.Levels[key];
+
+ if (!this.shouldShowLevel(level))
+ continue;
+
+ let status = "";
+ let name = this.getLevelName(level);
+ if (isCompleted(this.run, key))
+ status = translateWithContext("campaign status", "Completed");
+ else if (meetsRequirements(this.run, level))
+ status = coloredText(translateWithContext("campaign status", "Available"), "green");
+ else
+ name = coloredText(name, "gray");
+
+ list.push({ "ID": key, "name": name, "status": status });
+ }
+
+ list.sort((a, b) => this.run.template.Order.indexOf(a.ID) - this.run.template.Order.indexOf(b.ID));
+
+ list = prepareForDropdown(list);
+
+ this.levelSelection.list_name = list.name || [];
+ this.levelSelection.list_status = list.status || [];
+
+ // COList needs these changed last or crashes.
+ this.levelSelection.list = list.ID || [];
+ this.levelSelection.list_data = list.ID || [];
+ }
+
+ displayLevelDetails()
+ {
+ if (this.selectedLevel === -1)
+ {
+ Engine.GetGUIObjectByName("startButton").enabled = false;
+ Engine.GetGUIObjectByName("startButton").hidden = false;
+ return;
+ }
+
+ let level = this.getSelectedLevelData();
+
+ Engine.GetGUIObjectByName("scenarioName").caption = this.getLevelName(level);
+ Engine.GetGUIObjectByName("scenarioDesc").caption = this.getLevelDescription(level);
+ if (level.Preview)
+ Engine.GetGUIObjectByName('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":" + level.Preview;
+ else
+ Engine.GetGUIObjectByName('levelPreviewBox').sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+
+ Engine.GetGUIObjectByName("startButton").enabled = meetsRequirements(this.run, level);
+ Engine.GetGUIObjectByName("startButton").hidden = false;
+ Engine.GetGUIObjectByName("loadSavedButton").hidden = true;
+ }
+
+ render()
+ {
+ Engine.GetGUIObjectByName("campaignTitle").caption = this.run.getLabel();
+ this.displayLevelDetails();
+ this.displayLevelsList();
+ }
+}
+
+
+var g_CampaignMenu;
+
+function init(initData)
+{
+ let run;
+ try {
+ run = new CampaignRun(initData.filename).load();
+ } catch (err) {
+ error(sprintf(translate("Error loading campaign run %s: %s."), initData.filename, err));
+ Engine.SwitchGuiPage("page_pregame.xml", {});
+ }
+ g_CampaignMenu = new CampaignMenu(run);
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml (revision 24979)
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+ Campaign Name
+
+
+
+
+
+
+
+ Scenario Name
+
+
+
+ Status
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No scenario selected
+
+
+
+
+
+
+
+
+
+
+
+
+ Back to Main Menu
+
+
+
+ Saved Games
+
+
+
+ Start Scenario
+
+
+ Resume Saved Game
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/CampaignMenu.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.js (revision 24979)
@@ -0,0 +1,11 @@
+/**
+ * This is a transient page, triggered at the end of a game session,
+ * to perform custom computations on the endgame data.
+ */
+
+function init(endGameData)
+{
+ let run = CampaignRun.getCurrentRun();
+ if (endGameData.won)
+ markLevelComplete(run, endGameData.levelID);
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.xml (revision 24979)
@@ -0,0 +1,6 @@
+
+
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/endgame.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/page.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/page.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/page.xml (revision 24979)
@@ -0,0 +1,5 @@
+
+
+ campaigns/common_scripts.xml
+ campaigns/default_menu/endgame/endgame.xml
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/endgame/page.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/page.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/page.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/page.xml (revision 24979)
@@ -0,0 +1,13 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaigns/common_scripts.xml
+ campaigns/default_menu/CampaignMenu.xml
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/page.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/utils.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/utils.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/utils.js (revision 24979)
@@ -0,0 +1,26 @@
+/**
+ * Various utilities.
+ */
+function markLevelComplete(run, levelID)
+{
+ if (!isCompleted(run, levelID))
+ {
+ if (!run.data.completedLevels)
+ run.data.completedLevels = [];
+ run.data.completedLevels.push(levelID);
+ run.save();
+ }
+}
+
+function isCompleted(run, levelID)
+{
+ return run.data.completedLevels && run.data.completedLevels.indexOf(levelID) !== -1;
+}
+
+function meetsRequirements(run, levelData)
+{
+ if (!levelData.Requires)
+ return true;
+
+ return MatchesClassList(run.data.completedLevels || [], levelData.Requires);
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/default_menu/utils.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js (revision 24979)
@@ -0,0 +1,140 @@
+/**
+ * Mimics CampaignRun's necessary interface, but for a broken run
+ * (i.e. a run that can't be loaded), thus allowing to delete it.
+ */
+class BrokenRun
+{
+ constructor(file)
+ {
+ this.filename = file;
+ }
+
+ getLabel(forList)
+ {
+ if (!forList)
+ return this.filename + ".0adcampaign";
+ return coloredText(sprintf("%(filename)s (%(error)s)", {
+ "filename": this.filename + ".0adcampaign",
+ "error": translate("file cannot be loaded")
+ }), "red");
+ }
+
+ destroy()
+ {
+ Engine.DeleteCampaignSave("saves/campaigns/" + this.filename + ".0adcampaign");
+ }
+}
+
+/**
+ * Lets you load/delete/look at existing campaign runs in your user folder.
+ */
+class LoadModal extends AutoWatcher
+{
+ constructor(campaignTemplate)
+ {
+ super("render");
+
+ // _watch so render() is called anytime currentRuns are modified.
+ this.currentRuns = _watch(this.getRuns(), () => this.render());
+
+ Engine.GetGUIObjectByName('cancelButton').onPress = () => Engine.SwitchGuiPage("page_pregame.xml", {});
+ Engine.GetGUIObjectByName('deleteGameButton').onPress = () => this.deleteSelectedRun();
+ Engine.GetGUIObjectByName('startButton').onPress = () => this.startSelectedRun();
+
+ this.noCampaignsText = Engine.GetGUIObjectByName("noCampaignsText");
+
+ this.selectedRun = -1;
+ this.runSelection = Engine.GetGUIObjectByName("runSelection");
+ this.runSelection.onSelectionChange = () => {
+ this.selectedRun = this.runSelection.selected;
+ if (this.selectedRun === -1)
+ Engine.GetGUIObjectByName('runDescription').caption = "";
+ else
+ Engine.GetGUIObjectByName('runDescription').caption = this.currentRuns[this.selectedRun].getLabel();
+ };
+
+ this.runSelection.onMouseLeftDoubleClickItem = () => this.startSelectedRun();
+
+ this._ready = true;
+ }
+
+ getRuns()
+ {
+ let out = [];
+ let files = Engine.ListDirectoryFiles("saves/campaigns/", "*.0adcampaign", false);
+ for (let file of files)
+ {
+ let name = file.replace("saves/campaigns/", "").replace(".0adcampaign", "");
+ try
+ {
+ out.push(new CampaignRun(name).load());
+ }
+ catch(err)
+ {
+ error(err);
+ out.push(new BrokenRun(name));
+ }
+ }
+ return out;
+ }
+
+ loadCampaign()
+ {
+ let filename = this.currentRuns[this.selectedRun].filename;
+ let run = new CampaignRun(filename)
+ .load()
+ .setCurrent();
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+
+ deleteSelectedRun()
+ {
+ if (this.selectedRun === -1)
+ return;
+
+ let run = this.currentRuns[this.selectedRun];
+
+ messageBox(
+ 400, 200,
+ sprintf(translate("Are you sure you want to delete run %s? This cannot be undone."), run.getLabel()),
+ translate("Confirmation"),
+ [translate("No"), translate("Yes")],
+ [null, () => {
+ run.destroy();
+ this.currentRuns.splice(this.selectedRun, 1);
+ this.selectedRun = -1;
+ }]
+ );
+ }
+
+ startSelectedRun()
+ {
+ if (this.currentRuns[this.selectedRun] instanceof CampaignRun)
+ this.loadCampaign();
+ }
+
+ displayCurrentRuns()
+ {
+ this.runSelection.list = this.currentRuns.map(run => run.getLabel(true));
+ this.runSelection.list_data = this.currentRuns.map(run => run.filename);
+ }
+
+ render()
+ {
+ this.noCampaignsText.hidden = !!this.currentRuns.length;
+ Engine.GetGUIObjectByName('deleteGameButton').enabled = this.selectedRun !== -1;
+ Engine.GetGUIObjectByName('startButton').enabled = this.selectedRun !== -1 && this.currentRuns[this.selectedRun] instanceof CampaignRun;
+ this.displayCurrentRuns();
+ }
+}
+
+
+var g_LoadModal;
+
+function init()
+{
+ g_LoadModal = new LoadModal();
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml (revision 24979)
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Load Campaign
+
+
+
+
+
+
+ No ongoing campaigns.
+
+
+
+ Name of selected run:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Delete
+
+
+
+ Load Campaign
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/LoadModal.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/page.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/page.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/page.xml (revision 24979)
@@ -0,0 +1,13 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaigns/common_scripts.xml
+ campaigns/load_modal/LoadModal.xml
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/load_modal/page.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js (revision 24979)
@@ -0,0 +1,42 @@
+/**
+ * Modal screen that pops up when you start a new campaign from the setup screen.
+ * asking you to name it.
+ * Will then create the file with the according name and start everything up.
+ */
+class NewCampaignModal
+{
+ constructor(campaignTemplate)
+ {
+ this.template = campaignTemplate;
+
+ Engine.GetGUIObjectByName('cancelButton').onPress = () => Engine.PopGuiPage();
+ Engine.GetGUIObjectByName('startButton').onPress = () => this.createAndStartCampaign();
+ Engine.GetGUIObjectByName('runDescription').caption = this.template.Name;
+ Engine.GetGUIObjectByName('runDescription').onTextEdit = () => {
+ Engine.GetGUIObjectByName('startButton').enabled = Engine.GetGUIObjectByName('runDescription').caption.length > 0;
+ };
+ Engine.GetGUIObjectByName('runDescription').focus();
+ }
+
+ createAndStartCampaign()
+ {
+ let filename = this.template.identifier + "_" + Date.now() + "_" + Math.floor(Math.random()*100000);
+ let run = new CampaignRun(filename)
+ .setTemplate(this.template)
+ .setMeta(Engine.GetGUIObjectByName('runDescription').caption)
+ .save()
+ .setCurrent();
+
+ Engine.SwitchGuiPage(run.getMenuPath(), {
+ "filename": filename
+ });
+ }
+}
+
+
+var g_NewCampaignModal;
+
+function init(campaign_template_data)
+{
+ g_NewCampaignModal = new NewCampaignModal(campaign_template_data);
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml (revision 24979)
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+ Start a campaign
+
+
+
+ Please enter the name of your new campaign run:
+
+
+
+
+
+
+ Cancel
+
+
+
+ Start Campaign
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/NewCampaignModal.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/page.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/page.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/page.xml (revision 24979)
@@ -0,0 +1,14 @@
+
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaigns/common_scripts.xml
+ campaigns/new_modal/NewCampaignModal.xml
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/new_modal/page.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js (revision 24979)
@@ -0,0 +1,71 @@
+/**
+ * The campaign setup page shows you the list of available campaigns,
+ * some information about them, and lets you start a new one.
+ */
+class CampaignSetupPage extends AutoWatcher
+{
+ constructor()
+ {
+ super("render");
+
+ this.selectedIndex = -1;
+ this.templates = CampaignTemplate.getAvailableTemplates();
+
+ Engine.GetGUIObjectByName("mainMenuButton").onPress = () => Engine.SwitchGuiPage("page_pregame.xml");
+ Engine.GetGUIObjectByName("startCampButton").onPress = () => Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate);
+
+ this.campaignSelection = Engine.GetGUIObjectByName("campaignSelection");
+ this.campaignSelection.onMouseLeftDoubleClickItem = () => {
+ if (this.selectedIndex === -1)
+ return;
+ Engine.PushGuiPage("campaigns/new_modal/page.xml", this.selectedTemplate);
+ };
+ this.campaignSelection.onSelectionChange = () => {
+ this.selectedIndex = this.campaignSelection.selected;
+ if (this.selectedIndex !== -1)
+ this.selectedTemplate = this.templates[this.selectedIndex];
+ else
+ this.selectedTemplate = null;
+ };
+
+ this._ready = true;
+ }
+
+ displayCampaignDetails()
+ {
+ Engine.GetGUIObjectByName("startCampButton").enabled = this.selectedIndex !== -1;
+
+ if (!this.selectedTemplate)
+ {
+ Engine.GetGUIObjectByName("campaignTitle").caption = translate("No campaign selected.");
+ Engine.GetGUIObjectByName("campaignDesc").caption = "";
+ Engine.GetGUIObjectByName("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ return;
+ }
+
+ Engine.GetGUIObjectByName("campaignTitle").caption = translateWithContext("Campaign Template", this.selectedTemplate.Name);
+ Engine.GetGUIObjectByName("campaignDesc").caption = translateWithContext("Campaign Template", this.selectedTemplate.Description);
+ if ('Image' in this.selectedTemplate)
+ Engine.GetGUIObjectByName("campaignImage").sprite = "stretched:" + this.selectedTemplate.Image;
+ else
+ Engine.GetGUIObjectByName("campaignImage").sprite = "cropped:" + 400/512 + "," + 300/512 + ":session/icons/mappreview/nopreview.png";
+ }
+
+ render()
+ {
+ this.displayCampaignDetails();
+
+ Engine.GetGUIObjectByName("campaignSelection").list_name = this.templates.map((camp) => translateWithContext("Campaign Template", camp.Name));
+ // COList needs these changed last or crashes.
+ Engine.GetGUIObjectByName("campaignSelection").list = this.templates.map((camp) => camp.identifier) || [];
+ Engine.GetGUIObjectByName("campaignSelection").list_data = this.templates.map((camp) => camp.identifier) || [];
+ }
+}
+
+
+var g_CampaignSetupPage;
+
+function init()
+{
+ g_CampaignSetupPage = new CampaignSetupPage();
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml (revision 24979)
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+ Campaigns
+
+
+
+
+
+
+
+ Name
+
+
+
+
+
+
+
+
+
+
+ No Campaign selected
+
+
+
+
+
+
+
+
+
+
+
+
+ Main Menu
+
+
+
+
+ Start Campaign
+
+
+
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/CampaignSetupPage.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/page.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/campaigns/setup/page.xml (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/campaigns/setup/page.xml (revision 24979)
@@ -0,0 +1,13 @@
+
+
+ common/modern/setup.xml
+ common/modern/styles.xml
+ common/modern/sprites.xml
+
+ common/setup.xml
+ common/sprites.xml
+ common/styles.xml
+
+ campaigns/common_scripts.xml
+ campaigns/setup/CampaignSetupPage.xml
+
Property changes on: ps/trunk/binaries/data/mods/public/gui/campaigns/setup/page.xml
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js (revision 24979)
@@ -0,0 +1,129 @@
+// Cached run for CampaignRun.getCurrentRun()
+// TODO: Move this to a static member once linters accept it.
+var g_CurrentCampaignRun;
+
+/**
+ * A campaign "Run" saves metadata on a campaign progession.
+ * It is equivalent to a saved game for a game.
+ * It is named a "run" in an attempt to disambiguate with saved games from campaign runs,
+ * campaign templates, and the actual concept of a campaign at large.
+ */
+class CampaignRun
+{
+ static getCurrentRun()
+ {
+ let current = Engine.ConfigDB_GetValue("user", "currentcampaign");
+ if (g_CurrentCampaignRun && g_CurrentCampaignRun.ID == current)
+ return g_CurrentCampaignRun.run;
+ try
+ {
+ let run = new CampaignRun(current).load();
+ g_CurrentCampaignRun = {
+ "run": run,
+ "ID": current
+ };
+ return run;
+ }
+ catch(error)
+ {
+ return undefined;
+ }
+ }
+
+ constructor(name = "")
+ {
+ this.filename = name;
+ // Metadata on the run, such as its description.
+ this.meta = {};
+ // 'User' data
+ this.data = {};
+ // ID of the campaign templates.
+ this.template = null;
+ }
+
+ setData(data)
+ {
+ if (!data)
+ {
+ warn("Invalid campaign scenario end data. Nothing will be saved.");
+ return this;
+ }
+
+ this.data = data;
+ this.save();
+ return this;
+ }
+
+ setTemplate(template)
+ {
+ this.template = template;
+ this.save();
+ return this;
+ }
+
+ setMeta(description)
+ {
+ this.meta.userDescription = description;
+ this.save();
+ return this;
+ }
+
+ setCurrent()
+ {
+ Engine.ConfigDB_CreateValue("user", "currentcampaign", this.filename);
+ Engine.ConfigDB_WriteValueToFile("user", "currentcampaign", this.filename, "config/user.cfg");
+ return this;
+ }
+
+ getMenuPath()
+ {
+ return "campaigns/" + this.template.interface + "/page.xml";
+ }
+
+ getEndGamePath()
+ {
+ return "campaigns/" + this.template.interface + "/endgame/page.xml";
+ }
+
+ /**
+ * @param forlist - if true, generate a label for listing all runs.
+ * Otherwise, just return a short human readable name.
+ * (not currently used for regular runs).
+ */
+ getLabel(forList)
+ {
+ return sprintf(translate("%(userDesc)s - %(templateName)s"), {
+ "userDesc": this.meta.userDescription,
+ "templateName": this.template.Name
+ });
+ }
+
+ load()
+ {
+ if (!Engine.FileExists("saves/campaigns/" + this.filename + ".0adcampaign"))
+ throw new Error("Campaign file does not exist");
+ let data = Engine.ReadJSONFile("saves/campaigns/" + this.filename + ".0adcampaign");
+ this.data = data.data;
+ this.meta = data.meta;
+ this.template = CampaignTemplate.getTemplate(data.template_identifier);
+ if (!this.template)
+ throw new Error("Campaign template " + data.template_identifier + " does not exist (perhaps it comes from a mod?)");
+ return this;
+ }
+
+ save()
+ {
+ let data = {
+ "data": this.data,
+ "meta": this.meta,
+ "template_identifier": this.template.identifier
+ };
+ Engine.WriteJSONFile("saves/campaigns/" + this.filename + ".0adcampaign", data);
+ return this;
+ }
+
+ destroy()
+ {
+ Engine.DeleteCampaignSave("saves/campaigns/" + this.filename + ".0adcampaign");
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignRun.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js (revision 24979)
@@ -0,0 +1,54 @@
+// TODO: Move this to a static member once linters accept it.
+var g_CachedTemplates;
+
+class CampaignTemplate
+{
+ /**
+ * @returns a dictionary of campaign templates, as [ { 'identifier': id, 'data': data }, ... ]
+ */
+ static getAvailableTemplates()
+ {
+ if (g_CachedTemplates)
+ return g_CachedTemplates;
+
+ let campaigns = Engine.ListDirectoryFiles("campaigns/", "*.json", false);
+
+ g_CachedTemplates = [];
+
+ for (let filename of campaigns)
+ // Use file name as identifier to guarantee unicity.
+ g_CachedTemplates.push(new CampaignTemplate(filename.slice("campaigns/".length, -".json".length)));
+
+ return g_CachedTemplates;
+ }
+
+ static getTemplate(identifier)
+ {
+ if (!g_CachedTemplates)
+ CampaignTemplate.getAvailableTemplates();
+ let temp = g_CachedTemplates.filter(t => t.identifier == identifier);
+ if (!temp.length)
+ return null;
+ return temp[0];
+ }
+
+ constructor(identifier)
+ {
+ Object.assign(this, Engine.ReadJSONFile("campaigns/" + identifier + ".json"));
+
+ this.identifier = identifier;
+
+ if (this.Interface)
+ this.interface = this.Interface;
+ else
+ this.interface = "default_menu";
+
+ if (!this.isValid())
+ throw ("Campaign template " + this.identifier + ".json is not a valid campaign template.");
+ }
+
+ isValid()
+ {
+ return this.Name;
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/common/campaigns/CampaignTemplate.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/common/campaigns/utils.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/common/campaigns/utils.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/common/campaigns/utils.js (revision 24979)
@@ -0,0 +1,38 @@
+/**
+ * Wrap object in a proxy, that calls callback
+ * anytime a property is set, passing the property name as parameter.
+ * Note that this doesn't modify any variable that pointer towards object,
+ * so this is _not_ equivalent to replacing the target object with a proxy.
+ */
+function _watch(object, callback)
+{
+ return new Proxy(object, {
+ "get": (obj, key) => {
+ return obj[key];
+ },
+ "set": (obj, key, value) => {
+ obj[key] = value;
+ callback(key);
+ return true;
+ }
+ });
+}
+
+/**
+ * Inherit from AutoWatcher to make 'this' a proxy object that
+ * watches for its own property changes and calls the method given
+ * (takes a string because 'this' is unavailable when calling 'super').
+ * This can be used to e.g. automatically call a rendering function
+ * if a property is changed.
+ * Using inheritance is necessary because 'this' is immutable,
+ * and isn't defined in the class constructor _unless_ super() is called.
+ * (thus you can't do something like this = new Proxy(this) at the end).
+ */
+class AutoWatcher
+{
+ constructor(method_name)
+ {
+ this._ready = false;
+ return _watch(this, () => this._ready && this[method_name]());
+ }
+}
Property changes on: ps/trunk/binaries/data/mods/public/gui/common/campaigns/utils.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/gamesetup/Controls/GameSettingsControl.js (revision 24979)
@@ -1,301 +1,299 @@
/**
* This class provides a property independent interface to g_GameAttributes events.
* Classes may use this interface in order to react to changing g_GameAttributes.
*/
class GameSettingsControl
{
constructor(setupWindow, netMessages, startGameControl, mapCache)
{
this.startGameControl = startGameControl;
this.mapCache = mapCache;
this.gameSettingsFile = new GameSettingsFile(this);
this.previousMap = undefined;
this.depth = 0;
// This property may be read from publicly
this.autostart = false;
this.gameAttributesChangeHandlers = new Set();
this.gameAttributesBatchChangeHandlers = new Set();
this.gameAttributesFinalizeHandlers = new Set();
this.pickRandomItemsHandlers = new Set();
this.assignPlayerHandlers = new Set();
this.mapChangeHandlers = new Set();
setupWindow.registerLoadHandler(this.onLoad.bind(this));
setupWindow.registerGetHotloadDataHandler(this.onGetHotloadData.bind(this));
startGameControl.registerLaunchGameHandler(this.onLaunchGame.bind(this));
setupWindow.registerClosePageHandler(this.onClose.bind(this));
if (g_IsNetworked)
netMessages.registerNetMessageHandler("gamesetup", this.onGamesetupMessage.bind(this));
}
registerMapChangeHandler(handler)
{
this.mapChangeHandlers.add(handler);
}
unregisterMapChangeHandler(handler)
{
this.mapChangeHandlers.delete(handler);
}
/**
* This message is triggered everytime g_GameAttributes change.
* Handlers may subsequently change g_GameAttributes and trigger this message again.
*/
registerGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.add(handler);
}
unregisterGameAttributesChangeHandler(handler)
{
this.gameAttributesChangeHandlers.delete(handler);
}
/**
* This message is triggered after g_GameAttributes changed and recursed gameAttributesChangeHandlers finished.
* The use case for this is to update GUI objects which do not change g_GameAttributes but only display the attributes.
*/
registerGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.add(handler);
}
unregisterGameAttributesBatchChangeHandler(handler)
{
this.gameAttributesBatchChangeHandlers.delete(handler);
}
registerGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.add(handler);
}
unregisterGameAttributesFinalizeHandler(handler)
{
this.gameAttributesFinalizeHandlers.delete(handler);
}
registerAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.add(handler);
}
unregisterAssignPlayerHandler(handler)
{
this.assignPlayerHandlers.delete(handler);
}
registerPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.add(handler);
}
unregisterPickRandomItemsHandler(handler)
{
this.pickRandomItemsHandlers.delete(handler);
}
onLoad(initData, hotloadData)
{
if (initData && initData.map && initData.mapType)
{
- Object.defineProperty(this, "autostart", {
- "value": true,
- "writable": false,
- "configurable": false
- });
+ if (initData.autostart)
+ Object.defineProperty(this, "autostart", {
+ "value": true,
+ "writable": false,
+ "configurable": false
+ });
// TODO: Fix g_GameAttributes, g_GameAttributes.settings,
// g_GameAttributes.settings.PlayerData object references and
// copy over each attribute individually when receiving
// settings from the server or the local file.
- g_GameAttributes = {
- "mapType": initData.mapType,
- "map": initData.map
- };
+ g_GameAttributes = initData;
this.updateGameAttributes();
// Don't launchGame before all Load handlers finished
}
else
{
if (hotloadData)
g_GameAttributes = hotloadData.gameAttributes;
else if (g_IsController && this.gameSettingsFile.enabled)
g_GameAttributes = this.gameSettingsFile.loadFile();
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
}
onClose()
{
if (!this.autostart)
this.gameSettingsFile.saveFile();
}
onGetHotloadData(object)
{
object.gameAttributes = g_GameAttributes;
}
onGamesetupMessage(message)
{
if (!message.data)
return;
g_GameAttributes = message.data;
this.updateGameAttributes();
}
/**
* This is to be called whenever g_GameAttributes has been changed except on gameAttributes finalization.
*/
updateGameAttributes()
{
if (this.depth == 0)
Engine.ProfileStart("updateGameAttributes");
if (this.depth >= this.MaxDepth)
{
error("Infinite loop: " + new Error().stack);
Engine.ProfileStop();
return;
}
++this.depth;
// Basic sanitization
{
if (!g_GameAttributes.settings)
g_GameAttributes.settings = {};
if (!g_GameAttributes.settings.PlayerData)
g_GameAttributes.settings.PlayerData = new Array(this.DefaultPlayerCount);
for (let i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
if (!g_GameAttributes.settings.PlayerData[i])
g_GameAttributes.settings.PlayerData[i] = {};
}
// Map change handlers are triggered first, so that GameSettingControls can update their
// gameAttributes model prior to applying that model in their gameAttributesChangeHandler.
if (g_GameAttributes.map && this.previousMap != g_GameAttributes.map && g_GameAttributes.mapType)
{
this.previousMap = g_GameAttributes.map;
// Use a try..catch to avoid completely failing in case of an error
// as this prevents even going back to the main menu.
try
{
let mapData = this.mapCache.getMapData(g_GameAttributes.mapType, g_GameAttributes.map);
for (let handler of this.mapChangeHandlers)
handler(mapData);
} catch(err) {
// Report the error regardless so that the underlying bug gets fixed.
error(err);
error(err.stack);
}
}
for (let handler of this.gameAttributesChangeHandlers)
handler();
--this.depth;
if (this.depth == 0)
{
for (let handler of this.gameAttributesBatchChangeHandlers)
handler();
Engine.ProfileStop();
}
}
/**
* This function is to be called when a GUI control has initiated a value change.
*
* To avoid an infinite loop, do not call this function when a game setup message was
* received and the data had only been modified deterministically.
*
* This is run on a timer to avoid flooding the network with messages,
* e.g. when modifying a slider.
*/
setNetworkGameAttributes()
{
if (g_IsNetworked && this.timer === undefined)
this.timer = setTimeout(this.setNetworkGameAttributesImmediately.bind(this), this.Timeout);
}
setNetworkGameAttributesImmediately()
{
delete this.timer;
if (g_IsNetworked)
Engine.SetNetworkGameAttributes(g_GameAttributes);
}
getPlayerData(gameAttributes, playerIndex)
{
return gameAttributes &&
gameAttributes.settings &&
gameAttributes.settings.PlayerData &&
gameAttributes.settings.PlayerData[playerIndex] || undefined;
}
assignPlayer(sourcePlayerIndex, playerIndex)
{
if (playerIndex == -1)
return;
let target = this.getPlayerData(g_GameAttributes, playerIndex);
let source = this.getPlayerData(g_GameAttributes, sourcePlayerIndex);
for (let handler of this.assignPlayerHandlers)
handler(source, target);
this.updateGameAttributes();
this.setNetworkGameAttributes();
}
/**
* This function is called everytime a random setting selection was resolved,
* so that subsequent random settings are triggered too,
* for example picking a random biome after picking a random map.
*/
pickRandomItems()
{
for (let handler of this.pickRandomItemsHandlers)
handler();
}
onLaunchGame()
{
if (!this.autostart)
this.gameSettingsFile.saveFile();
this.pickRandomItems();
for (let handler of this.gameAttributesFinalizeHandlers)
handler();
this.setNetworkGameAttributesImmediately();
}
}
GameSettingsControl.prototype.MaxDepth = 512;
/**
* Wait (at most) this many milliseconds before sending network messages.
*/
GameSettingsControl.prototype.Timeout = 400;
/**
* This number is used when selecting the random map type, which doesn't provide PlayerData.
*/
GameSettingsControl.prototype.DefaultPlayerCount = 4;
Index: ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/loadgame/SavegameList.js (revision 24979)
@@ -1,177 +1,200 @@
/**
* This class obtains the list of savegames from the engine,
* builds the list dependent on selected filters and sorting order.
*
* If the selected savegame changes, class instances that subscribed via
* registerSelectionChangeHandler will have their onSelectionChange function
* called with the relevant savegame data.
*/
class SavegameList
{
- constructor()
+ constructor(campaignRun)
{
this.savedGamesMetadata = [];
this.selectionChangeHandlers = [];
+ // If not null, only show games for the following campaign run
+ // (campaign save-games are not shown by default).
+ // Campaign games are saved in the same folder as regular ones,
+ // as there is no strong reason to do otherwise (since games from different runs
+ // need to be hidden from one another anyways, we need code to handle it).
+ this.campaignRun = campaignRun;
+
this.gameSelection = Engine.GetGUIObjectByName("gameSelection");
this.gameSelectionFeedback = Engine.GetGUIObjectByName("gameSelectionFeedback");
this.confirmButton = Engine.GetGUIObjectByName("confirmButton");
this.compatibilityFilter = Engine.GetGUIObjectByName("compatibilityFilter");
this.compatibilityFilter.onPress = () => { this.updateSavegameList(); };
this.initSavegameList();
}
initSavegameList()
{
let engineInfo = Engine.GetEngineInfo();
this.gameSelection.onSelectionColumnChange = () => { this.updateSavegameList(); };
this.gameSelection.onMouseLeftDoubleClickItem = () => { this.confirmButton.onPress(); };
this.gameSelection.onSelectionChange = () => {
let gameId = this.gameSelection.list_data[this.gameSelection.selected];
let metadata = this.savedGamesMetadata[this.gameSelection.selected];
let label = this.generateSavegameLabel(metadata, engineInfo);
for (let handler of this.selectionChangeHandlers)
handler.onSelectionChange(gameId, metadata, label);
};
this.updateSavegameList();
}
registerSelectionChangeHandler(selectionChangeHandler)
{
this.selectionChangeHandlers.push(selectionChangeHandler);
}
onSavegameListChange()
{
this.updateSavegameList();
// Allow subscribers (delete button) to update their press function in case
// the list items changed but the selected index remained the same.
this.gameSelection.onSelectionChange();
}
selectFirst()
{
if (this.gameSelection.list.length)
this.gameSelection.selected = 0;
}
updateSavegameList()
{
let savedGames = Engine.GetSavedGames();
// Get current game version and loaded mods
let engineInfo = Engine.GetEngineInfo();
if (this.compatibilityFilter.checked)
- savedGames = savedGames.filter(game => this.isCompatibleSavegame(game.metadata, engineInfo));
+ savedGames = savedGames.filter(game => {
+ return this.isCompatibleSavegame(game.metadata, engineInfo) &&
+ this.campaignFilter(game.metadata, this.campaignRun);
+ });
+ else if (this.campaignRun)
+ savedGames = savedGames.filter(game => this.campaignFilter(game.metadata, this.campaignRun));
+
this.gameSelection.enabled = !!savedGames.length;
this.gameSelectionFeedback.hidden = !!savedGames.length;
let selectedGameId = this.gameSelection.list_data[this.gameSelection.selected];
// Save metadata for the detailed view
this.savedGamesMetadata = savedGames.map(game => {
game.metadata.id = game.id;
return game.metadata;
});
let sortKey = this.gameSelection.selected_column;
let sortOrder = this.gameSelection.selected_column_order;
this.savedGamesMetadata = this.savedGamesMetadata.sort((a, b) => {
let cmpA, cmpB;
switch (sortKey)
{
case 'date':
cmpA = +a.time;
cmpB = +b.time;
break;
case 'mapName':
cmpA = translate(a.initAttributes.settings.Name);
cmpB = translate(b.initAttributes.settings.Name);
break;
case 'mapType':
cmpA = translateMapType(a.initAttributes.mapType);
cmpB = translateMapType(b.initAttributes.mapType);
break;
case 'description':
cmpA = a.description;
cmpB = b.description;
break;
}
if (cmpA < cmpB)
return -sortOrder;
else if (cmpA > cmpB)
return +sortOrder;
return 0;
});
let list = this.savedGamesMetadata.map(metadata => {
- let isCompatible = this.isCompatibleSavegame(metadata, engineInfo);
+ let isCompatible = this.isCompatibleSavegame(metadata, engineInfo) &&
+ this.campaignFilter(metadata, this.campaignRun);
return {
"date": this.generateSavegameDateString(metadata, engineInfo),
"mapName": compatibilityColor(translate(metadata.initAttributes.settings.Name), isCompatible),
"mapType": compatibilityColor(translateMapType(metadata.initAttributes.mapType), isCompatible),
"description": compatibilityColor(metadata.description, isCompatible)
};
});
if (list.length)
list = prepareForDropdown(list);
this.gameSelection.list_date = list.date || [];
this.gameSelection.list_mapName = list.mapName || [];
this.gameSelection.list_mapType = list.mapType || [];
this.gameSelection.list_description = list.description || [];
// Change these last, otherwise crash
this.gameSelection.list = this.savedGamesMetadata.map(metadata => 0);
this.gameSelection.list_data = this.savedGamesMetadata.map(metadata => metadata.id);
// Restore selection if the selected savegame still exists.
// If the last savegame was deleted, or if it was hidden by the compatibility filter, select the new last item.
let selectedGameIndex = this.savedGamesMetadata.findIndex(metadata => metadata.id == selectedGameId);
if (selectedGameIndex != -1)
this.gameSelection.selected = selectedGameIndex;
else if (this.gameSelection.selected >= this.savedGamesMetadata.length)
this.gameSelection.selected = this.savedGamesMetadata.length - 1;
}
+ campaignFilter(metadata, campaignRun)
+ {
+ if (!campaignRun)
+ return !metadata.initAttributes.campaignData;
+ if (metadata.initAttributes.campaignData)
+ return metadata.initAttributes.campaignData.run == campaignRun;
+ return false;
+ }
+
isCompatibleSavegame(metadata, engineInfo)
{
return engineInfo &&
metadata.engine_version &&
metadata.engine_version == engineInfo.engine_version &&
hasSameMods(metadata.mods, engineInfo.mods);
}
generateSavegameDateString(metadata, engineInfo)
{
return compatibilityColor(
Engine.FormatMillisecondsIntoDateStringLocal(metadata.time * 1000, translate("yyyy-MM-dd HH:mm:ss")),
this.isCompatibleSavegame(metadata, engineInfo));
}
generateSavegameLabel(metadata, engineInfo)
{
if (!metadata)
return undefined;
return sprintf(
metadata.description ?
translate("%(dateString)s %(map)s - %(description)s") :
translate("%(dateString)s %(map)s"),
{
"dateString": this.generateSavegameDateString(metadata, engineInfo),
"map": metadata.initAttributes.map,
"description": metadata.description || ""
});
}
}
Index: ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/MainMenuItemHandler.js (revision 24979)
@@ -1,177 +1,177 @@
/**
* This class sets up the main menu buttons, animates submenu that opens when
* clicking on category buttons, assigns the defined actions and hotkeys to every button.
*/
class MainMenuItemHandler
{
constructor(menuItems)
{
this.menuItems = menuItems;
this.lastTickTime = Date.now();
this.lastOpenItem = undefined;
this.mainMenu = Engine.GetGUIObjectByName("mainMenu");
this.mainMenuButtons = Engine.GetGUIObjectByName("mainMenuButtons");
this.submenu = Engine.GetGUIObjectByName("submenu");
this.submenuButtons = Engine.GetGUIObjectByName("submenuButtons");
this.MainMenuPanelRightBorderTop = Engine.GetGUIObjectByName("MainMenuPanelRightBorderTop");
this.MainMenuPanelRightBorderBottom = Engine.GetGUIObjectByName("MainMenuPanelRightBorderBottom");
this.setupMenuButtons(this.mainMenuButtons.children, this.menuItems);
this.setupHotkeys(this.menuItems);
Engine.GetGUIObjectByName("closeMenuButton").onPress = this.closeSubmenu.bind(this);
}
setupMenuButtons(buttons, menuItems)
{
buttons.forEach((button, i) => {
let item = menuItems[i];
button.hidden = !item;
if (button.hidden)
return;
button.size = new GUISize(
0, (this.ButtonHeight + this.Margin) * i,
0, (this.ButtonHeight + this.Margin) * i + this.ButtonHeight,
0, 0, 100, 0);
button.caption = item.caption;
button.tooltip = item.tooltip;
- button.enabled = item.enabled === undefined || item.enabled;
+ button.enabled = item.enabled === undefined || item.enabled();
button.onPress = this.pressButton.bind(this, item, i);
button.hidden = false;
});
if (buttons.length < menuItems.length)
error("GUI page has space for " + buttons.length + " menu buttons, but " + menuItems.length + " items are provided!");
}
/**
* Expand selected submenu, or collapse if it already is expanded.
*/
pressButton(item, i)
{
if (this.submenu.hidden)
{
this.performButtonAction(item, i);
}
else
{
this.closeSubmenu();
if (this.lastOpenItem && this.lastOpenItem != item)
this.performButtonAction(item, i);
else
this.lastOpenItem = undefined;
}
}
/**
* Expand submenu or perform action specified by the button object.
*/
performButtonAction(item, i)
{
this.lastOpenItem = item;
if (item.onPress)
item.onPress();
else
this.openSubmenu(i);
}
setupHotkeys(menuItems)
{
for (let i in menuItems)
{
let item = menuItems[i];
if (item.onPress && item.hotkey)
Engine.SetGlobalHotkey(item.hotkey, "Press", () => {
this.closeSubmenu();
item.onPress();
});
if (item.submenu)
this.setupHotkeys(item.submenu);
}
}
openSubmenu(i)
{
this.setupMenuButtons(this.submenuButtons.children, this.menuItems[i].submenu);
let top = this.mainMenuButtons.size.top + this.mainMenuButtons.children[i].size.top;
this.submenu.size = new GUISize(
this.submenu.size.left, top - this.Margin,
this.submenu.size.right, top + (this.ButtonHeight + this.Margin) * this.menuItems[i].submenu.length);
this.submenu.hidden = false;
{
let size = this.MainMenuPanelRightBorderTop.size;
size.bottom = this.submenu.size.top + this.Margin;
size.rbottom = 0;
this.MainMenuPanelRightBorderTop.size = size;
}
{
let size = this.MainMenuPanelRightBorderBottom.size;
size.top = this.submenu.size.bottom;
this.MainMenuPanelRightBorderBottom.size = size;
}
// Start animation
this.lastTickTime = Date.now();
this.mainMenu.onTick = this.onTick.bind(this);
}
closeSubmenu()
{
this.submenu.hidden = true;
this.submenu.size = this.mainMenu.size;
let size = this.MainMenuPanelRightBorderTop.size;
size.top = 0;
size.bottom = 0;
size.rbottom = 100;
this.MainMenuPanelRightBorderTop.size = size;
}
onTick()
{
let now = Date.now();
if (now == this.lastTickTime)
return;
let maxOffset = this.mainMenu.size.right - this.submenu.size.left;
let offset = Math.min(this.MenuSpeed * (now - this.lastTickTime), maxOffset);
this.lastTickTime = now;
if (this.submenu.hidden || !offset)
{
delete this.mainMenu.onTick;
return;
}
let size = this.submenu.size;
size.left += offset;
size.right += offset;
this.submenu.size = size;
}
}
/**
* Vertical size per button.
*/
MainMenuItemHandler.prototype.ButtonHeight = 28;
/**
* Distance between consecutive buttons.
*/
MainMenuItemHandler.prototype.Margin = 4;
/**
* Collapse / expansion speed in pixels per milliseconds used when animating the button menu size.
*/
MainMenuItemHandler.prototype.MenuSpeed = 1.2;
Index: ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/pregame/mainmenu.xml (revision 24979)
@@ -1,15 +1,16 @@
+
Index: ps/trunk/binaries/data/mods/public/gui/session/campaigns/CampaignSession.js
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/campaigns/CampaignSession.js (nonexistent)
+++ ps/trunk/binaries/data/mods/public/gui/session/campaigns/CampaignSession.js (revision 24979)
@@ -0,0 +1,42 @@
+class CampaignSession
+{
+ constructor(data)
+ {
+ this.run = new CampaignRun(data.run).load();
+ this.levelID = data.levelID;
+ registerPlayersFinishedHandler(this.onFinish.bind(this));
+ this.endGameData = {
+ "levelID": data.levelID,
+ "won": false,
+ "custom": {}
+ };
+ }
+
+ onFinish(players, won)
+ {
+ let playerID = Engine.GetPlayerID();
+ if (players.indexOf(playerID) === -1)
+ return;
+
+ this.endGameData.custom = Engine.GuiInterfaceCall("GetCampaignGameEndData", {
+ "player": playerID
+ });
+ this.endGameData.won = won;
+
+ // Run the endgame script.
+ Engine.PushGuiPage(this.getEndGame(), this.endGameData);
+ Engine.PopGuiPage();
+ }
+
+ getMenu()
+ {
+ return this.run.getMenuPath();
+ }
+
+ getEndGame()
+ {
+ return this.run.getEndGamePath();
+ }
+}
+
+var g_CampaignSession;
Property changes on: ps/trunk/binaries/data/mods/public/gui/session/campaigns/CampaignSession.js
___________________________________________________________________
Added: svn:eol-style
## -0,0 +1 ##
+native
\ No newline at end of property
Index: ps/trunk/binaries/data/mods/public/gui/session/session.xml
===================================================================
--- ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 24978)
+++ ps/trunk/binaries/data/mods/public/gui/session/session.xml (revision 24979)
@@ -1,117 +1,119 @@
+
+
onTick();
restoreSavedGameData(arguments[0]);
onSimulationUpdate();
Index: ps/trunk/binaries/data/mods/public/l10n/messages.json
===================================================================
--- ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24978)
+++ ps/trunk/binaries/data/mods/public/l10n/messages.json (revision 24979)
@@ -1,821 +1,879 @@
[
{
"output": "public-civilizations.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/civs/**.json"
],
"options": {
"keywords": [
"Name",
"Description",
"History",
"Special",
"AINames"
]
}
}
]
},
{
"output": "public-gui-ingame.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/session/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/session/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
}
]
},
{
"output": "public-gui-gamesetup.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/gamesetup/**.js",
"gui/gamesetup_mp/**.js",
"gui/loading/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/gamesetup/**.xml",
"gui/gamesetup_mp/**.xml",
"gui/loading/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/text/quotes.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-lobby.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/lobby/**.js",
"gui/prelobby/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/lobby/**.xml",
"gui/prelobby/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/prelobby/common/terms/*.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-manual.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"gui/manual/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"gui/manual/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "txt",
"filemasks": [
"gui/manual/intro.txt"
],
"options": {
}
}
]
},
{
"output": "public-gui-userreport.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "txt",
"filemasks": [
"gui/userreport/**.txt"
],
"options": {
}
}
]
},
{
+ "output": "public-gui-campaigns.pot",
+ "inputRoot": "..",
+ "project": "0 A.D. — Empires Ascendant",
+ "copyrightHolder": "Wildfire Games",
+ "rules": [
+ {
+ "extractor": "javascript",
+ "filemasks": [
+ "gui/campaigns/**.js",
+ "gui/common/campaigns/**.js"
+ ],
+ "options": {
+ "format": "javascript-format",
+ "keywords": {
+ "translate": [1],
+ "translatePlural": [1, 2],
+ "translateWithContext": [[1], 2],
+ "translatePluralWithContext": [[1], 2, 3],
+ "markForTranslation": [1],
+ "markForTranslationWithContext": [[1], 2],
+ "markForPluralTranslation": [1, 2]
+ },
+ "commentTags": [
+ "Translation:"
+ ]
+ }
+ },
+ {
+ "extractor": "xml",
+ "filemasks": [
+ "gui/campaigns/**.xml",
+ "gui/common/campaigns/**.xml"
+ ],
+ "options": {
+ "keywords": {
+ "translatableAttribute": {
+ "locationAttributes": ["id"]
+ },
+ "translate": {}
+ }
+ }
+ },
+ {
+ "extractor": "json",
+ "filemasks": [
+ "campaigns/**.json"
+ ],
+ "options": {
+ "keywords": [
+ "Name",
+ "Description"
+ ],
+ "context": "Campaign Template"
+ }
+ }
+ ]
+ },
+ {
"output": "public-gui-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"globalscripts/**.js",
"gui/common/**.js",
"gui/credits/**.js",
"gui/hotkeys/**.js",
"gui/loadgame/**.js",
"gui/locale/**.js",
"gui/maps/**.js",
"gui/options/**.js",
"gui/pregame/**.js",
"gui/reference/**.js",
"gui/replaymenu/**.js",
"gui/splashscreen/**.js",
"gui/summary/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"dennis-ignore:",
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"globalscripts/**.xml",
"gui/common/**.xml",
"gui/credits/**.xml",
"gui/hotkeys/**.xml",
"gui/loadgame/**.xml",
"gui/locale/**.xml",
"gui/maps/**.xml",
"gui/options/**.xml",
"gui/pregame/**.xml",
"gui/reference/**.xml",
"gui/replaymenu/**.xml",
"gui/splashscreen/**.xml",
"gui/summary/**.xml"
],
"options": {
"keywords": {
"translatableAttribute": {
"locationAttributes": ["id"]
},
"translate": {}
}
}
},
{
"extractor": "json",
"filemasks": [
"gui/credits/texts/**.json"
],
"options": {
"keywords": [
"Title",
"Subtitle"
]
}
},
{
"extractor": "json",
"filemasks": [
"gui/options/**.json"
],
"options": {
"keywords": [
"label",
"tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"description"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used at the beginning of a sentence or as a single-word sentence."
],
"context": "firstWord"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/resources/**.json"
],
"options": {
"keywords": [
"name",
"subtypes"
],
"comments": [
"Translation: Word as used in the middle of a sentence (which may require using lowercase for your language)."
],
"context": "withinSentence"
}
},
{
"extractor": "txt",
"filemasks": [
"gui/gamesetup/**.txt",
"gui/splashscreen/splashscreen.txt",
"gui/text/tips/**.txt"
],
"options": {
}
}
]
},
{
"output": "public-templates-units.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
],
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-buildings.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": [
"simulation/templates/template_structure_*.xml",
"simulation/templates/structures/**.xml"
],
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"StatusName": {
"customContext": "status effect"
},
"ApplierTooltip": {
"customContext": "status effect"
},
"ReceiverTooltip": {
"customContext": "status effect"
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
}
]
},
{
"output": "public-templates-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "xml",
"filemasks": {
"includeMasks": [
"simulation/templates/**.xml"
],
"excludeMasks": [
"simulation/templates/structures/**.xml",
"simulation/templates/template_structure_*.xml",
"simulation/templates/template_unit_*.xml",
"simulation/templates/units/**.xml"
]
},
"options": {
"keywords": {
"AttackName": {
"customContext": "Name of an attack, usually the weapon."
},
"GenericName": {},
"SpecificName": {},
"History": {},
"VisibleClasses": {
"splitOnWhitespace": true
},
"Tooltip": {},
"DisabledTooltip": {},
"FormationName": {},
"FromClass": {},
"Rank": {
"tagAsContext": true
}
}
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/template_helpers/damage_types/*.json"
],
"options": {
"keywords": [
"name",
"description"
],
"context": "damage type"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/status_effects/*.json"
],
"options": {
"keywords": [
"statusName",
"applierTooltip",
"receiverTooltip"
],
"context": "status effect"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/attack_effects/*.json"
],
"options": {
"keywords": [
"name",
"description"
],
"context": "effect caused by an attack"
}
}
]
},
{
"output": "public-simulation-auras.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/auras/**.json"
],
"options": {
"keywords": [
"auraName",
"auraDescription"
]
}
}
]
},
{
"output": "public-simulation-technologies.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": [
"simulation/data/technologies/**.json"
],
"options": {
"keywords": [
"specificName",
"genericName",
"description",
"tooltip",
"requirementsTooltip"
]
}
}
]
},
{
"output": "public-simulation-other.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"simulation/ai/**.js",
"simulation/components/**.js",
"simulation/helpers/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"translate": [1],
"translatePlural": [1, 2],
"translateWithContext": [[1], 2],
"translatePluralWithContext": [[1], 2, 3],
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/player_defaults.json"
],
"options": {
"keywords": [
"Name"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/game_speeds.json"
],
"options": {
"keywords": ["Title"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/victory_conditions/*.json"
],
"options": {
"keywords": ["Title", "Description"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/starting_resources.json"
],
"options": {
"keywords": ["Title"],
"context": "startingResources"
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/trigger_difficulties.json"
],
"options": {
"keywords": ["Title", "Tooltip"]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/data/settings/map_sizes.json"
],
"options": {
"keywords": [
"Name",
"Tooltip"
]
}
},
{
"extractor": "json",
"filemasks": [
"simulation/ai/**.json"
],
"options": {
"keywords": [
"name",
"description"
]
}
}
]
},
{
"output": "public-maps.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "json",
"filemasks": {
"includeMasks": [
"maps/random/**.json"
],
"excludeMasks": [
"maps/random/rmbiome/**.json"
]
},
"options": {
"keywords": [
"Name",
"Description"
]
}
},
{
"extractor": "javascript",
"filemasks": [
"maps/scenarios/**.js",
"maps/skirmishes/**.js",
"maps/random/**.js",
"maps/scripts/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/scenarios/**.xml",
"maps/skirmishes/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
},
{
"extractor": "json",
"filemasks": [
"maps/random/rmbiome/**.json"
],
"options": {
"keywords": ["Description"],
"context": "biome definition"
}
}
]
},
{
"output": "public-tutorials.pot",
"inputRoot": "..",
"project": "0 A.D. — Empires Ascendant",
"copyrightHolder": "Wildfire Games",
"rules": [
{
"extractor": "javascript",
"filemasks": [
"maps/tutorials/**.js"
],
"options": {
"format": "javascript-format",
"keywords": {
"markForTranslation": [1],
"markForTranslationWithContext": [[1], 2],
"markForPluralTranslation": [1, 2]
},
"commentTags": [
"Translation:"
]
}
},
{
"extractor": "xml",
"filemasks": [
"maps/tutorials/**.xml"
],
"options": {
"keywords": {
"ScriptSettings": {
"extractJson": {
"keywords": [
"Name",
"Description"
]
}
}
}
}
}
]
}
]
Index: ps/trunk/source/ps/scripting/JSInterface_VFS.h
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 24978)
+++ ps/trunk/source/ps/scripting/JSInterface_VFS.h (revision 24979)
@@ -1,58 +1,62 @@
-/* Copyright (C) 2018 Wildfire Games.
+/* Copyright (C) 2021 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_JSI_VFS
#define INCLUDED_JSI_VFS
#include "scriptinterface/ScriptInterface.h"
namespace JSI_VFS
{
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
JS::Value BuildDirEntList(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse);
// Return true iff the file exists
bool FileExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filename);
// Return time [seconds since 1970] of the last modification to the specified file.
double GetFileMTime(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename);
// Return current size of file.
unsigned int GetFileSize(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename);
// Return file contents in a string.
JS::Value ReadFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename);
// Return file contents as an array of lines.
JS::Value ReadFileLines(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename);
// Return file contents parsed as a JS Object
JS::Value ReadJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath);
// Save given JS Object to a JSON file
void WriteJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath, JS::HandleValue val1);
+ // Delete the given campaign save.
+ // This is limited to campaign save to avoid mods deleting the wrong file.
+ bool DeleteCampaignSave(ScriptInterface::CmptPrivate* pCmptPrivate, const CStrW& filePath);
+
// Tests whether the current script context is allowed to read from the given directory
bool PathRestrictionMet(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath);
void RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface);
void RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface);
void RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface);
}
#endif // INCLUDED_JSI_VFS
Index: ps/trunk/binaries/data/mods/public/l10n/.tx/config
===================================================================
--- ps/trunk/binaries/data/mods/public/l10n/.tx/config (revision 24978)
+++ ps/trunk/binaries/data/mods/public/l10n/.tx/config (revision 24979)
@@ -1,78 +1,83 @@
[main]
host = https://www.transifex.com
[0ad.public-civilizations]
file_filter = .public-civilizations.po
source_file = public-civilizations.pot
source_lang = en
[0ad.public-gui-gamesetup]
file_filter = .public-gui-gamesetup.po
source_file = public-gui-gamesetup.pot
source_lang = en
[0ad.public-gui-ingame]
file_filter = .public-gui-ingame.po
source_file = public-gui-ingame.pot
source_lang = en
[0ad.public-gui-lobby]
file_filter = .public-gui-lobby.po
source_file = public-gui-lobby.pot
source_lang = en
[0ad.public-gui-manual]
file_filter = .public-gui-manual.po
source_file = public-gui-manual.pot
source_lang = en
[0ad.public-gui-other]
file_filter = .public-gui-other.po
source_file = public-gui-other.pot
source_lang = en
+[0ad.public-gui-campaigns]
+file_filter = .public-gui-campaigns.po
+source_file = public-gui-campaigns.pot
+source_lang = en
+
[0ad.public-gui-userreport]
file_filter = .public-gui-userreport.po
source_file = public-gui-userreport.pot
source_lang = en
[0ad.public-maps]
file_filter = .public-maps.po
source_file = public-maps.pot
source_lang = en
[0ad.public-simulation-auras]
file_filter = .public-simulation-auras.po
source_file = public-simulation-auras.pot
source_lang = en
[0ad.public-simulation-other]
file_filter = .public-simulation-other.po
source_file = public-simulation-other.pot
source_lang = en
[0ad.public-simulation-technologies]
file_filter = .public-simulation-technologies.po
source_file = public-simulation-technologies.pot
source_lang = en
[0ad.public-templates-buildings]
file_filter = .public-templates-buildings.po
source_file = public-templates-buildings.pot
source_lang = en
[0ad.public-templates-other]
file_filter = .public-templates-other.po
source_file = public-templates-other.pot
source_lang = en
[0ad.public-templates-units]
file_filter = .public-templates-units.po
source_file = public-templates-units.pot
source_lang = en
[0ad.public-tutorials]
file_filter = .public-tutorials.po
source_file = public-tutorials.pot
source_lang = en
Index: ps/trunk/source/ps/scripting/JSInterface_VFS.cpp
===================================================================
--- ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24978)
+++ ps/trunk/source/ps/scripting/JSInterface_VFS.cpp (revision 24979)
@@ -1,276 +1,289 @@
-/* Copyright (C) 2020 Wildfire Games.
+/* Copyright (C) 2021 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 "JSInterface_VFS.h"
#include "lib/file/vfs/vfs_util.h"
#include "ps/CLogger.h"
#include "ps/CStr.h"
#include "ps/Filesystem.h"
#include "scriptinterface/ScriptExtraHeaders.h"
#include "scriptinterface/ScriptInterface.h"
#include
// Only allow engine compartments to read files they may be concerned about.
#define PathRestriction_GUI {L""}
#define PathRestriction_Simulation {L"simulation/"}
#define PathRestriction_Maps {L"simulation/", L"maps/"}
// shared error handling code
#define JS_CHECK_FILE_ERR(err)\
/* this is liable to happen often, so don't complain */\
if (err == ERR::VFS_FILE_NOT_FOUND)\
{\
return 0; \
}\
/* unknown failure. We output an error message. */\
else if (err < 0)\
LOGERROR("Unknown failure in VFS %i", err );
/* else: success */
// state held across multiple BuildDirEntListCB calls; init by BuildDirEntList.
struct BuildDirEntListState
{
ScriptInterface* pScriptInterface;
JS::PersistentRootedObject filename_array;
int cur_idx;
BuildDirEntListState(ScriptInterface* scriptInterface)
: pScriptInterface(scriptInterface),
filename_array(scriptInterface->GetGeneralJSContext()),
cur_idx(0)
{
ScriptRequest rq(pScriptInterface);
filename_array = JS::NewArrayObject(rq.cx, JS::HandleValueArray::empty());
}
};
// called for each matching directory entry; add its full pathname to array.
static Status BuildDirEntListCB(const VfsPath& pathname, const CFileInfo& UNUSED(fileINfo), uintptr_t cbData)
{
BuildDirEntListState* s = (BuildDirEntListState*)cbData;
ScriptRequest rq(s->pScriptInterface);
JS::RootedObject filenameArrayObj(rq.cx, s->filename_array);
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStrW(pathname.string()) );
JS_SetElement(rq.cx, filenameArrayObj, s->cur_idx++, val);
return INFO::OK;
}
// Return an array of pathname strings, one for each matching entry in the
// specified directory.
// filter_string: default "" matches everything; otherwise, see vfs_next_dirent.
// recurse: should subdirectories be included in the search? default false.
JS::Value JSI_VFS::BuildDirEntList(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const std::wstring& path, const std::wstring& filterStr, bool recurse)
{
if (!PathRestrictionMet(pCmptPrivate, validPaths, path))
return JS::NullValue();
// convert to const wchar_t*; if there's no filter, pass 0 for speed
// (interpreted as: "accept all files without comparing").
const wchar_t* filter = 0;
if (!filterStr.empty())
filter = filterStr.c_str();
int flags = recurse ? vfs::DIR_RECURSIVE : 0;
// build array in the callback function
BuildDirEntListState state(pCmptPrivate->pScriptInterface);
vfs::ForEachFile(g_VFS, path, BuildDirEntListCB, (uintptr_t)&state, filter, flags);
return JS::ObjectValue(*state.filename_array);
}
// Return true iff the file exits
bool JSI_VFS::FileExists(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filename)
{
return PathRestrictionMet(pCmptPrivate, validPaths, filename) && g_VFS->GetFileInfo(filename, 0) == INFO::OK;
}
// Return time [seconds since 1970] of the last modification to the specified file.
double JSI_VFS::GetFileMTime(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (double)fileInfo.MTime();
}
// Return current size of file.
unsigned int JSI_VFS::GetFileSize(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const std::wstring& filename)
{
CFileInfo fileInfo;
Status err = g_VFS->GetFileInfo(filename, &fileInfo);
JS_CHECK_FILE_ERR(err);
return (unsigned int)fileInfo.Size();
}
// Return file contents in a string. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// Decode as UTF-8
ScriptRequest rq(pCmptPrivate->pScriptInterface);
JS::RootedValue ret(rq.cx);
ScriptInterface::ToJSVal(rq, &ret, contents.FromUTF8());
return ret;
}
// Return file contents as an array of lines. Assume file is UTF-8 encoded text.
JS::Value JSI_VFS::ReadFileLines(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filename)
{
CVFSFile file;
if (file.Load(g_VFS, filename) != PSRETURN_OK)
return JS::NullValue();
CStr contents = file.DecodeUTF8(); // assume it's UTF-8
// Fix CRLF line endings. (This function will only ever be used on text files.)
contents.Replace("\r\n", "\n");
// split into array of strings (one per line)
std::stringstream ss(contents);
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
JS::RootedValue line_array(rq.cx);
ScriptInterface::CreateArray(rq, &line_array);
std::string line;
int cur_line = 0;
while (std::getline(ss, line))
{
// Decode each line as UTF-8
JS::RootedValue val(rq.cx);
ScriptInterface::ToJSVal(rq, &val, CStr(line).FromUTF8());
scriptInterface.SetPropertyInt(line_array, cur_line++, val);
}
return line_array;
}
JS::Value JSI_VFS::ReadJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath)
{
if (!PathRestrictionMet(pCmptPrivate, validPaths, filePath))
return JS::NullValue();
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
JS::RootedValue out(rq.cx);
scriptInterface.ReadJSONFile(filePath, &out);
return out;
}
void JSI_VFS::WriteJSONFile(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath, JS::HandleValue val1)
{
const ScriptInterface& scriptInterface = *pCmptPrivate->pScriptInterface;
ScriptRequest rq(scriptInterface);
// TODO: This is a workaround because we need to pass a MutableHandle to StringifyJSON.
JS::RootedValue val(rq.cx, val1);
std::string str(scriptInterface.StringifyJSON(&val, false));
VfsPath path(filePath);
WriteBuffer buf;
buf.Append(str.c_str(), str.length());
g_VFS->CreateFile(path, buf.Data(), buf.Size());
}
+bool JSI_VFS::DeleteCampaignSave(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate), const CStrW& filePath)
+{
+ OsPath realPath;
+ if (filePath.Left(16) != L"saves/campaigns/" || filePath.Right(12) != L".0adcampaign")
+ return false;
+
+ return VfsFileExists(filePath) &&
+ g_VFS->GetRealPath(filePath, realPath) == INFO::OK &&
+ g_VFS->RemoveFile(filePath) == INFO::OK &&
+ wunlink(realPath) == 0;
+}
+
bool JSI_VFS::PathRestrictionMet(ScriptInterface::CmptPrivate* pCmptPrivate, const std::vector& validPaths, const CStrW& filePath)
{
for (const CStrW& validPath : validPaths)
if (filePath.find(validPath) == 0)
return true;
CStrW allowedPaths;
for (std::size_t i = 0; i < validPaths.size(); ++i)
{
if (i != 0)
allowedPaths += L", ";
allowedPaths += L"\"" + validPaths[i] + L"\"";
}
ScriptRequest rq(pCmptPrivate->pScriptInterface);
ScriptException::Raise(rq, "This part of the engine may only read from %s!", utf8_from_wstring(allowedPaths).c_str());
return false;
}
#define VFS_ScriptFunctions(context)\
JS::Value Script_ReadJSONFile_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::ReadJSONFile(pCmptPrivate, PathRestriction_##context, filePath);\
}\
JS::Value Script_ListDirectoryFiles_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& path, const std::wstring& filterStr, bool recurse)\
{\
return JSI_VFS::BuildDirEntList(pCmptPrivate, PathRestriction_##context, path, filterStr, recurse);\
}\
bool Script_FileExists_##context(ScriptInterface::CmptPrivate* pCmptPrivate, const std::wstring& filePath)\
{\
return JSI_VFS::FileExists(pCmptPrivate, PathRestriction_##context, filePath);\
}\
VFS_ScriptFunctions(GUI);
VFS_ScriptFunctions(Simulation);
VFS_ScriptFunctions(Maps);
#undef VFS_ScriptFunctions
void JSI_VFS::RegisterScriptFunctions_GUI(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("GetFileMTime");
scriptInterface.RegisterFunction("GetFileSize");
scriptInterface.RegisterFunction("ReadFile");
scriptInterface.RegisterFunction("ReadFileLines");
scriptInterface.RegisterFunction("ReadJSONFile");
scriptInterface.RegisterFunction("WriteJSONFile");
+ scriptInterface.RegisterFunction("DeleteCampaignSave");
}
void JSI_VFS::RegisterScriptFunctions_Simulation(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
}
void JSI_VFS::RegisterScriptFunctions_Maps(const ScriptInterface& scriptInterface)
{
scriptInterface.RegisterFunction("ListDirectoryFiles");
scriptInterface.RegisterFunction("FileExists");
scriptInterface.RegisterFunction("ReadJSONFile");
}