NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tiberium Alliances Report Stats // @version 0.5.3 // @namespace https://openuserjs.org/users/petui // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html // @author petui // @description Calculates combined RT and CP costs and loot of multiple combat reports // @include http*://prodgame*.alliances.commandandconquer.com/*/index.aspx* // @updateURL https://openuserjs.org/meta/petui/Tiberium_Alliances_Report_Stats.meta.js // ==/UserScript== 'use strict'; (function() { var main = function() { 'use strict'; function createReportStats() { console.log('ReportStats loaded'); qx.Class.define('ReportStats', { type: 'singleton', extend: qx.core.Object, statics: { BaseInfoExtraWidth: 6, // width to add to BaseInfoWindow to get rid of horizontal scroll bar StatusbarHeight: 35, // height to add to BaseInfoWindow to accomodate statusbar being visible CheckboxColumnWidth: 28, ResourceTypes: {} }, defer: function(statics) { var fileManager = ClientLib.File.FileManager.GetInstance(); statics.ResourceTypes[ClientLib.Base.EResourceType.Tiberium] = fileManager.GetPhysicalPath('ui/common/icn_res_tiberium.png'); statics.ResourceTypes[ClientLib.Base.EResourceType.Crystal] = fileManager.GetPhysicalPath('ui/common/icn_res_chrystal.png'); statics.ResourceTypes[ClientLib.Base.EResourceType.Gold] = fileManager.GetPhysicalPath('ui/common/icn_res_dollar.png'); statics.ResourceTypes[ClientLib.Base.EResourceType.Power] = fileManager.GetPhysicalPath('ui/common/icn_res_power.png'); statics.ResourceTypes[ClientLib.Base.EResourceType.ResearchPoints] = fileManager.GetPhysicalPath('ui/common/icn_res_research.png'); }, members: { reportsLoading: [], reportsLoaded: [], skipBaseInfoReportsReload: 0, initialize: function() { this.initializeHacks(); this.initializeUserInterface(); }, initializeHacks: function() { var source; if (typeof qx.ui.table.model.Abstract.prototype.addColumn !== 'function') { source = qx.ui.table.model.Abstract.prototype.getColumnId.toString(); var columnIdsMemberName = source.match(/return this\.([A-Za-z_]+)\[[A-Z]\];/)[1]; source = qx.ui.table.model.Abstract.prototype.getColumnName.toString(); var columnNamesMemberName = source.match(/return this\.([A-Za-z_]+)\[[A-Z]\];/)[1]; /** * @param {String} id * @param {String} name * @returns {Number} */ qx.ui.table.model.Abstract.prototype.addColumn = function(id, name) { var columnIndex = this[columnIdsMemberName].push(id) - 1; this[columnNamesMemberName].push(name); this.fireEvent('metaDataChanged'); return columnIndex; }; } if (typeof qx.ui.table.columnmodel.Basic.prototype.addColumn !== 'function') { source = qx.ui.table.columnmodel.Basic.prototype.getColumnWidth.toString(); var columnsMemberName = source.match(/return this\.([A-Za-z_]+)\[[A-Z]\]\.width;/)[1]; source = qx.ui.table.columnmodel.Basic.prototype.getOverallColumnCount.toString(); var columnOrderMemberName = source.match(/return this\.([A-Za-z_]+)\.length;/)[1]; source = qx.ui.table.columnmodel.Basic.prototype.getVisibleColumnAtX.toString(); var columnVisibilityMemberName = source.match(/return this\.([A-Za-z_]+)\[[A-Z]\];/)[1]; source = qx.ui.table.columnmodel.Basic.prototype._getColToXPosMap.toString(); var columnToXPosMapMemberName = source.match(/return this\.([A-Za-z_]+);\}$/)[1]; source = qx.ui.table.columnmodel.Basic.prototype.init.toString(); var matches = source.match(/this\.([A-Za-z_]+)\|\|\(this\.\1=new qx\.ui\.table\.columnmodel\.Basic\.DEFAULT_HEADER_RENDERER\(\)\);.+this\.([A-Za-z_]+)\|\|\(this\.\2=new qx\.ui\.table\.columnmodel\.Basic\.DEFAULT_DATA_RENDERER\(\)\);.+this\.([A-Za-z_]+)\|\|\(this\.\3=new qx\.ui\.table\.columnmodel\.Basic\.DEFAULT_EDITOR_FACTORY\(\)\);/); var headerRendererMemberName = matches[1]; var dataRendererMemberName = matches[2]; var editorFactoryMemberName = matches[3]; /** * @param {Boolean} visible * @returns {Number} */ qx.ui.table.columnmodel.Basic.prototype.addColumn = function(visible) { var columnIndex = this[columnsMemberName].push({ width: qx.ui.table.columnmodel.Basic.DEFAULT_WIDTH, headerRenderer: this[headerRendererMemberName], dataRenderer: this[dataRendererMemberName], editorFactory: this[editorFactoryMemberName] }) - 1; this[columnToXPosMapMemberName] = null; this[columnOrderMemberName].push(columnIndex); if (!visible) { this[columnVisibilityMemberName].push(columnIndex); } this.setColumnVisible(columnIndex, visible); return columnIndex; }; } if (typeof webfrontend.gui.info.BaseInfoWindow.prototype.onCellClick !== 'function') { source = Function.prototype.toString.call(webfrontend.gui.info.BaseInfoWindow.constructor); var createOutgoingTabMethodName = source.match(/;[A-Za-z]+\.add\(this\.([A-Za-z_]+)\(\)\);this\.[A-Za-z_]+=new webfrontend\.gui\.widgets\.confirmationWidgets\.ProtectionConfirmationWidget\(\);/)[1]; source = webfrontend.gui.info.BaseInfoWindow.prototype[createOutgoingTabMethodName].toString(); var onCellClickMethodName = source.match(/([A-Za-z]+)\.set\(\{statusBarVisible:false,columnVisibilityButtonVisible:false\}\);\1\.addListener\([A-Za-z]+,this\.([A-Za-z_]+),this\.[A-Za-z_]+\);/)[2]; webfrontend.gui.info.BaseInfoWindow.prototype.onCellClick = webfrontend.gui.info.BaseInfoWindow.prototype[onCellClickMethodName]; } if (typeof webfrontend.gui.info.BaseInfoWindow.prototype.onTotalUnreadCountUpdated !== 'function') { source = webfrontend.gui.info.BaseInfoWindow.prototype._onClose.toString(); var onTotalUnreadCountUpdatedMethodName = source.match(/ClientLib\.Data\.Reports\.TotalUnreadCountUpdated,this,this\.([A-Za-z_]+)\);/)[1]; webfrontend.gui.info.BaseInfoWindow.prototype.onTotalUnreadCountUpdated = webfrontend.gui.info.BaseInfoWindow.prototype[onTotalUnreadCountUpdatedMethodName]; var context = this; webfrontend.gui.info.BaseInfoWindow.prototype[onTotalUnreadCountUpdatedMethodName] = function() { return context.onTotalUnreadCountUpdated(this, arguments); }; } /* Detect and fix bug described in http://forum.alliances.commandandconquer.com/showthread.php?tid=30346 */ { source = ClientLib.Data.Reports.Reports.prototype.AddReport.toString(); var initMethodName = source.match(/break;\}\}[a-z]\.([A-Z]{6})\([a-z]\);if/)[1]; source = ClientLib.Data.Reports.CombatReport.prototype[initMethodName].toString(); var setDataMethodName = source.match(/this\.([A-Z]{6})\([A-Za-z]+\);/)[1]; source = ClientLib.Data.Reports.CombatReport.prototype[setDataMethodName].toString(); var matches = source.match(/this\.([A-Z]{6})=([a-z])\.abl;this\.[A-Z]{6}=\2\.abl;/); if (matches !== null) { var attackerBaseIdMemberName = matches[1]; var original = ClientLib.Data.Reports.CombatReport.prototype[setDataMethodName]; ClientLib.Data.Reports.CombatReport.prototype[setDataMethodName] = function(data) { original.call(this, data); this[attackerBaseIdMemberName] = data.d.abi; }; } else { console.warn('ReportStats::initializeHacks', 'Unable to patch ClientLib.Data.Reports.CombatReport.prototype.' + setDataMethodName + '. Its likely already fixed in the game code.'); } } if (typeof qx.ui.table.Table.prototype.getLastFocusedRow !== 'function') { qx.ui.table.Table.prototype.lastFocusedRow = null; var originalSetFocusedCell = qx.ui.table.Table.prototype.setFocusedCell; qx.ui.table.Table.prototype.setFocusedCell = function() { this.lastFocusedRow = this.getFocusedRow(); originalSetFocusedCell.apply(this, arguments); }; /** * @returns {Number} */ qx.ui.table.Table.prototype.getLastFocusedRow = function() { return this.lastFocusedRow; }; } }, initializeUserInterface: function() { var baseInfoWindow = webfrontend.gui.info.BaseInfoWindow.getInstance(); var tabs = baseInfoWindow.getChildren()[0].getChildren(); for (var tabIndex = 1; tabIndex <= 2; tabIndex++) { var table = tabs[tabIndex].getChildren()[0]; var tableModel = table.getTableModel(); var tableModelIndex = tableModel.addColumn('ReportStatsCheckbox', ''); tableModel.setColumnSortable(tableModelIndex, false); tableModel.addListener('dataChanged', this.onTableModelDataChange, this); tableModel.setUserData('checkboxColumnIndex', tableModelIndex); var columnModel = table.getTableColumnModel(); var columnModelIndex = columnModel.addColumn(true); columnModel.setDataCellRenderer(columnModelIndex, new qx.ui.table.cellrenderer.Boolean()); columnModel.setColumnWidth(columnModelIndex, ReportStats.CheckboxColumnWidth); columnModel.moveColumn(columnModelIndex, 0); var cellClickEventName = PerforceChangelist >= 434241 ? 'cellTap' : 'cellClick'; table.removeListener(cellClickEventName, baseInfoWindow.onCellClick, tableModel); table.addListener(cellClickEventName, this.onCellClickDelegate, this); table.getChildControl('statusbar').set({ height: ReportStats.StatusbarHeight, rich: true, toolTip: new qx.ui.tooltip.ToolTip().set({ label: '<div>"Loot" is the sum of resources gained from destruction, plunder and own repair costs.</div><br/>' + '<div>Tip: You can select multiple reports at once by holding down the Shift key.</div>', rich: true }) }); } baseInfoWindow.setWidth(baseInfoWindow.getWidth() + ReportStats.CheckboxColumnWidth + ReportStats.BaseInfoExtraWidth); baseInfoWindow.setHeight(baseInfoWindow.getHeight() + ReportStats.StatusbarHeight); }, /** * Sets checkbox value to false for rows being initialized * @param {qx.event.type.Data} event */ onTableModelDataChange: function(event) { var data = event.getData(); if (data.firstColumn !== 0) { return; } var tableModel = event.getTarget(); var columnIndex = tableModel.getUserData('checkboxColumnIndex'); var columnId = tableModel.getColumnId(columnIndex); for (var row = data.firstRow; row <= data.lastRow; row++) { var rowData = tableModel.getRowData(row); if (rowData && rowData[columnId] === undefined) { rowData[columnId] = false; } } tableModel.fireDataEvent('dataChanged', { firstRow: data.firstRow, lastRow: data.lastRow, firstColumn: columnIndex, lastColumn: columnIndex }); if (this.isReportTab(this.getCurrentBaseInfoTab())) { this.calculateCombinedRepairCosts(tableModel); } }, /** * @param {qx.ui.table.pane.CellEvent} event */ onCellClickDelegate: function(event) { var tableModel = event.getTarget().getTable().getTableModel(); if (event.getColumn() === tableModel.getUserData('checkboxColumnIndex') && tableModel.getRowData(event.getRow())) { this.onCheckboxClick(event); } else { webfrontend.gui.info.BaseInfoWindow.prototype.onCellClick.call(tableModel, event); } }, /** * @param {qx.ui.table.pane.CellEvent} event */ onCheckboxClick: function(event) { var table = event.getTarget().getTable(); var tableModel = table.getTableModel(); var newValue = !tableModel.getValue(event.getColumn(), event.getRow()); if (event.isShiftPressed() && table.getLastFocusedRow() !== null) { var start = Math.min(event.getRow(), table.getLastFocusedRow()); var end = Math.max(event.getRow(), table.getLastFocusedRow()); for (var row = start; row <= end; row++) { tableModel.setValue(event.getColumn(), row, newValue); } } else { tableModel.setValue(event.getColumn(), event.getRow(), newValue); } this.calculateCombinedRepairCosts(tableModel); }, /** * @param {webfrontend.data.ReportHeaderDataModel} tableModel */ calculateCombinedRepairCosts: function(tableModel) { var wasLoading = this.reportsLoading.length > 0; this.reportsLoading = []; this.reportsLoaded = []; var rowCount = tableModel.getRowCount(); for (var row = 0; row < rowCount; row++) { var rowData = tableModel.getRowData(row); if (rowData && rowData.ReportStatsCheckbox) { this.reportsLoading.push(rowData.Id); } } if (this.reportsLoading.length > 0) { var reports = ClientLib.Data.MainData.GetInstance().get_Reports(); if (!wasLoading) { phe.cnc.Util.attachNetEvent(reports, 'ReportDelivered', ClientLib.Data.Reports.ReportDelivered, this, this.onReportDelivered); } for (var i = this.reportsLoading.length - 1; i >= 0; i--) { reports.RequestReportData(this.reportsLoading[i]); } if (this.reportsLoading.length > 0) { var table = this.getCurrentBaseInfoTab().getChildren()[0]; table.getChildControl('statusbar').setValue('Please wait...'); } } else { this.onAllReportsLoaded(); } }, /** * @param {webfrontend.gui.info.BaseInfoWindow} baseInfoWindow * @param {Object} parameters */ onTotalUnreadCountUpdated: function(baseInfoWindow, parameters) { if (!this.skipBaseInfoReportsReload) { baseInfoWindow.onTotalUnreadCountUpdated.apply(baseInfoWindow, parameters); } else { this.skipBaseInfoReportsReload--; } }, /** * @param {ClientLib.Data.Reports.CombatReport} report */ onReportDelivered: function(report) { var index = this.reportsLoading.indexOf(report.get_Id()); if (index !== -1) { this.reportsLoading.splice(index, 1); this.reportsLoaded.push(report); if (!this.reportsLoading.length) { this.onAllReportsLoaded(); } } if (!report.get_IsRead()) { report.set_IsRead(true); this.skipBaseInfoReportsReload++; } }, onAllReportsLoaded: function() { phe.cnc.Util.detachNetEvent(ClientLib.Data.MainData.GetInstance().get_Reports(), 'ReportDelivered', ClientLib.Data.Reports.ReportDelivered, this, this.onReportDelivered); var hasSelectedReports = this.reportsLoaded.length > 0; var table = this.getCurrentBaseInfoTab().getChildren()[0]; table.setStatusBarVisible(hasSelectedReports); if (hasSelectedReports) { var attackerBaseIds = []; var defenderBaseIds = []; var repairTimeCosts = 0; var minCommandPointCosts = 0; var maxCommandPointCosts = 0; var firstAttack = null; var lastAttack = 0; var loot = {}; var getTotalLootMethod, getRepairCostsMethod; if (this.reportsLoaded[0].get_PlayerReportType() === ClientLib.Data.Reports.EPlayerReportType.CombatOffense) { getTotalLootMethod = ClientLib.Data.Reports.CombatReport.prototype.GetAttackerTotalResourceReceived; getRepairCostsMethod = ClientLib.Data.Reports.CombatReport.prototype.GetAttackerRepairCosts; } else { getTotalLootMethod = ClientLib.Data.Reports.CombatReport.prototype.GetDefenderTotalResourceCosts; getRepairCostsMethod = ClientLib.Data.Reports.CombatReport.prototype.GetDefenderRepairCosts; } var server = ClientLib.Data.MainData.GetInstance().get_Server(); var combatCostMinimum = server.get_CombatCostMinimum(); var combatCostMinimumPvP = server.get_UsesRebalancingI() ? server.get_PvPCombatCostMinimum() : combatCostMinimum; var combatCostPerFieldInside = server.get_CombatCostPerField(); var combatCostPerFieldOutside = server.get_CombatCostPerFieldOutsideTerritory(); for (var i = 0; i < this.reportsLoaded.length; i++) { var report = this.reportsLoaded[i]; if (!(report instanceof ClientLib.Data.Reports.CombatReport)) { continue; } if (attackerBaseIds.indexOf(report.get_AttackerBaseId()) === -1) { attackerBaseIds.push(report.get_AttackerBaseId()); } if (defenderBaseIds.indexOf(report.get_DefenderBaseId()) === -1) { defenderBaseIds.push(report.get_DefenderBaseId()); } repairTimeCosts += report.GetAttackerMaxRepairTime(); var distance = Math.sqrt( Math.pow(report.get_AttackerBaseXCoord() - report.get_DefenderBaseXCoord(), 2) + Math.pow(report.get_AttackerBaseYCoord() - report.get_DefenderBaseYCoord(), 2) ); switch (report.get_Type()) { case ClientLib.Data.Reports.EReportType.Combat: var isFriendlyTerritory = report.get_AttackerAllianceName() === report.get_DefenderAllianceName(); var cost = Math.floor(combatCostMinimumPvP + (isFriendlyTerritory ? combatCostPerFieldInside : combatCostPerFieldOutside) * distance); minCommandPointCosts += cost; maxCommandPointCosts += cost; break; case ClientLib.Data.Reports.EReportType.NPCRaid: switch (parseInt(report.get_DefenderBaseName(), 10)) { case ClientLib.Data.Reports.ENPCCampType.Base: case ClientLib.Data.Reports.ENPCCampType.Fortress: var cost = Math.floor(combatCostMinimum + combatCostPerFieldOutside * distance); minCommandPointCosts += cost; maxCommandPointCosts += cost; break; default: minCommandPointCosts += Math.floor(combatCostMinimum + combatCostPerFieldInside * distance); maxCommandPointCosts += Math.floor(combatCostMinimum + combatCostPerFieldOutside * distance); } break; case ClientLib.Data.Reports.EReportType.NPCPlayerCombat: // No repair time or command point cost for Forgotten attacks break; default: throw 'Unexpected report type (' + report.get_Type() + ')'; } if (firstAttack === null || report.get_Time() < firstAttack) { firstAttack = report.get_Time(); } if (report.get_Time() > lastAttack) { lastAttack = report.get_Time(); } for (var resourceType in ReportStats.ResourceTypes) { var resourceCount = getTotalLootMethod.call(report, resourceType) - getRepairCostsMethod.call(report, resourceType); if (resourceCount !== 0) { if (!(resourceType in loot)) { loot[resourceType] = 0; } loot[resourceType] += resourceCount; } } } var lootRow = 'Loot:'; for (var resourceType in loot) { lootRow += ' <img width="17" height="17" src="' + ReportStats.ResourceTypes[resourceType] + '" style="vertical-align: text-bottom;"/>'; if (loot[resourceType] < 0) { lootRow += '<span style="color: #d00;">' + phe.cnc.gui.util.Numbers.formatNumbersCompact(loot[resourceType]) + '</span>'; } else { lootRow += phe.cnc.gui.util.Numbers.formatNumbersCompact(loot[resourceType]); } } table.getChildControl('statusbar').setValue( attackerBaseIds.length + ' attacker' + (attackerBaseIds.length === 1 ? '' : 's') + ', ' + defenderBaseIds.length + ' defender' + (defenderBaseIds.length === 1 ? '' : 's') + ', ' + this.reportsLoaded.length + ' attack' + (this.reportsLoaded.length === 1 ? '' : 's') + ', ' + phe.cnc.Util.getTimespanString(repairTimeCosts) + ' RT and ' + (minCommandPointCosts === maxCommandPointCosts ? minCommandPointCosts : (minCommandPointCosts + '-' + maxCommandPointCosts) ) + ' CPs spent' + (this.reportsLoaded.length > 1 ? ' in ' + phe.cnc.Util.getTimespanString((lastAttack - firstAttack) / 1000) : '' ) + '<br/>' + lootRow ); } }, /** * @returns {qx.ui.tabview.Page} */ getCurrentBaseInfoTab: function() { return webfrontend.gui.info.BaseInfoWindow.getInstance().getChildren()[0].getSelection()[0]; }, /** * @param {qx.ui.tabview.Page} tab * @returns {Boolean} */ isReportTab: function(tab) { var tabView = webfrontend.gui.info.BaseInfoWindow.getInstance().getChildren()[0]; var tabIndex = tabView.getChildren().indexOf(tab); return 1 <= tabIndex && tabIndex <= 2; } } }); } function waitForGame() { try { if (typeof qx !== 'undefined' && qx.core.Init.getApplication() && qx.core.Init.getApplication().initDone) { createReportStats(); ReportStats.getInstance().initialize(); } else { setTimeout(waitForGame, 1000); } } catch (e) { console.log('ReportStats: ', e.toString()); } } setTimeout(waitForGame, 1000); }; var script = document.createElement('script'); script.innerHTML = '(' + main.toString() + ')();'; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); })();