mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-08-15 14:43:32 -07:00
GetEntityState is a performance-critical function in the GUI when a large number of units are selected. The goal of this patch is to increase the efficiency of it without modifying the returned states visible to rest of the GUI in any way (it renames a few properties of entities states or moves them around, but the information they contain and the way to access it remain the complete same) As explained the comments, certain parts (the template data) of an entity state are "predictable" and can be reused between entities with the same template. What one has to account for, however, is that values of the template data can be modified. This happens on two different levels: Firstly, player-wide modifications, which apply to all entities owned by that player. This includes stuff like bonuses from researched techs, civs bonuses, team bonuses. And secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs from status effects or auras (of other entities). So we can construct a whole entity state by first just computing the dynamic state (stuff like current hitpoints, which differs from entity to entity) and then adding the (potentially already cached) template data to it, which can be reused between entities with the same template and owning player. And then overwrite the values affected by entity-local modifications, which are usually just a few and even they can be cached and reused for entities with the same owning player, template, and entity-local modifications (identified by the "modifications ID"). This saves the effort of retrieving/computing a ton of data each turn and also saves the time it takes the engine to clone the data from the simulation to the GUI (as the return value of the GUI interface call), which previously took just as long as retrieving the entity states themselves. Since only the dynamic state is read from the components, a number of small getter methods have become unused; they are kept (for now at least) since they might be useful again in the future or for mods.
962 lines
29 KiB
JavaScript
962 lines
29 KiB
JavaScript
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_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_CinemaOverlay;
|
|
var g_DeveloperOverlay;
|
|
var g_DiplomacyColors;
|
|
var g_DiplomacyDialog;
|
|
var g_GameSpeedControl;
|
|
var g_MatchSettingsDialog;
|
|
var g_Menu;
|
|
var g_MiniMapPanel;
|
|
var g_NetworkStatusOverlay;
|
|
var g_NetworkDelayOverlay;
|
|
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;
|
|
var g_TutorialManager;
|
|
|
|
/**
|
|
* Map, player and match settings set in game setup.
|
|
*/
|
|
const g_InitAttributes = 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 = new Map();
|
|
var g_TemplateData = new Map();
|
|
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 = ["Civilian", "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;
|
|
}
|
|
|
|
/**
|
|
* Piece together the entire entity state, either from g_TemplateData or the data provided (one of which
|
|
* should always be possible) and then cache it in g_EntityStates.
|
|
* Intended for internal use only.
|
|
*/
|
|
function _processNewEntityState(data, entId)
|
|
{
|
|
if (!data?.dynamicState)
|
|
{
|
|
g_EntityStates.set(entId, null);
|
|
return null;
|
|
}
|
|
|
|
// Certain parts (the template data) of an entity state are "predictable" and can therefore be reused between
|
|
// entities with the same template. What one has to account for, however, is that values of the template data
|
|
// can be modified. This happens on two different levels:
|
|
// - Firstly, player-global modifications, which apply to all entities owned by that player. This includes stuff
|
|
// like bonuses from researched techs, civs bonuses, team bonuses.
|
|
// - Secondly, entity-local modifications, which apply to individual entities. This includes buffs or debuffs
|
|
// from status effects or auras (of other entities).
|
|
//
|
|
// To take advantage of this, the whole entity state consists of three parts that we need to combine here:
|
|
// 1. The template data of the entity's template, which only has the player-global modifications applied. It is
|
|
// the same for all entities with the same template and owner, so it cached and reused when possible.
|
|
// 2. The modified template data of the entity, which can contain a subset of the template data's values,
|
|
// overwriting them. It has both the player-global and entity-local modifications applied, but only contains the
|
|
// values that actually differ from its template data. It is therefore the same for all entities with the same
|
|
// owner, template, and modifications ID, so it cached and reused when possible.
|
|
// 3. the dynamic state, which differs from entity to entity and therefore isn't cached.
|
|
|
|
const state = data.dynamicState;
|
|
|
|
// This is the only two-level-deep value of the dynamic state supposed to overwrite the corresponding value in the
|
|
// (modified) template data, so we have to treat it explicitly here, which is quite ugly.
|
|
const elevationAdaptedRange = state.attack?.Ranged?.elevationAdaptedRange;
|
|
|
|
const playerCache = g_TemplateData.get(state.player);
|
|
let templateData = data.templateData;
|
|
if (templateData)
|
|
{
|
|
translateObjectKeys(templateData, ["specific", "generic", "tooltip"]);
|
|
playerCache.set(state.templateName, deepfreeze(templateData));
|
|
}
|
|
else
|
|
templateData = playerCache.get(state.templateName);
|
|
|
|
let modifiedTemplateData;
|
|
if (data.modificationsID)
|
|
{
|
|
const cacheKey = `${state.templateName}: ${data.modificationsID}`;
|
|
modifiedTemplateData = data.modifiedTemplateData;
|
|
if (modifiedTemplateData)
|
|
playerCache.set(cacheKey, deepfreeze(modifiedTemplateData));
|
|
else
|
|
modifiedTemplateData = playerCache.get(cacheKey);
|
|
}
|
|
|
|
for (const key in templateData)
|
|
{
|
|
const templateVal = modifiedTemplateData?.[key] || templateData[key];
|
|
const stateVal = state[key];
|
|
// Individual entries of dynamic state don't overlap with the ones in the (modified) template data, but they are
|
|
// organised into child objects. So we are only combining the two here, not actually overwriting anything.
|
|
// (elevationAdaptedRange is the only exception)
|
|
if (typeof stateVal == "object")
|
|
Object.assign(stateVal, templateVal);
|
|
else
|
|
state[key] = templateVal;
|
|
}
|
|
|
|
if (elevationAdaptedRange)
|
|
state.attack.Ranged.elevationAdaptedRange = elevationAdaptedRange;
|
|
|
|
g_EntityStates.set(entId, deepfreeze(state));
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Pull states of multiple entities from the simulation and cache them.
|
|
* This is faster than doing it for each individually.
|
|
*/
|
|
function PreloadMultipleEntityStates(ents)
|
|
{
|
|
const newEnts = ents.filter(ent => !g_EntityStates.has(ent));
|
|
if (!newEnts.length)
|
|
return;
|
|
|
|
const items = Engine.GuiInterfaceCall("GetMultipleEntityStates", newEnts);
|
|
for (let i = 0; i < items.length; i++)
|
|
_processNewEntityState(items[i], newEnts[i]);
|
|
}
|
|
|
|
/**
|
|
* Get the current state of a given entity. The data is pulled from the simulation.
|
|
*
|
|
* An entity state is like a subtype of template data (returned by GetTemplateData), it contains
|
|
* all of the values present in the latter, plus some more. Substitutability is given, it can be
|
|
* used everywhere where a template is expected.
|
|
* However, even the values also in present template data could be modified entity-locally, by
|
|
* stuff like auras and status effects. For example, two spearman of the same player can have
|
|
* different attack damage with both different from the value in the spearman template data.
|
|
* To obtain the values that are consistent player-wide (which a new entity with that template of
|
|
* that player would have) call GetTemplateData(GetEntityState(entId).templateName)
|
|
*
|
|
* The state is null, if the ID is undefined, invalid or no entity with the ID exists (anymore).
|
|
*
|
|
* Note about performance: Calling this the first time for an entity in a simulation turn is rather
|
|
* expensive, but every time after that extremely cheap, since the states are cached.
|
|
*/
|
|
function GetEntityState(entId)
|
|
{
|
|
if (!entId || entId == INVALID_ENTITY)
|
|
return null;
|
|
|
|
let state = g_EntityStates.get(entId);
|
|
// The cached state can very well be null, we only need to retrieve it from the simulation if it's undefined
|
|
// (i.e not yet present in the cache at all)
|
|
if (state === undefined)
|
|
state = _processNewEntityState(Engine.GuiInterfaceCall("GetEntityState", entId), entId);
|
|
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Returns template data of a given template accounting for player-wide modifications such
|
|
* as researched techs or team bonuses.
|
|
* @param {string} templateName - Name of the template.
|
|
* @param {number} [player] - The player whose modifications to apply to the data.
|
|
* If undefined, the currently viewed player is be used.
|
|
*/
|
|
function GetTemplateData(templateName, player)
|
|
{
|
|
const targetPlayer = player || (g_ViewedPlayer >= 0 ? g_ViewedPlayer : 0);
|
|
const playerCache = g_TemplateData.get(targetPlayer);
|
|
|
|
let templateData = playerCache.get(templateName);
|
|
if (!templateData)
|
|
{
|
|
templateData = Engine.GuiInterfaceCall("GetTemplateData", { "templateName": templateName, "player": targetPlayer });
|
|
translateObjectKeys(templateData, ["specific", "generic", "tooltip"]);
|
|
playerCache.set(templateName, deepfreeze(templateData));
|
|
}
|
|
|
|
return templateData;
|
|
}
|
|
|
|
function GetTechnologyData(technologyName, civ)
|
|
{
|
|
if (!g_TechnologyData[civ])
|
|
g_TechnologyData[civ] = {};
|
|
|
|
if (!(technologyName in g_TechnologyData[civ]))
|
|
{
|
|
const tech = TechnologyTemplates.Get(technologyName);
|
|
if (!tech)
|
|
return undefined;
|
|
const template = GetTechnologyDataHelper(tech, civ, g_ResourceData);
|
|
translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
|
|
g_TechnologyData[civ][technologyName] = deepfreeze(template);
|
|
}
|
|
|
|
return g_TechnologyData[civ][technologyName];
|
|
}
|
|
|
|
async function init(initData, hotloadData)
|
|
{
|
|
if (!g_Settings)
|
|
{
|
|
Engine.EndGame();
|
|
return { [Engine.openRequest]: { "page": "page_pregame.xml" } };
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
|
|
// Necessary when hotloading in order to repopulate g_TemplateData
|
|
Engine.GuiInterfaceCall("ResetEntityStateRetriever");
|
|
|
|
if (g_InitAttributes.campaignData)
|
|
g_CampaignSession = new CampaignSession(g_InitAttributes.campaignData);
|
|
|
|
const 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_PauseControl = new PauseControl();
|
|
g_PlayerViewControl.registerPreViewedPlayerChangeHandler(removeStatusBarDisplay);
|
|
g_Ambient = new Ambient();
|
|
g_AutoFormation = new AutoFormation();
|
|
g_Chat = new Chat(g_PlayerViewControl, g_Cheats);
|
|
g_CinemaOverlay = new CinemaOverlay();
|
|
g_DeveloperOverlay = new DeveloperOverlay(g_PlayerViewControl, g_Selection);
|
|
g_DiplomacyDialog = new DiplomacyDialog(g_PlayerViewControl, g_DiplomacyColors);
|
|
g_GameSpeedControl = new GameSpeedControl(g_PlayerViewControl);
|
|
g_MatchSettingsDialog = new MatchSettingsDialog(g_PlayerViewControl, mapCache);
|
|
g_MiniMapPanel = new MiniMapPanel(g_PlayerViewControl, g_DiplomacyColors, g_WorkerTypes);
|
|
g_NetworkDelayOverlay = new NetworkDelayOverlay();
|
|
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_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_MatchSettingsDialog, g_GameSpeedControl);
|
|
g_TimeNotificationOverlay = new TimeNotificationOverlay(g_PlayerViewControl);
|
|
|
|
initUnitsAndBuildingsHotkeys();
|
|
initBatchTrain();
|
|
initDisplayedNames();
|
|
initSelectionPanels();
|
|
LoadModificationTemplates();
|
|
updatePlayerData();
|
|
initializeMusic(); // before changing the perspective
|
|
Engine.SetBoundingBoxDebugOverlay(false);
|
|
Engine.SetGlobalHotkey("catafalque", "Press", async() =>
|
|
{
|
|
closeOpenDialogs();
|
|
g_PauseControl.implicitPause();
|
|
await Engine.OpenChildPage("page_catafalque.xml");
|
|
resumeGame();
|
|
});
|
|
Engine.SetGlobalHotkey("tips", "Press", async() =>
|
|
{
|
|
closeOpenDialogs();
|
|
g_PauseControl.implicitPause();
|
|
await Engine.OpenChildPage("page_tips.xml");
|
|
resumeGame();
|
|
});
|
|
|
|
g_DiplomacyColors.updateDisplayedPlayerColors();
|
|
|
|
const promise = Promise.race([new Promise((_, reject) =>
|
|
{
|
|
if (g_IsNetworked)
|
|
handleNetMessages().catch(reject);
|
|
}), new Promise(closeSession =>
|
|
{
|
|
g_Menu = new Menu(g_PauseControl, g_PlayerViewControl, g_Chat, closeSession);
|
|
g_NetworkStatusOverlay = new NetworkStatusOverlay(closeSession);
|
|
g_QuitConfirmationDefeat = new QuitConfirmationDefeat(closeSession);
|
|
g_QuitConfirmationReplay = new QuitConfirmationReplay(closeSession);
|
|
g_TutorialManager = new TutorialManager(closeSession, hotloadData?.tutorial);
|
|
})]);
|
|
|
|
// TODO: use event instead
|
|
onSimulationUpdate();
|
|
Engine.GetGUIObjectByName("session").onSimulationUpdate = onSimulationUpdate;
|
|
|
|
for (const handler of g_PlayersInitHandlers)
|
|
handler();
|
|
|
|
for (const handler of g_HotkeyChangeHandlers)
|
|
handler();
|
|
|
|
registerPlayersFinishedHandler(updatePlayerData);
|
|
g_DiplomacyColors.registerDiplomacyColorsChangeHandler(updatePlayerData);
|
|
registerCeasefireEndedHandler(updatePlayerData);
|
|
|
|
if (hotloadData)
|
|
{
|
|
g_Selection.selected = hotloadData.selection;
|
|
g_PlayerAssignments = hotloadData.playerAssignments;
|
|
g_Players = hotloadData.player;
|
|
}
|
|
|
|
setTimeout(displayGamestateNotifications, 1000);
|
|
|
|
const showSummary = await promise;
|
|
|
|
// Depends on the simulation, so it has to be called before EndGame.
|
|
const openRequest = getNextPageOpenRequest(showSummary);
|
|
|
|
Engine.EndGame();
|
|
// After the replay file was closed in EndGame
|
|
if (!g_IsReplay)
|
|
Engine.AddReplayToCache(Engine.GetCurrentReplayDirectory());
|
|
if (g_IsController && Engine.HasXmppClient())
|
|
Engine.SendUnregisterGame();
|
|
|
|
return { [Engine.openRequest]: openRequest };
|
|
}
|
|
|
|
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()
|
|
{
|
|
const simState = GetSimState();
|
|
if (!simState)
|
|
return;
|
|
|
|
const playerData = [];
|
|
|
|
for (let i = 0; i < simState.players.length; ++i)
|
|
{
|
|
const playerState = simState.players[i];
|
|
|
|
playerData.push({
|
|
"name": playerState.name,
|
|
"id": i,
|
|
"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,
|
|
"teamLocked": playerState.teamLocked,
|
|
"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
|
|
});
|
|
|
|
// This is done here in order to theoretically support the number of players changing.
|
|
if (i >= g_TemplateData.size)
|
|
g_TemplateData.set(i, new Map());
|
|
}
|
|
|
|
for (const guid in g_PlayerAssignments)
|
|
{
|
|
const 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)
|
|
{
|
|
const 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);
|
|
|
|
registerPlayersFinishedHandler((players, won) =>
|
|
{
|
|
if (players.includes(Engine.GetPlayerID()) && !Engine.IsAtlasRunning())
|
|
global.music.setState(won ? global.music.states.VICTORY : global.music.states.DEFEAT);
|
|
});
|
|
|
|
}
|
|
|
|
/**
|
|
* Returns true if the player with that ID is in observermode.
|
|
*/
|
|
function isPlayerObserver(playerID)
|
|
{
|
|
const 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)
|
|
{
|
|
const playerStates = GetSimState().players;
|
|
|
|
return !!playerStates[Engine.GetPlayerID()] &&
|
|
playerStates[Engine.GetPlayerID()].controlsAll ||
|
|
Engine.GetPlayerID() == playerID &&
|
|
!!playerStates[playerID] &&
|
|
playerStates[playerID].state != "defeated";
|
|
}
|
|
|
|
function resumeGame()
|
|
{
|
|
g_PauseControl.implicitResume();
|
|
}
|
|
|
|
function closeOpenDialogs()
|
|
{
|
|
g_Menu.close();
|
|
g_Chat.closePage();
|
|
g_DiplomacyDialog.close();
|
|
g_MatchSettingsDialog.close();
|
|
g_TradeDialog.close();
|
|
}
|
|
|
|
/**
|
|
* Put together an open request for the next GUI page to show after closing the session.
|
|
* This could be, depending on the situation, the the summary screen, the main menu, the lobby, etc.
|
|
*/
|
|
function getNextPageOpenRequest(showSummary)
|
|
{
|
|
const summaryData = {
|
|
"sim": Engine.GuiInterfaceCall("GetReplayMetadata"),
|
|
"gui": {
|
|
"dialog": false,
|
|
"assignedPlayer": Engine.GetPlayerID(),
|
|
"disconnected": g_Disconnected,
|
|
"isReplay": g_IsReplay,
|
|
"replayDirectory": !g_HasRejoined && Engine.GetCurrentReplayDirectory(),
|
|
"replaySelectionData": g_ReplaySelectionData
|
|
}
|
|
};
|
|
|
|
if (g_InitAttributes.campaignData)
|
|
{
|
|
const menu = g_CampaignSession.getMenu();
|
|
if (g_InitAttributes.campaignData.skipSummary)
|
|
return { "page": menu };
|
|
|
|
summaryData.campaignData = { "filename": g_InitAttributes.campaignData.run };
|
|
summaryData.nextPage = menu;
|
|
}
|
|
|
|
if (showSummary)
|
|
return { "page": "page_summary.xml", "argument": summaryData };
|
|
if (g_InitAttributes.campaignData)
|
|
return { "page": summaryData.nextPage, "argument": summaryData.campaignData };
|
|
if (Engine.HasXmppClient())
|
|
return { "page": "page_lobby.xml", "argument": { "dialog": false } };
|
|
if (g_IsReplay)
|
|
return { "page": "page_replaymenu.xml" };
|
|
|
|
return { "page": "page_pregame.xml" };
|
|
}
|
|
|
|
// 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,
|
|
"tutorial": g_TutorialManager.getHotloadData()
|
|
};
|
|
}
|
|
|
|
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 (const 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;
|
|
|
|
const now = Date.now();
|
|
const tickLength = now - g_LastTickTime;
|
|
g_LastTickTime = now;
|
|
|
|
updateCursorAndTooltip();
|
|
updateTimers();
|
|
|
|
if (g_CinemaOverlay.isInCutsceneMode())
|
|
return;
|
|
|
|
if (g_Selection.dirty)
|
|
{
|
|
g_Selection.dirty = false;
|
|
// When selection changed, get the entityStates of new entities
|
|
PreloadMultipleEntityStates(g_Selection.toList());
|
|
|
|
for (const 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();
|
|
|
|
Engine.GuiInterfaceCall("ClearRenamedEntities");
|
|
}
|
|
|
|
function onSimulationUpdate()
|
|
{
|
|
g_EntityStates.clear();
|
|
g_SimState = undefined;
|
|
|
|
Engine.ProfileStart("sdf");
|
|
const info = Engine.GuiInterfaceCall("GetAndClearTemplateChanges");
|
|
if (info.playersWithChangedModifications)
|
|
for (const player of info.playersWithChangedModifications)
|
|
// Player-global modifications changed (e.g. by a technology), so the template data cache needs to be cleared.
|
|
g_TemplateData[player] = {};
|
|
|
|
// Some changes require reloading some cached values even if the selection itself didn't change.
|
|
if (info.selectionDirty)
|
|
g_Selection.onChange();
|
|
|
|
Engine.ProfileStop("sdf");
|
|
|
|
if (!GetSimState())
|
|
return;
|
|
|
|
PreloadMultipleEntityStates(g_Selection.toList());
|
|
|
|
for (const handler of g_SimulationUpdateHandlers)
|
|
handler();
|
|
|
|
// TODO: Move to handlers
|
|
handleNotifications();
|
|
updateGUIObjects();
|
|
}
|
|
|
|
function toggleGUI()
|
|
{
|
|
g_ShowGUI = !g_ShowGUI;
|
|
Engine.GetGUIObjectByName("primaryOverlays").hidden = !g_ShowGUI;
|
|
Engine.GetGUIObjectByName("supplementaryOverlays").hidden = !g_ShowGUI;
|
|
}
|
|
|
|
// 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.
|
|
const 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
|
|
const getCostSum = (ent) =>
|
|
{
|
|
const entState = GetEntityState(ent);
|
|
const cost = GetTemplateData(entState.templateName, entState.player).cost;
|
|
return cost ? Object.keys(cost).map(key => cost[key]).reduce((sum, cur) => sum + cur) : 0;
|
|
};
|
|
|
|
for (const i in Engine.GetGUIObjectByName("unitGroupPanel").children)
|
|
{
|
|
Engine.GetGUIObjectByName("unitGroupLabel[" + i + "]").caption = +i + 1;
|
|
|
|
const button = Engine.GetGUIObjectByName("unitGroupButton[" + i + "]");
|
|
button.hidden = g_Groups.groups[i].getTotalCount() == 0;
|
|
button.onPress = (function(groupId) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), groupId); }; })(i);
|
|
button.onDoublePress = (function(groupId) { return function() { performGroup("snap", groupId); }; })(i);
|
|
button.onPressRight = (function(groupId) { return function() { performGroup("breakUp", groupId); }; })(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)
|
|
{
|
|
const 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]).templateName).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
|
|
{
|
|
const selected = g_Selection.toList();
|
|
for (const ent of g_Selection.highlighted)
|
|
selected.push(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);
|
|
}
|
|
|
|
/**
|
|
* Updates the primary/secondary names in the simulation and GUI.
|
|
*/
|
|
function updateDisplayedNames()
|
|
{
|
|
g_SpecificNamesPrimary = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 2;
|
|
g_ShowSecondaryNames = Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 0 || Engine.ConfigDB_GetValue("user", "gui.session.howtoshownames") == 1;
|
|
}
|
|
|
|
/**
|
|
* Inverts the given configuration boolean and returns the current state.
|
|
* For example "silhouettes".
|
|
*/
|
|
function toggleConfigBool(configName)
|
|
{
|
|
const enabled = Engine.ConfigDB_GetValue("user", configName) != "true";
|
|
Engine.ConfigDB_CreateAndSaveValue("user", configName, String(enabled));
|
|
return enabled;
|
|
}
|
|
|
|
// Update the additional list of entities to be highlighted.
|
|
function updateAdditionalHighlight()
|
|
{
|
|
const entsAdd = []; // list of entities units to be highlighted
|
|
const entsRemove = [];
|
|
const highlighted = g_Selection.toList();
|
|
for (const ent of g_Selection.highlighted)
|
|
highlighted.push(ent);
|
|
|
|
if (g_ShowGuarding)
|
|
// flag the guarding entities to add in this additional highlight
|
|
for (const sel of g_Selection.toList())
|
|
{
|
|
const state = GetEntityState(sel);
|
|
if (!state.guard || !state.guard.entities.length)
|
|
continue;
|
|
|
|
for (const 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 (const sel of g_Selection.toList())
|
|
{
|
|
const state = GetEntityState(sel);
|
|
if (!state.unitAI || !state.unitAI.isGuarding)
|
|
continue;
|
|
const 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 (const 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;
|
|
}
|