// ==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*
// ==/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);
})();