Index: ps/trunk/binaries/data/mods/public/gui/session/session.js =================================================================== --- ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 14751) +++ ps/trunk/binaries/data/mods/public/gui/session/session.js (revision 14752) @@ -1,1012 +1,1026 @@ // Network Mode var g_IsNetworked = false; // Is this user in control of game settings (i.e. is a network server, or offline player) var g_IsController; // Match ID for tracking var g_MatchID; // Cache the basic player data (name, civ, color) var g_Players = []; // Cache the useful civ data var g_CivData = {}; var g_GameSpeeds = {}; var g_CurrentSpeed; var g_PlayerAssignments = { "local": { "name": "You", "player": 1 } }; // Cache dev-mode settings that are frequently or widely used var g_DevSettings = { controlAll: false }; // Whether status bars should be shown for all of the player's units. var g_ShowAllStatusBars = false; // Indicate when one of the current player's training queues is blocked // (this is used to support population counter blinking) var g_IsTrainingBlocked = false; // Cache simulation state (updated on every simulation update) var g_SimState; // Cache EntityStates var g_EntityStates = {}; // {id:entState} // Whether the player has lost/won and reached the end of their game var g_GameEnded = false; var g_Disconnected = false; // Lost connection to server // Holds player states from the last tick var g_CachedLastStates = ""; // Colors to flash when pop limit reached const DEFAULT_POPULATION_COLOR = "white"; const POPULATION_ALERT_COLOR = "orange"; // List of additional entities to highlight var g_ShowGuarding = false; var g_ShowGuarded = false; var g_AdditionalHighlight = []; // for saving the hitpoins of the hero (is there a better way to do that?) // Should be possible with AttackDetection but might be an overkill because it would have to loop // always through the list of all ongoing attacks... var g_previousHeroHitPoints = undefined; function GetSimState() { if (!g_SimState) g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); return g_SimState; } function GetEntityState(entId) { if (!(entId in g_EntityStates)) { var entState = Engine.GuiInterfaceCall("GetEntityState", entId); if (entState) entState.extended = false; g_EntityStates[entId] = entState; } return g_EntityStates[entId]; } function GetExtendedEntityState(entId) { if (entId in g_EntityStates) var entState = g_EntityStates[entId]; else var entState = Engine.GuiInterfaceCall("GetEntityState", entId); if (!entState || entState.extended) return entState; var extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId); for (var prop in extension) entState[prop] = extension[prop]; entState.extended = true; g_EntityStates[entId] = entState; return entState; } // Cache TemplateData var g_TemplateData = {}; // {id:template} function GetTemplateData(templateName) { if (!(templateName in g_TemplateData)) { var template = Engine.GuiInterfaceCall("GetTemplateData", templateName); g_TemplateData[templateName] = template; } return g_TemplateData[templateName]; } // Cache TechnologyData var g_TechnologyData = {}; // {id:template} function GetTechnologyData(technologyName) { if (!(technologyName in g_TechnologyData)) { var template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName); g_TechnologyData[technologyName] = template; } return g_TechnologyData[technologyName]; } // Init function init(initData, hotloadData) { if (initData) { g_IsNetworked = initData.isNetworked; // Set network mode g_IsController = initData.isController; // Set controller mode g_PlayerAssignments = initData.playerAssignments; g_MatchID = initData.attribs.matchID; // Cache the player data // (This may be updated at runtime by handleNetMessage) g_Players = getPlayerData(g_PlayerAssignments); if (initData.savedGUIData) restoreSavedGameData(initData.savedGUIData); Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked; } else // Needed for autostart loading option { g_Players = getPlayerData(null); } // Cache civ data g_CivData = loadCivData(); g_CivData["gaia"] = { "Code": "gaia", "Name": "Gaia" }; g_GameSpeeds = initGameSpeeds(); g_CurrentSpeed = Engine.GetSimRate(); var gameSpeed = Engine.GetGUIObjectByName("gameSpeed"); gameSpeed.list = g_GameSpeeds.names; gameSpeed.list_data = g_GameSpeeds.speeds; var idx = g_GameSpeeds.speeds.indexOf(g_CurrentSpeed); gameSpeed.selected = idx != -1 ? idx : g_GameSpeeds["default"]; gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); } Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[Engine.GetPlayerID()].civ].Emblem; Engine.GetGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[Engine.GetPlayerID()].civ].Name; initMenuPosition(); // set initial position // Populate player selection dropdown var playerNames = []; var playerIDs = []; for (var player in g_Players) { playerNames.push(g_Players[player].name); playerIDs.push(player); } var viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer"); viewPlayerDropdown.list = playerNames; viewPlayerDropdown.list_data = playerIDs; viewPlayerDropdown.selected = Engine.GetPlayerID(); // If in Atlas editor, disable the exit button if (Engine.IsAtlasRunning()) Engine.GetGUIObjectByName("menuExitButton").enabled = false; if (hotloadData) { g_Selection.selected = hotloadData.selection; } else { // Starting for the first time: var civMusic = g_CivData[g_Players[Engine.GetPlayerID()].civ].Music; initMusic(); global.music.storeTracks(civMusic); global.music.setState(global.music.states.PEACE); playRandomAmbient("temperate"); } if (Engine.ConfigDB_GetValue("user", "gui.session.timeelapsedcounter") === "true") Engine.GetGUIObjectByName("timeElapsedCounter").hidden = false; onSimulationUpdate(); // Report the performance after 5 seconds (when we're still near // the initial camera view) and a minute (when the profiler will // have settled down if framerates as very low), to give some // extremely rough indications of performance setTimeout(function() { reportPerformance(5); }, 5000); setTimeout(function() { reportPerformance(60); }, 60000); } function selectViewPlayer(playerID) { Engine.SetPlayerID(playerID); if (playerID != 0) { Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[playerID].civ].Emblem; Engine.GetGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[playerID].civ].Name; } } function reportPerformance(time) { var settings = Engine.GetMapSettings(); var data = { time: time, map: settings.Name, seed: settings.Seed, // only defined for random maps size: settings.Size, // only defined for random maps profiler: Engine.GetProfilerState() }; Engine.SubmitUserReport("profile", 3, JSON.stringify(data)); } function resignGame() { var simState = GetSimState(); // Players can't resign if they've already won or lost. if (simState.players[Engine.GetPlayerID()].state != "active" || g_Disconnected) return; // Tell other players that we have given up and been defeated Engine.PostNetworkCommand({ "type": "defeat-player", "playerId": Engine.GetPlayerID() }); global.music.setState(global.music.states.DEFEAT); resumeGame(); } function leaveGame() { var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState"); var playerState = extendedSimState.players[Engine.GetPlayerID()]; var mapSettings = Engine.GetMapSettings(); var gameResult; if (g_Disconnected) { gameResult = "You have been disconnected." } else if (playerState.state == "won") { gameResult = "You have won the battle!"; } else if (playerState.state == "defeated") { gameResult = "You have been defeated..."; } else // "active" { gameResult = "You have abandoned the game."; // Tell other players that we have given up and been defeated Engine.PostNetworkCommand({ "type": "defeat-player", "playerId": Engine.GetPlayerID() }); global.music.setState(global.music.states.DEFEAT); } stopAmbient(); Engine.EndGame(); if (g_IsController && Engine.HasXmppClient()) Engine.SendUnregisterGame(); Engine.SwitchGuiPage("page_summary.xml", { "gameResult" : gameResult, "timeElapsed" : extendedSimState.timeElapsed, "playerStates": extendedSimState.players, "players": g_Players, "mapSettings": mapSettings }); } // Return some data that we'll use when hotloading this file after changes function getHotloadData() { return { selection: g_Selection.selected }; } // Return some data that will be stored in saved game files function getSavedGameData() { var data = {}; data.playerAssignments = g_PlayerAssignments; data.groups = g_Groups.groups; // TODO: any other gui state? return data; } function restoreSavedGameData(data) { // Clear selection when loading a game g_Selection.reset(); // Restore control groups for (var groupNumber in data.groups) { g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups; g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents; } updateGroups(); } var lastTickTime = new Date; var lastXmppClientPoll = Date.now(); /** * Called every frame. */ function onTick() { var now = new Date; var tickLength = new Date - lastTickTime; lastTickTime = now; checkPlayerState(); while (true) { var message = Engine.PollNetworkClient(); if (!message) break; handleNetMessage(message); } updateCursorAndTooltip(); // If the selection changed, we need to regenerate the sim display (the display depends on both the // simulation state and the current selection). if (g_Selection.dirty) { g_Selection.dirty = false; onSimulationUpdate(); // Display rally points for selected buildings Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() }); } // Run timers updateTimers(); // Animate menu updateMenuPosition(tickLength); // When training is blocked, flash population (alternates colour every 500msec) if (g_IsTrainingBlocked && (Date.now() % 1000) < 500) Engine.GetGUIObjectByName("resourcePop").textcolor = POPULATION_ALERT_COLOR; else Engine.GetGUIObjectByName("resourcePop").textcolor = DEFAULT_POPULATION_COLOR; // Clear renamed entities list Engine.GuiInterfaceCall("ClearRenamedEntities"); // If the lobby is running, wake it up every 10 seconds so we stay connected. if (Engine.HasXmppClient() && (Date.now() - lastXmppClientPoll) > 10000) { Engine.RecvXmppClient(); lastXmppClientPoll = Date.now(); } } function checkPlayerState() { // Once the game ends, we're done here. if (g_GameEnded) return; // Send a game report for each player in this game. var m_simState = GetSimState(); var playerState = m_simState.players[Engine.GetPlayerID()]; var tempStates = ""; for each (var player in m_simState.players) {tempStates += player.state + ",";} if (g_CachedLastStates != tempStates) { g_CachedLastStates = tempStates; reportGame(Engine.GuiInterfaceCall("GetExtendedSimulationState")); } // If the local player hasn't finished playing, we return here to avoid the victory/defeat messages. if (playerState.state == "active") return; // We can't resign once the game is over. Engine.GetGUIObjectByName("menuResignButton").enabled = false; // Make sure nothing is open to avoid stacking. closeMenu(); closeOpenDialogs(); // Make sure this doesn't run again. g_GameEnded = true; if (Engine.IsAtlasRunning()) { // If we're in Atlas, we can't leave the game var btCaptions = ["OK"]; var btCode = [null]; var message = "Press OK to continue"; } else { var btCaptions = ["Yes", "No"]; var btCode = [leaveGame, null]; var message = "Do you want to quit?"; } if (playerState.state == "defeated") { global.music.setState(global.music.states.DEFEAT); messageBox(400, 200, message, "DEFEATED!", 0, btCaptions, btCode); } else if (playerState.state == "won") { global.music.setState(global.music.states.VICTORY); // TODO: Reveal map directly instead of this silly proxy. if (!Engine.GetGUIObjectByName("devCommandsRevealMap").checked) Engine.GetGUIObjectByName("devCommandsRevealMap").checked = true; messageBox(400, 200, message, "VICTORIOUS!", 0, btCaptions, btCode); } } function changeGameSpeed(speed) { // For non-networked games only if (!g_IsNetworked) { Engine.SetSimRate(speed); g_CurrentSpeed = speed; } } /** * Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation * update (see session.xml), or from onTick when the selection has changed. */ function onSimulationUpdate() { g_EntityStates = {}; g_TemplateData = {}; g_TechnologyData = {}; g_SimState = Engine.GuiInterfaceCall("GetSimulationState"); // If we're called during init when the game is first loading, there will be no simulation yet, so do nothing if (!g_SimState) return; handleNotifications(); if (g_ShowAllStatusBars) recalculateStatusBarDisplay(); if (g_ShowGuarding || g_ShowGuarded) updateAdditionalHighlight(); updateHero(); updateGroups(); updateDebug(); updatePlayerDisplay(); updateSelectionDetails(); updateResearchDisplay(); updateBuildingPlacementPreview(); updateTimeElapsedCounter(); updateTimeNotifications(); // Update music state on basis of battle state. var battleState = Engine.GuiInterfaceCall("GetBattleState", Engine.GetPlayerID()); if (battleState) global.music.setState(global.music.states[battleState]); } /** * updates a status bar on the GUI * nameOfBar: name of the bar * points: points to show * maxPoints: max points * direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3; */ function updateGUIStatusBar(nameOfBar, points, maxPoints, direction) { // check, if optional direction parameter is valid. if (!direction || !(direction >= 0 && direction < 4)) direction = 0; // get the bar and update it var statusBar = Engine.GetGUIObjectByName(nameOfBar); if (!statusBar) return; var healthSize = statusBar.size; var value = 100*Math.max(0, Math.min(1, points / maxPoints)); // inverse bar if(direction == 2 || direction == 3) value = 100 - value; if(direction == 0) healthSize.rright = value; else if(direction == 1) healthSize.rbottom = value; else if(direction == 2) healthSize.rleft = value; else if(direction == 3) healthSize.rtop = value; // update bar statusBar.size = healthSize; } function updateHero() { var simState = GetSimState(); var playerState = simState.players[Engine.GetPlayerID()]; var unitHeroPanel = Engine.GetGUIObjectByName("unitHeroPanel"); var heroButton = Engine.GetGUIObjectByName("unitHeroButton"); if (!playerState || playerState.heroes.length <= 0) { g_previousHeroHitPoints = undefined; unitHeroPanel.hidden = true; return; } var heroImage = Engine.GetGUIObjectByName("unitHeroImage"); var heroState = GetExtendedEntityState(playerState.heroes[0]); var template = GetTemplateData(heroState.template); heroImage.sprite = "stretched:session/portraits/" + template.icon; var hero = playerState.heroes[0]; heroButton.onpress = function() { if (!Engine.HotkeyIsPressed("selection.add")) g_Selection.reset(); g_Selection.addList([hero]); }; heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); }; unitHeroPanel.hidden = false; // Setup tooltip var tooltip = "[font=\"serif-bold-16\"]" + template.name.specific + "[/font]"; tooltip += "\n[font=\"serif-bold-13\"]Health:[/font] " + heroState.hitpoints + "/" + heroState.maxHitpoints; tooltip += "\n[font=\"serif-bold-13\"]" + (heroState.attack ? heroState.attack.type + " " : "") + "Attack:[/font] " + damageTypeDetails(heroState.attack); // Show max attack range if ranged attack, also convert to tiles (4m per tile) if (heroState.attack && heroState.attack.type == "Ranged") tooltip += ", [font=\"serif-bold-13\"]Range:[/font] " + Math.round(heroState.attack.maxRange/4); tooltip += "\n[font=\"serif-bold-13\"]Armor:[/font] " + damageTypeDetails(heroState.armour); tooltip += "\n" + template.tooltip; heroButton.tooltip = tooltip; // update heros health bar updateGUIStatusBar("heroHealthBar", heroState.hitpoints, heroState.maxHitpoints); // define the hit points if not defined if (!g_previousHeroHitPoints) g_previousHeroHitPoints = heroState.hitpoints; // check, if the health of the hero changed since the last update if (heroState.hitpoints < g_previousHeroHitPoints) { g_previousHeroHitPoints = heroState.hitpoints; // trigger the animation startColorFade("heroHitOverlay", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit); return; } } function updateGroups() { var guiName = "Group"; g_Groups.update(); for (var i = 0; i < 10; i++) { var button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]"); var label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i; if (g_Groups.groups[i].getTotalCount() == 0) button.hidden = true; else button.hidden = false; 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); } var numButtons = i; var rowLength = 1; var numRows = Math.ceil(numButtons / rowLength); var buttonSideLength = Engine.GetGUIObjectByName("unit"+guiName+"Button[0]").size.bottom; var buttonSpacer = buttonSideLength+1; for (var i = 0; i < numRows; i++) layoutButtonRow(i, guiName, buttonSideLength, buttonSpacer, rowLength*i, rowLength*(i+1) ); } function updateDebug() { var simState = GetSimState(); var debug = Engine.GetGUIObjectByName("debug"); if (Engine.GetGUIObjectByName("devDisplayState").checked) { debug.hidden = false; } else { debug.hidden = true; return; } var conciseSimState = deepcopy(simState); conciseSimState.players = "<<>>"; var text = "simulation: " + uneval(conciseSimState); var selection = g_Selection.toList(); if (selection.length) { var entState = GetExtendedEntityState(selection[0]); if (entState) { var template = GetTemplateData(entState.template); text += "\n\nentity: {\n"; for (var k in entState) text += " "+k+":"+uneval(entState[k])+"\n"; text += "}\n\ntemplate: " + uneval(template); } } debug.caption = text; } function updatePlayerDisplay() { var simState = GetSimState(); var playerState = simState.players[Engine.GetPlayerID()]; if (!playerState) return; Engine.GetGUIObjectByName("resourceFood").caption = Math.floor(playerState.resourceCounts.food); Engine.GetGUIObjectByName("resourceWood").caption = Math.floor(playerState.resourceCounts.wood); Engine.GetGUIObjectByName("resourceStone").caption = Math.floor(playerState.resourceCounts.stone); Engine.GetGUIObjectByName("resourceMetal").caption = Math.floor(playerState.resourceCounts.metal); Engine.GetGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit; g_IsTrainingBlocked = playerState.trainingBlocked; } function selectAndMoveTo(ent) { var entState = GetEntityState(ent); if (!entState || !entState.position) return; g_Selection.reset(); g_Selection.addList([ent]); var position = entState.position; Engine.CameraMoveTo(position.x, position.z); } function updateResearchDisplay() { var researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", Engine.GetPlayerID()); if (!researchStarted) return; // Set up initial positioning. var buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right; for (var i = 0; i < 10; ++i) { var button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]"); var size = button.size; size.top = (4 + buttonSideLength) * i; size.bottom = size.top + buttonSideLength; button.size = size; } var numButtons = 0; for (var tech in researchStarted) { // Show at most 10 in-progress techs. if (numButtons >= 10) break; var template = GetTechnologyData(tech); var button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]"); button.hidden = false; button.tooltip = getEntityNames(template); button.onpress = (function(e) { return function() { selectAndMoveTo(e) } })(researchStarted[tech].researcher); var icon = "stretched:session/portraits/" + template.icon; Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon; // Scale the progress indicator. var size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size; // Buttons are assumed to be square, so left/right offsets can be used for top/bottom. size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left)); Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size; ++numButtons; } // Hide unused buttons. for (var i = numButtons; i < 10; ++i) Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true; } function updateTimeElapsedCounter() { var simState = GetSimState(); var speed = g_CurrentSpeed != 1.0 ? " (" + g_CurrentSpeed + "x)" : ""; var timeElapsedCounter = Engine.GetGUIObjectByName("timeElapsedCounter"); timeElapsedCounter.caption = timeToString(simState.timeElapsed) + speed; } // Toggles the display of status bars for all of the player's entities. function recalculateStatusBarDisplay() { if (g_ShowAllStatusBars) var entities = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID()); else { var selected = g_Selection.toList(); for each (var ent in g_Selection.highlighted) selected.push(ent); // Remove selected entities from the 'all entities' array, to avoid disabling their status bars. var entities = Engine.GuiInterfaceCall("GetPlayerEntities").filter( function(idx) { return (selected.indexOf(idx) == -1); } ); } Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars }); } // Update the additional list of entities to be highlighted. function updateAdditionalHighlight() { var entsAdd = []; // list of entities units to be highlighted var entsRemove = []; var highlighted = g_Selection.toList(); for each (var ent in g_Selection.highlighted) highlighted.push(ent); if (g_ShowGuarding) { // flag the guarding entities to add in this additional highlight for each (var sel in g_Selection.selected) { var state = GetEntityState(sel); if (!state.guard || !state.guard.entities.length) continue; for each (var ent in 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 each (var sel in g_Selection.selected) { var state = GetEntityState(sel); if (!state.unitAI || !state.unitAI.isGuarding) continue; var 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 each (var ent in g_AdditionalHighlight) if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1) entsRemove.push(ent); _setHighlight(entsAdd , HIGHLIGHTED_ALPHA, true ); _setHighlight(entsRemove, 0 , false); g_AdditionalHighlight = entsAdd; } // Temporarily adding this here const AMBIENT_TEMPERATE = "temperate"; var currentAmbient; function playRandomAmbient(type) { switch (type) { case AMBIENT_TEMPERATE: // Seem to need the underscore at the end of "temperate" to avoid crash // (Might be caused by trying to randomly load day_temperate.xml) // currentAmbient = newRandomSound("ambient", "temperate_", "dayscape"); const AMBIENT = "audio/ambient/dayscape/day_temperate_gen_03.ogg"; Engine.PlayAmbientSound( AMBIENT, true ); break; default: Engine.Console_Write("Unrecognized ambient type: " + type); break; } } // Temporarily adding this here function stopAmbient() { if (currentAmbient) { currentAmbient.free(); currentAmbient = null; } } // Send a report on the game status to the lobby function reportGame(extendedSimState) { if (!Engine.HasXmppClient()) return; // units var unitsClasses = [ "total", "Infantry", "Worker", "Female", "Cavalry", "Champion", "Hero", "Ship" ]; var unitsCountersTypes = [ "unitsTrained", "unitsLost", "enemyUnitsKilled" ]; // buildings var buildingsClasses = [ "total", "CivCentre", "House", "Economic", "Outpost", "Military", "Fortress", "Wonder" ]; var buildingsCountersTypes = [ "buildingsConstructed", "buildingsLost", "enemyBuildingsDestroyed" ]; // resources var resourcesTypes = [ "wood", "food", "stone", "metal" ]; var resourcesCounterTypes = [ "resourcesGathered", "resourcesUsed", "resourcesSold", "resourcesBought" ]; var playerStatistics = { }; // Unit Stats for each (var unitCounterType in unitsCountersTypes) { if (!playerStatistics[unitCounterType]) playerStatistics[unitCounterType] = { }; for each (var unitsClass in unitsClasses) playerStatistics[unitCounterType][unitsClass] = ""; } playerStatistics.unitsLostValue = ""; playerStatistics.unitsKilledValue = ""; // Building stats for each (var buildingCounterType in buildingsCountersTypes) { if (!playerStatistics[buildingCounterType]) playerStatistics[buildingCounterType] = { }; for each (var buildingsClass in buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] = ""; } playerStatistics.buildingsLostValue = ""; playerStatistics.enemyBuildingsDestroyedValue = ""; // Resources for each (var resourcesCounterType in resourcesCounterTypes) { if (!playerStatistics[resourcesCounterType]) playerStatistics[resourcesCounterType] = { }; for each (var resourcesType in resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] = ""; } playerStatistics.resourcesGathered.vegetarianFood = ""; playerStatistics.tradeIncome = ""; // Tribute playerStatistics.tributesSent = ""; playerStatistics.tributesReceived = ""; + // Total + playerStatistics.economyScore = ""; + playerStatistics.militaryScore = ""; + playerStatistics.totalScore = ""; // Various playerStatistics.treasuresCollected = ""; playerStatistics.feminisation = ""; playerStatistics.percentMapExplored = ""; var mapName = Engine.GetMapSettings().Name; var playerStates = ""; var playerCivs = ""; var teams = ""; var teamsLocked = true; // Serialize the statistics for each player into a comma-separated list. for each (var player in extendedSimState.players) { playerStates += player.state + ","; playerCivs += player.civ + ","; teams += player.team + ","; teamsLocked = teamsLocked && player.teamsLocked; for each (var resourcesCounterType in resourcesCounterTypes) for each (var resourcesType in resourcesTypes) playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ","; playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ","; for each (var unitCounterType in unitsCountersTypes) for each (var unitsClass in unitsClasses) playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ","; for each (var buildingCounterType in buildingsCountersTypes) for each (var buildingsClass in buildingsClasses) playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ","; - + var total = 0; + for each (var res in player.statistics.resourcesGathered) + total += res; + playerStatistics.economyScore += total + ","; + playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue + + player.statistics.enemyBuildingsDestroyedValue) / 10) + ","; + playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue + + player.statistics.enemyBuildingsDestroyedValue) / 10)) + ","; playerStatistics.tradeIncome += player.statistics.tradeIncome + ","; playerStatistics.tributesSent += player.statistics.tributesSent + ","; playerStatistics.tributesReceived += player.statistics.tributesReceived + ","; playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ","; playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ","; } // Send the report with serialized data var reportObject = { }; reportObject.timeElapsed = extendedSimState.timeElapsed; reportObject.playerStates = playerStates; reportObject.playerID = Engine.GetPlayerID(); reportObject.matchID = g_MatchID; reportObject.civs = playerCivs; reportObject.teams = teams; reportObject.teamsLocked = String(teamsLocked); reportObject.mapName = mapName; + reportObject.economyScore = playerStatistics.economyScore; + reportObject.militaryScore = playerStatistics.militaryScore; + reportObject.totalScore = playerStatistics.totalScore; for each (var rct in resourcesCounterTypes) { for each (var rt in resourcesTypes) reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt]; // eg. rt = food rct.substr = Gathered rct = resourcesGathered } reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood; for each (var type in unitsClasses) { // eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type]; reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type]; } for each (var type in buildingsClasses) { reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type]; reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type]; reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type]; } reportObject.tributesSent = playerStatistics.tributesSent; reportObject.tributesReceived = playerStatistics.tributesReceived; reportObject.percentMapExplored = playerStatistics.percentMapExplored; reportObject.treasuresCollected = playerStatistics.treasuresCollected; reportObject.tradeIncome = playerStatistics.tradeIncome; Engine.SendGameReport(reportObject); } Index: ps/trunk/source/tools/XpartaMuPP/LobbyRanking.py =================================================================== --- ps/trunk/source/tools/XpartaMuPP/LobbyRanking.py (revision 14751) +++ ps/trunk/source/tools/XpartaMuPP/LobbyRanking.py (revision 14752) @@ -1,99 +1,135 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Copyright (C) 2013 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . """ import sqlalchemy -from sqlalchemy import Column, ForeignKey, Integer, String +from sqlalchemy import Column, ForeignKey, Integer, String, Boolean from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.ext.declarative import declarative_base engine = sqlalchemy.create_engine('sqlite:///lobby_rankings.sqlite3') Session = sessionmaker(bind=engine) session = Session() Base = declarative_base() class Player(Base): __tablename__ = 'players' id = Column(Integer, primary_key=True) jid = Column(String(255)) rating = Column(Integer) games = relationship('Game', secondary='players_info') # These two relations really only exist to satisfy the linkage # between PlayerInfo and Player and Game and player. games_info = relationship('PlayerInfo', backref='player') games_won = relationship('Game', backref='winner') class PlayerInfo(Base): __tablename__ = 'players_info' id = Column(Integer, primary_key=True) player_id = Column(Integer, ForeignKey('players.id')) game_id = Column(Integer, ForeignKey('games.id')) - civ = String(20) + civs = Column(String(20)) + teams = Column(Integer) economyScore = Column(Integer) militaryScore = Column(Integer) - explorationScore = Column(Integer) - # Store to avoid needlessly recomputing it. totalScore = Column(Integer) - unitsTrained = Column(Integer) - unitsLost = Column(Integer) - unitsKilled = Column(Integer) - buildingsConstructed = Column(Integer) - buildingsLost = Column(Integer) - buildingsDestroyed = Column(Integer) - civCentersBuilt = Column(Integer) - civCentersDestroyed = Column(Integer) - percentMapExplored = Column(Integer) foodGathered = Column(Integer) foodUsed = Column(Integer) woodGathered = Column(Integer) woodUsed = Column(Integer) stoneGathered = Column(Integer) stoneUsed = Column(Integer) metalGathered = Column(Integer) metalUsed = Column(Integer) - vegetarianRatio = Column(Integer) + vegetarianFoodGathered = Column(Integer) treasuresCollected = Column(Integer) tributesSent = Column(Integer) - tributesRecieved = Column(Integer) - foodBought = Column(Integer) - foodSold = Column(Integer) + tributesReceived = Column(Integer) + totalUnitsTrained = Column(Integer) + totalUnitsLost = Column(Integer) + enemytotalUnitsKilled = Column(Integer) + infantryUnitsTrained = Column(Integer) + infantryUnitsLost = Column(Integer) + enemyInfantryUnitsKilled = Column(Integer) + workerUnitsTrained = Column(Integer) + workerUnitsLost = Column(Integer) + enemyWorkerUnitsKilled = Column(Integer) + femaleUnitsTrained = Column(Integer) + femaleUnitsLost = Column(Integer) + enemyFemaleUnitsKilled = Column(Integer) + cavalryUnitsTrained = Column(Integer) + cavalryUnitsLost = Column(Integer) + enemyCavalryUnitsKilled = Column(Integer) + championUnitsTrained = Column(Integer) + championUnitsLost = Column(Integer) + enemyChampionUnitsKilled = Column(Integer) + heroUnitsTrained = Column(Integer) + heroUnitsLost = Column(Integer) + enemyHeroUnitsKilled = Column(Integer) + shipUnitsTrained = Column(Integer) + shipUnitsLost = Column(Integer) + enemyShipUnitsKilled = Column(Integer) + totalBuildingsConstructed = Column(Integer) + totalBuildingsLost = Column(Integer) + enemytotalBuildingsDestroyed = Column(Integer) + civCentreBuildingsConstructed = Column(Integer) + civCentreBuildingsLost = Column(Integer) + enemyCivCentreBuildingsDestroyed = Column(Integer) + houseBuildingsConstructed = Column(Integer) + houseBuildingsLost = Column(Integer) + enemyHouseBuildingsDestroyed = Column(Integer) + economicBuildingsConstructed = Column(Integer) + economicBuildingsLost = Column(Integer) + enemyEconomicBuildingsDestroyed = Column(Integer) + outpostBuildingsConstructed = Column(Integer) + outpostBuildingsLost = Column(Integer) + enemyOutpostBuildingsDestroyed = Column(Integer) + militaryBuildingsConstructed = Column(Integer) + militaryBuildingsLost = Column(Integer) + enemyMilitaryBuildingsDestroyed = Column(Integer) + fortressBuildingsConstructed = Column(Integer) + fortressBuildingsLost = Column(Integer) + enemyFortressBuildingsDestroyed = Column(Integer) + wonderBuildingsConstructed = Column(Integer) + wonderBuildingsLost = Column(Integer) + enemyWonderBuildingsDestroyed = Column(Integer) woodBought = Column(Integer) - woodSold = Column(Integer) + foodBought = Column(Integer) stoneBought = Column(Integer) - stoneSold = Column(Integer) metalBought = Column(Integer) - metalSold = Column(Integer) - barterEfficiency = Column(Integer) tradeIncome = Column(Integer) + percentMapExplored = Column(Integer) class Game(Base): __tablename__ = 'games' id = Column(Integer, primary_key=True) map = Column(String(80)) duration = Column(Integer) + teamsLocked = Column(Boolean) + matchID = Column(String(20)) winner_id = Column(Integer, ForeignKey('players.id')) player_info = relationship('PlayerInfo', backref='game') players = relationship('Player', secondary='players_info') if __name__ == '__main__': Base.metadata.create_all(engine) Index: ps/trunk/source/tools/XpartaMuPP/XpartaMuPP.py =================================================================== --- ps/trunk/source/tools/XpartaMuPP/XpartaMuPP.py (revision 14751) +++ ps/trunk/source/tools/XpartaMuPP/XpartaMuPP.py (revision 14752) @@ -1,791 +1,807 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- """Copyright (C) 2014 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 of the License, or * (at your option) any later version. * * 0 A.D. is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with 0 A.D. If not, see . """ import logging, time, traceback from optparse import OptionParser import sleekxmpp from sleekxmpp.stanza import Iq from sleekxmpp.xmlstream import ElementBase, register_stanza_plugin, ET from sleekxmpp.xmlstream.handler import Callback from sleekxmpp.xmlstream.matcher import StanzaPath from LobbyRanking import session as db, Game, Player, PlayerInfo from ELO import get_rating_adjustment # Rating that new players should be inserted into the # database with, before they've played any games. leaderboard_default_rating = 1200 ## Class that contains and manages leaderboard data ## class LeaderboardList(): def __init__(self, room): self.room = room self.lastRated = "" def getOrCreatePlayer(self, JID): """ Stores a player(JID) in the database if they don't yet exist. Returns either the newly created instance of the Player model, or the one that already exists in the database. """ players = db.query(Player).filter_by(jid=str(JID)) if not players.first(): player = Player(jid=str(JID), rating=-1) db.add(player) db.commit() return player return players.first() def removePlayer(self, JID): """ Remove a player(JID) from database. Returns the player that was removed, or None if that player didn't exist. """ players = db.query(Player).filter_by(jid=JID) player = players.first() if not player: return None players.delete() return player def addGame(self, gamereport): """ Adds a game to the database and updates the data on a player(JID) from game results. Returns the created Game object, or None if the creation failed for any reason. Side effects: Inserts a new Game instance into the database. """ # Discard any games still in progress. if any(map(lambda state: state == 'active', dict.values(gamereport['playerStates']))): return None players = map(lambda jid: db.query(Player).filter_by(jid=jid).first(), dict.keys(gamereport['playerStates'])) winning_jid = list(dict.keys({jid: state for jid, state in gamereport['playerStates'].items() if state == 'won'}))[0] - def get(stat, jid): return gamereport[stat][jid] - stats = {'civ': 'civs', 'foodGathered': 'foodGathered', 'foodUsed': 'foodUsed', - 'woodGathered': 'woodGathered', 'woodUsed': 'woodUsed', - 'stoneGathered': 'stoneGathered', 'stoneUsed': 'stoneUsed', - 'metalGathered': 'metalGathered', 'metalUsed': 'metalUsed'} + singleStats = {'timeElapsed', 'mapName', 'teamsLocked', 'matchID'} + totalScoreStats = {'economyScore', 'militaryScore', 'totalScore'} + resourceStats = {'foodGathered', 'foodUsed', 'woodGathered', 'woodUsed', + 'stoneGathered', 'stoneUsed', 'metalGathered', 'metalUsed', 'vegetarianFoodGathered', + 'treasuresCollected', 'tributesSent', 'tributesReceived'} + unitsStats = {'totalUnitsTrained', 'totalUnitsLost', 'enemytotalUnitsKilled', 'infantryUnitsTrained', + 'infantryUnitsLost', 'enemyInfantryUnitsKilled', 'workerUnitsTrained', 'workerUnitsLost', + 'enemyWorkerUnitsKilled', 'femaleUnitsTrained', 'femaleUnitsLost', 'enemyFemaleUnitsKilled', + 'cavalryUnitsTrained', 'cavalryUnitsLost', 'enemyCavalryUnitsKilled', 'championUnitsTrained', + 'championUnitsLost', 'enemyChampionUnitsKilled', 'heroUnitsTrained', 'heroUnitsLost', + 'enemyHeroUnitsKilled', 'shipUnitsTrained', 'shipUnitsLost', 'enemyShipUnitsKilled'} + buildingsStats = {'totalBuildingsConstructed', 'totalBuildingsLost', 'enemytotalBuildingsDestroyed', + 'civCentreBuildingsConstructed', 'civCentreBuildingsLost', 'enemyCivCentreBuildingsDestroyed', + 'houseBuildingsConstructed', 'houseBuildingsLost', 'enemyHouseBuildingsDestroyed', + 'economicBuildingsConstructed', 'economicBuildingsLost', 'enemyEconomicBuildingsDestroyed', + 'outpostBuildingsConstructed', 'outpostBuildingsLost', 'enemyOutpostBuildingsDestroyed', + 'militaryBuildingsConstructed', 'militaryBuildingsLost', 'enemyMilitaryBuildingsDestroyed', + 'fortressBuildingsConstructed', 'fortressBuildingsLost', 'enemyFortressBuildingsDestroyed', + 'wonderBuildingsConstructed', 'wonderBuildingsLost', 'enemyWonderBuildingsDestroyed'} + marketStats = {'woodBought', 'foodBought', 'stoneBought', 'metalBought', 'tradeIncome'} + miscStats = {'civs', 'teams', 'percentMapExplored'} + stats = totalScoreStats | resourceStats | unitsStats | buildingsStats | marketStats | miscStats playerInfos = [] for player in players: jid = player.jid playerinfo = PlayerInfo(player=player) - for dbname, reportname in stats.items(): - setattr(playerinfo, dbname, get(reportname, jid)) + for reportname in stats: + setattr(playerinfo, reportname, get(reportname, jid)) playerInfos.append(playerinfo) - game = Game(map=gamereport['mapName'], duration=int(gamereport['timeElapsed'])) + game = Game(map=gamereport['mapName'], duration=int(gamereport['timeElapsed']), teamsLocked=bool(gamereport['teamsLocked']), matchID=gamereport['matchID']) game.players.extend(players) game.player_info.extend(playerInfos) game.winner = db.query(Player).filter_by(jid=winning_jid).first() db.add(game) db.commit() return game def verifyGame(self, gamereport): """ Returns a boolean based on whether the game should be rated. Here, we can specify the criteria for rated games. """ winning_jids = list(dict.keys({jid: state for jid, state in gamereport['playerStates'].items() if state == 'won'})) # We only support 1v1s right now. TODO: Support team games. if len(winning_jids) * 2 > len(dict.keys(gamereport['playerStates'])): # More than half the people have won. This is not a balanced team game or duel. return False if len(dict.keys(gamereport['playerStates'])) != 2: return False return True def rateGame(self, game): """ Takes a game with 2 players and alters their ratings based on the result of the game. Returns self. Side effects: Changes the game's players' ratings in the database. """ player1 = game.players[0] player2 = game.players[1] # TODO: Support draws. Since it's impossible to draw in the game currently, # the database model, and therefore this code, requires a winner. # The Elo implementation does not, however. result = 1 if player1 == game.winner else -1 # Player's ratings are -1 unless they have played a rated game. if player1.rating == -1: player1.rating = leaderboard_default_rating if player2.rating == -1: player2.rating = leaderboard_default_rating rating_adjustment1 = int(get_rating_adjustment(player1.rating, player2.rating, len(player1.games), len(player2.games), result)) rating_adjustment2 = int(get_rating_adjustment(player2.rating, player1.rating, len(player2.games), len(player1.games), result * -1)) if result == 1: resultQualitative = "won" elif result == 0: resultQualitative = "drew" else: resultQualitative = "lost" name1 = '@'.join(player1.jid.split('@')[:-1]) name2 = '@'.join(player2.jid.split('@')[:-1]) self.lastRated = "A rated game has ended. %s %s against %s. Rating Adjustment: %s (%s -> %s) and %s (%s -> %s)."%(name1, resultQualitative, name2, name1, player1.rating, player1.rating + rating_adjustment1, name2, player2.rating, player2.rating + rating_adjustment2) player1.rating += rating_adjustment1 player2.rating += rating_adjustment2 db.commit() return self - + def getLastRatedMessage(self): """ Gets the string of the last rated game. Triggers an update chat for the bot. """ return self.lastRated - + def addAndRateGame(self, gamereport): """ Calls addGame and if the game has only two players, also calls rateGame. Returns the result of addGame. """ game = self.addGame(gamereport) if game and self.verifyGame(gamereport): self.rateGame(game) else: self.lastRated = "" return game def getBoard(self): """ Returns a dictionary of player rankings to JIDs for sending. """ board = {} players = db.query(Player).order_by(Player.rating.desc()).limit(100).all() for rank, player in enumerate(players): # Don't send uninitialized ratings. if player.rating == -1: continue board[player.jid] = {'name': '@'.join(player.jid.split('@')[:-1]), 'rating': str(player.rating)} return board def getRatingList(self, nicks): """ Returns a rating list of players currently in the lobby by nick because the client can't link JID to nick conveniently. """ ratinglist = {} for JID in nicks.keys(): players = db.query(Player).filter_by(jid=str(JID)) if players.first(): if players.first().rating == -1: ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''} else: ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': str(players.first().rating)} else: ratinglist[nicks[JID]] = {'name': nicks[JID], 'rating': ''} return ratinglist ## Class to tracks all games in the lobby ## class GameList(): def __init__(self): self.gameList = {} def addGame(self, JID, data): """ Add a game """ data['players-init'] = data['players'] data['nbp-init'] = data['nbp'] data['state'] = 'init' self.gameList[str(JID)] = data def removeGame(self, JID): """ Remove a game attached to a JID """ del self.gameList[str(JID)] def getAllGames(self): """ Returns all games """ return self.gameList def changeGameState(self, JID, data): """ Switch game state between running and waiting """ JID = str(JID) if JID in self.gameList: if self.gameList[JID]['nbp-init'] > data['nbp']: logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'waiting') self.gameList[JID]['nbp'] = data['nbp'] self.gameList[JID]['state'] = 'waiting' else: logging.debug("change game (%s) state from %s to %s", JID, self.gameList[JID]['state'], 'running') self.gameList[JID]['nbp'] = data['nbp'] self.gameList[JID]['state'] = 'running' ## Class which manages different game reports from clients ## ## and calls leaderboard functions as appropriate. ## class ReportManager(): def __init__(self, leaderboard): self.leaderboard = leaderboard self.interimReportTracker = [] self.interimJIDTracker = [] def addReport(self, JID, rawGameReport): """ Adds a game to the interface between a raw report and the leaderboard database. """ # cleanRawGameReport is a copy of rawGameReport with all reporter specific information removed. cleanRawGameReport = rawGameReport.copy() del cleanRawGameReport["playerID"] if cleanRawGameReport not in self.interimReportTracker: # Store the game. appendIndex = len(self.interimReportTracker) self.interimReportTracker.append(cleanRawGameReport) # Initilize the JIDs and store the initial JID. numPlayers = self.getNumPlayers(rawGameReport) JIDs = [None] * numPlayers if numPlayers - int(rawGameReport["playerID"]) > -1: JIDs[int(rawGameReport["playerID"])-1] = str(JID) self.interimJIDTracker.append(JIDs) else: # We get the index at which the JIDs coresponding to the game are stored. index = self.interimReportTracker.index(cleanRawGameReport) # We insert the new report JID into the acending list of JIDs for the game. JIDs = self.interimJIDTracker[index] if len(JIDs) - int(rawGameReport["playerID"]) > -1: JIDs[int(rawGameReport["playerID"])-1] = str(JID) self.interimJIDTracker[index] = JIDs self.checkFull() def expandReport(self, rawGameReport, JIDs): """ Takes an raw game report and re-formats it into Python data structures leaving JIDs empty. Returns a processed gameReport of type dict. """ processedGameReport = {} for key in rawGameReport: if rawGameReport[key].find(",") == -1: processedGameReport[key] = rawGameReport[key] else: split = rawGameReport[key].split(",") # Remove the false split positive. split.pop() # We just delete gaia for now. split.pop(0) statToJID = {} for i, part in enumerate(split): statToJID[JIDs[i]] = part processedGameReport[key] = statToJID return processedGameReport def checkFull(self): """ Searches internal database to check if enough reports have been submitted to add a game to the leaderboard. If so, the report will be interpolated and addAndRateGame will be called with the result. """ i = 0 length = len(self.interimReportTracker) while(i < length): numPlayers = self.getNumPlayers(self.interimReportTracker[i]) numReports = 0 for JID in self.interimJIDTracker[i]: if JID != None: numReports += 1 if numReports == numPlayers: self.leaderboard.addAndRateGame(self.expandReport(self.interimReportTracker[i], self.interimJIDTracker[i])) del self.interimJIDTracker[i] del self.interimReportTracker[i] length -= 1 else: i += 1 self.leaderboard.lastRated = "" def getNumPlayers(self, rawGameReport): """ Computes the number of players in a raw gameReport. Returns int, the number of players. """ # Find a key in the report which holds values for multiple players. for key in rawGameReport: if rawGameReport[key].find(",") != -1: # Count the number of values, minus one for gaia and one for the false split positive. return len(rawGameReport[key].split(","))-2 # Return -1 in case of failure. return -1 ## Class for custom gamelist stanza extension ## class GameListXmppPlugin(ElementBase): name = 'query' namespace = 'jabber:iq:gamelist' interfaces = set(('game', 'command')) sub_interfaces = interfaces plugin_attrib = 'gamelist' def addGame(self, data): itemXml = ET.Element("game", data) self.xml.append(itemXml) def getGame(self): """ Required to parse incoming stanzas with this extension. """ game = self.xml.find('{%s}game' % self.namespace) data = {} for key, item in game.items(): data[key] = item return data ## Class for custom boardlist and ratinglist stanza extension ## class BoardListXmppPlugin(ElementBase): name = 'query' namespace = 'jabber:iq:boardlist' interfaces = set(('board', 'command')) sub_interfaces = interfaces plugin_attrib = 'boardlist' def addCommand(self, command): commandXml = ET.fromstring("%s" % command) self.xml.append(commandXml) def addItem(self, name, rating): itemXml = ET.Element("board", {"name": name, "rating": rating}) self.xml.append(itemXml) ## Class for custom gamereport stanza extension ## class GameReportXmppPlugin(ElementBase): name = 'report' namespace = 'jabber:iq:gamereport' plugin_attrib = 'gamereport' interfaces = ('game') sub_interfaces = interfaces def getGame(self): """ Required to parse incoming stanzas with this extension. """ game = self.xml.find('{%s}game' % self.namespace) data = {} for key, item in game.items(): data[key] = item return data ## Main class which handles IQ data and sends new data ## class XpartaMuPP(sleekxmpp.ClientXMPP): """ A simple list provider """ def __init__(self, sjid, password, room, nick): sleekxmpp.ClientXMPP.__init__(self, sjid, password) self.sjid = sjid self.room = room self.nick = nick # Game collection self.gameList = GameList() # Init leaderboard object self.leaderboard = LeaderboardList(room) # gameReport to leaderboard abstraction self.reportManager = ReportManager(self.leaderboard) # Store mapping of nicks and XmppIDs, attached via presence stanza self.nicks = {} self.lastLeft = "" register_stanza_plugin(Iq, GameListXmppPlugin) register_stanza_plugin(Iq, BoardListXmppPlugin) register_stanza_plugin(Iq, GameReportXmppPlugin) self.register_handler(Callback('Iq Gamelist', StanzaPath('iq/gamelist'), self.iqhandler, instream=True)) self.register_handler(Callback('Iq Boardlist', StanzaPath('iq/boardlist'), self.iqhandler, instream=True)) self.register_handler(Callback('Iq GameReport', StanzaPath('iq/gamereport'), self.iqhandler, instream=True)) self.add_event_handler("session_start", self.start) self.add_event_handler("muc::%s::got_online" % self.room, self.muc_online) self.add_event_handler("muc::%s::got_offline" % self.room, self.muc_offline) self.add_event_handler("groupchat_message", self.muc_message) def start(self, event): """ Process the session_start event """ self.plugin['xep_0045'].joinMUC(self.room, self.nick) self.send_presence() self.get_roster() logging.info("XpartaMuPP started") def muc_online(self, presence): """ Process presence stanza from a chat room. """ if presence['muc']['nick'] != self.nick: # If it doesn't already exist, store player JID mapped to their nick. if str(presence['muc']['jid']) not in self.nicks: self.nicks[str(presence['muc']['jid'])] = presence['muc']['nick'] # Check the jid isn't already in the lobby. if str(presence['muc']['jid']) != self.lastLeft: - self.send_message(mto=presence['from'], mbody="Hello %s, welcome to the 0 A.D. lobby. Polish your weapons and get ready to fight!" %(presence['muc']['nick']), mtype='') # Send Gamelist to new player. self.sendGameList(presence['muc']['jid']) # Following two calls make sqlalchemy complain about using objects in the # incorrect thread. TODO: Figure out how to fix this. # Send Leaderboard to new player. #self.sendBoardList(presence['muc']['jid']) # Register on leaderboard. #self.leaderboard.getOrCreatePlayer(presence['muc']['jid']) logging.debug("Client '%s' connected with a nick of '%s'." %(presence['muc']['jid'], presence['muc']['nick'])) def muc_offline(self, presence): """ Process presence stanza from a chat room. """ # Clean up after a player leaves if presence['muc']['nick'] != self.nick: # Delete any games they were hosting. for JID in self.gameList.getAllGames(): if JID == str(presence['muc']['jid']): self.gameList.removeGame(JID) self.sendGameList() break # Remove them from the local player list. self.lastLeft = str(presence['muc']['jid']) if str(presence['muc']['jid']) in self.nicks: del self.nicks[str(presence['muc']['jid'])] def muc_message(self, msg): """ Process new messages from the chatroom. """ if msg['mucnick'] != self.nick and self.nick.lower() in msg['body'].lower(): self.send_message(mto=msg['from'].bare, - mbody="I (%s) am the administrative bot in this lobby and cannot participate in any games." % self.nick, + mbody="I am the administrative bot in this lobby and cannot participate in any games.", mtype='groupchat') def iqhandler(self, iq): """ Handle the custom stanzas This method should be very robust because we could receive anything """ if iq['type'] == 'error': logging.error('iqhandler error' + iq['error']['condition']) #self.disconnect() elif iq['type'] == 'get': """ Request lists. """ # Send lists/register on leaderboard; depreciated once muc_online # can send lists/register automatically on joining the room. if 'gamelist' in iq.plugins: try: self.sendGameList(iq['from']) except: traceback.print_exc() logging.error("Failed to process gamelist request from %s" % iq['from'].bare) elif 'boardlist' in iq.plugins: command = iq['boardlist']['command'] if command == 'getleaderboard': try: self.leaderboard.getOrCreatePlayer(iq['from']) self.sendBoardList(iq['from']) except: traceback.print_exc() logging.error("Failed to process leaderboardlist request from %s" % iq['from'].bare) elif command == 'getratinglist': try: self.leaderboard.getOrCreatePlayer(iq['from']) self.sendRatingList(iq['from']) except: traceback.print_exc() logging.error("Failed to process ratinglist request from %s" % iq['from'].bare) else: logging.error("Failed to process boardlist request from %s" % iq['from'].bare) else: logging.error("Unknown 'get' type stanza request from %s" % iq['from'].bare) elif iq['type'] == 'result': """ Iq successfully received """ pass elif iq['type'] == 'set': if 'gamelist' in iq.plugins: """ Register-update / unregister a game """ command = iq['gamelist']['command'] if command == 'register': # Add game try: self.gameList.addGame(iq['from'], iq['gamelist']['game']) self.sendGameList() except: traceback.print_exc() logging.error("Failed to process game registration data") elif command == 'unregister': # Remove game try: self.gameList.removeGame(iq['from']) self.sendGameList() except: traceback.print_exc() logging.error("Failed to process game unregistration data") elif command == 'changestate': # Change game status (waiting/running) try: self.gameList.changeGameState(iq['from'], iq['gamelist']['game']) self.sendGameList() except: traceback.print_exc() logging.error("Failed to process changestate data") else: logging.error("Failed to process command '%s' received from %s" % command, iq['from'].bare) elif 'gamereport' in iq.plugins: """ Client is reporting end of game statistics """ try: self.reportManager.addReport(iq['from'], iq['gamereport']['game']) if self.leaderboard.getLastRatedMessage() != "": self.send_message(mto=self.room, mbody=self.leaderboard.getLastRatedMessage(), mtype="groupchat", mnick=self.nick) self.sendBoardList() self.sendRatingList() except: traceback.print_exc() logging.error("Failed to update game statistics for %s" % iq['from'].bare) else: logging.error("Failed to process stanza type '%s' received from %s" % iq['type'], iq['from'].bare) def sendGameList(self, to = ""): """ Send a massive stanza with the whole game list. If no target is passed the gamelist is broadcasted to all clients. """ games = self.gameList.getAllGames() if to == "": for JID in self.nicks.keys(): stz = GameListXmppPlugin() ## Pull games and add each to the stanza for JIDs in games: g = games[JIDs] # Only send the games that are in the 'init' state and games # that are in the 'waiting' state which the receiving player is in. TODO if g['state'] == 'init' or (g['state'] == 'waiting' and self.nicks[str(JID)] in g['players-init']): stz.addGame(g) ## Set additional IQ attributes iq = self.Iq() iq['type'] = 'result' iq['to'] = JID iq.setPayload(stz) ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send game list") else: ## Check recipient exists if str(to) not in self.nicks: logging.error("No player with the XmPP ID '%s' known to send gamelist to." % str(to)) return stz = GameListXmppPlugin() ## Pull games and add each to the stanza for JIDs in games: g = games[JIDs] # Only send the games that are in the 'init' state and games # that are in the 'waiting' state which the receiving player is in. TODO if g['state'] == 'init' or (g['state'] == 'waiting' and self.nicks[str(to)] in g['players-init']): stz.addGame(g) ## Set additional IQ attributes iq = self.Iq() iq['type'] = 'result' iq['to'] = to iq.setPayload(stz) ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send game list") def sendBoardList(self, to = ""): """ Send the whole leaderboard list. If no target is passed the boardlist is broadcasted to all clients. """ ## Pull leaderboard data and add it to the stanza board = self.leaderboard.getBoard() stz = BoardListXmppPlugin() iq = self.Iq() iq['type'] = 'result' for i in board: stz.addItem(board[i]['name'], board[i]['rating']) stz.addCommand('boardlist') iq.setPayload(stz) if to == "": for JID in self.nicks.keys(): ## Set additional IQ attributes iq['to'] = JID ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send leaderboard list") else: ## Check recipient exists if str(to) not in self.nicks: logging.error("No player with the XmPP ID '%s' known to send boardlist to" % str(to)) return ## Set additional IQ attributes iq['to'] = to ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send leaderboard list") def sendRatingList(self, to = ""): """ Send the rating list. If no target is passed the rating list is broadcasted to all clients. """ ## Pull rating list data and add it to the stanza ratinglist = self.leaderboard.getRatingList(self.nicks) stz = BoardListXmppPlugin() iq = self.Iq() iq['type'] = 'result' for i in ratinglist: stz.addItem(ratinglist[i]['name'], ratinglist[i]['rating']) stz.addCommand('ratinglist') iq.setPayload(stz) if to == "": for JID in self.nicks.keys(): ## Set additional IQ attributes iq['to'] = JID ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send rating list") else: ## Check recipient exists if str(to) not in self.nicks: logging.error("No player with the XmPP ID '%s' known to send ratinglist to" % str(to)) return ## Set additional IQ attributes iq['to'] = to ## Try sending the stanza try: iq.send(block=False, now=True) except: logging.error("Failed to send rating list") ## Main Program ## if __name__ == '__main__': # Setup the command line arguments. optp = OptionParser() # Output verbosity options. optp.add_option('-q', '--quiet', help='set logging to ERROR', action='store_const', dest='loglevel', const=logging.ERROR, default=logging.INFO) optp.add_option('-d', '--debug', help='set logging to DEBUG', action='store_const', dest='loglevel', const=logging.DEBUG, default=logging.INFO) optp.add_option('-v', '--verbose', help='set logging to COMM', action='store_const', dest='loglevel', const=5, default=logging.INFO) # XpartaMuPP configuration options optp.add_option('-m', '--domain', help='set xpartamupp domain', action='store', dest='xdomain', default="lobby.wildfiregames.com") optp.add_option('-l', '--login', help='set xpartamupp login', action='store', dest='xlogin', default="xpartamupp") optp.add_option('-p', '--password', help='set xpartamupp password', action='store', dest='xpassword', default="XXXXXX") optp.add_option('-n', '--nickname', help='set xpartamupp nickname', action='store', dest='xnickname', default="WFGbot") optp.add_option('-r', '--room', help='set muc room to join', action='store', dest='xroom', default="arena") opts, args = optp.parse_args() # Setup logging. logging.basicConfig(level=opts.loglevel, format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%Y-%m-%d %H:%M:%S') # XpartaMuPP xmpp = XpartaMuPP(opts.xlogin+'@'+opts.xdomain+'/CC', opts.xpassword, opts.xroom+'@conference.'+opts.xdomain, opts.xnickname) xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0004') # Data Forms xmpp.register_plugin('xep_0045') # Multi-User Chat # used xmpp.register_plugin('xep_0060') # PubSub xmpp.register_plugin('xep_0199') # XMPP Ping if xmpp.connect(): xmpp.process(threaded=False) else: logging.error("Unable to connect")