NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Tiberium Alliances ReplayShare // @version 0.4.1 // @namespace https://openuserjs.org/users/petui // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html // @author petui // @description Share combat reports with your friends in other alliances and worlds // @include http*://prodgame*.alliances.commandandconquer.com/*/index.aspx* // @updateURL https://openuserjs.org/meta/petui/Tiberium_Alliances_ReplayShare.meta.js // ==/UserScript== 'use strict'; (function() { var main = function() { 'use strict'; function createReplayShare() { console.log('ReplayShare loaded'); var Replay = function() {}; Replay.prototype.id = null; Replay.prototype.data = null; /** * @returns {Boolean} */ Replay.prototype.isNew = function() { return this.id === null; }; /** * @returns {String} */ Replay.prototype.getId = function() { return this.id; }; /** * @param {Number} id * @returns {Replay} */ Replay.prototype.setId = function(id) { this.id = id; return this; }; /** * @returns {Object} */ Replay.prototype.getData = function() { return this.data; }; /** * @param {Object} data * @returns {Replay} */ Replay.prototype.setData = function(data) { this.data = data; return this; }; /** * @param {Object} data * @returns {Boolean} */ Replay.prototype.equals = function(data) { return JSON.stringify(this.getData()) === JSON.stringify(data); }; qx.Class.define('ReplayShare', { type: 'singleton', extend: qx.core.Object, events: { lastReplayDataChange: 'qx.event.type.Data' }, members: { lastReplayData: null, window: null, initialize: function() { this.initializeHacks(); this.initializeEntryPoints(); }, initializeHacks: function() { if (ClientLib.Vis.Battleground.Battleground.prototype.LoadCombatDirect === undefined) { var onSimulateBattleMethodName = ClientLib.API.Battleground.prototype.SimulateBattle.toString() .match(/"SimulateBattle",\s?\{battleSetup:[a-z]+\},\s?\(new \$I\.[A-Z]{6}\)\.[A-Z]{6}\(this,this\.([A-Z]{6})\),\s?this\);/)[1]; var loadCombatDirectMethodName = ClientLib.API.Battleground.prototype[onSimulateBattleMethodName].toString() .match(/\$I\.[A-Z]{6}\.[A-Z]{6}\(\)\.[A-Z]{6}\(\)\.([A-Z]{6})\(b\.d\);/)[1]; ClientLib.Vis.Battleground.Battleground.prototype.LoadCombatDirect = ClientLib.Vis.Battleground.Battleground.prototype[loadCombatDirectMethodName]; } var source = ClientLib.Vis.Battleground.Battleground.prototype.LoadCombatDirect.toString(); var initCombatMethodName = source.match(/this\.([A-Z]{6})\(null,[a-z]\);}$/)[1]; var context = this; var originalInitCombat = ClientLib.Vis.Battleground.Battleground.prototype[initCombatMethodName]; ClientLib.Vis.Battleground.Battleground.prototype[initCombatMethodName] = function(extra, data) { originalInitCombat.call(this, extra, data); context.lastReplayData = data; context.fireDataEvent('lastReplayDataChange', data); }; var originalOpenLink = webfrontend.gui.Util.openLink; webfrontend.gui.Util.openLink = function(url) { if (!context.handleLink(url)) { originalOpenLink.apply(this, arguments); } }; }, initializeEntryPoints: function() { var scriptsButton = qx.core.Init.getApplication().getMenuBar().getScriptsButton(); scriptsButton.Add('ReplayShare', 'FactionUI/icons/icn_replay_speedup.png'); var children = scriptsButton.getMenu().getChildren(); var lastChild = children[children.length - 1]; lastChild.addListener('execute', this.openWindow, this); var shareButton = new qx.ui.form.Button('Share').set({ appearance: 'button-text-small', toolTipText: 'Open in ReplayShare', width: 80 }); shareButton.addListener('execute', this.onClickShare, this); qx.core.Init.getApplication().getReportReplayOverlay().add(shareButton, { right: 70, top: 35 }); }, /** * @param {String} key * @returns {Object} */ getConfig: function(key) { var config = JSON.parse(localStorage.getItem('ReplayShare')) || {}; return key in config ? config[key] : null; }, /** * @param {String} key * @param {Object} value */ setConfig: function(key, value) { var config = JSON.parse(localStorage.getItem('ReplayShare')) || {}; config[key] = value; localStorage.setItem('ReplayShare', JSON.stringify(config)); }, /** * @param {String} url * @returns {Boolean} */ handleLink: function(url) { var matches = url.match(/^https?:\/\/replayshare\.(?:parseapp\.com|petui\.net)\/([A-Za-z0-9]+)/); if (matches !== null) { var id = matches[1]; if (this.getConfig('dontAsk')) { this.openWindow(); this.window.download(id); } else { var context = this; var widget = new ReplayShare.ConfirmationWidget(url, function(dontAskAgain) { context.openWindow(); context.window.download(id); if (dontAskAgain) { context.setConfig('dontAsk', true); } }); widget.open(); } return true; } return false; }, openWindow: function() { if (this.window === null) { this.window = new ReplayShare.Window(this); } this.window.open(); }, onClickShare: function() { this.openWindow(); this.window.onClickFetchReplayData(); }, /** * @param {Object} replayData * @returns {Boolean} */ tryPlay: function(replayData) { qx.core.Init.getApplication().getPlayArea().setView(ClientLib.Data.PlayerAreaViewMode.pavmCombatReplay, -1, 0, 0); try { ClientLib.Vis.VisMain.GetInstance().get_Battleground().LoadCombatDirect(replayData); } catch (e) { console.log('ReplayShare::tryPlay', e.toString()); return false; } return true; }, /** * @returns {Boolean} */ hasLastReplayData: function() { return this.lastReplayData !== null; }, /** * @returns {Object} */ getLastReplayData: function() { return this.lastReplayData; } } }); qx.Class.define('ReplayShare.Window', { extend: qx.ui.window.Window, construct: function(replayShare) { qx.ui.window.Window.call(this); this.replayShare = replayShare; this.service = new ReplayShare.Service(); this.set({ caption: 'ReplayShare', icon: 'FactionUI/icons/icn_replay_speedup.png', contentPaddingTop: 0, contentPaddingBottom: 2, contentPaddingRight: 6, contentPaddingLeft: 6, showMaximize: false, showMinimize: false, allowMaximize: false, allowMinimize: false, resizable: true }); this.getChildControl('icon').set({ scale: true, width: 20, height: 12, alignY: 'middle' }); this.initializePosition(); this.addListener('move', this.onWindowMove, this); this.setLayout(new qx.ui.layout.VBox()); this.add(this.errorMessageLabel = new qx.ui.basic.Label().set({ font: 'font_size_13', textColor: '#e44', visibility: 'excluded' })); var createPlayerGroupBox = function(legend, factionImage, nameLabel, baseLabel, allianceLabel) { var nameContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(4)); nameContainer.add(factionImage.set({ width: 18, height: 18, scale: true })); nameContainer.add(nameLabel); var groupBox = new qx.ui.groupbox.GroupBox(legend); groupBox.setLayout(new qx.ui.layout.Grid(2, 2) .setColumnFlex(0, 1) .setColumnFlex(1, 9) ); groupBox.add(new qx.ui.basic.Label('Name:').set({ font: 'font_size_13_bold' }), { row: 0, column: 0 }); groupBox.add(nameContainer, { row: 0, column: 1 }); groupBox.add(new qx.ui.basic.Label('Base:').set({ font: 'font_size_13_bold' }), { row: 1, column: 0 }); groupBox.add(baseLabel, { row: 1, column: 1 }); groupBox.add(new qx.ui.basic.Label('Alliance:').set({ font: 'font_size_13_bold' }), { row: 2, column: 0 }); groupBox.add(allianceLabel, { row: 2, column: 1 }); return groupBox; }; var detailsContainer = new qx.ui.container.Composite(new qx.ui.layout.Flow()).set({ font: 'font_size_13', textColor: '#111' }); this.add(detailsContainer); detailsContainer.add(createPlayerGroupBox('Attacker', this.attackerFactionImage = new qx.ui.basic.Image(), this.attackerNameLabel = new qx.ui.basic.Label(), this.attackerBaseLabel = new qx.ui.basic.Label(), this.attackerAllianceLabel = new qx.ui.basic.Label() ).set({ width: 290 })); detailsContainer.add(createPlayerGroupBox('Defender', this.defenderFactionImage = new qx.ui.basic.Image(), this.defenderNameLabel = new qx.ui.basic.Label(), this.defenderBaseLabel = new qx.ui.basic.Label(), this.defenderAllianceLabel = new qx.ui.basic.Label() ).set({ width: 290 })); this.add(this.timeOfAttackLabel = new qx.ui.basic.Label().set({ alignX: 'right', textColor: '#aaa' })); var controlsContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(4)).set({ marginBottom: 4, marginLeft: 2, marginTop: 4 }); this.add(controlsContainer); this.fetchReplayDataButton = new qx.ui.form.Button('Fetch').set({ enabled: this.replayShare.hasLastReplayData(), toolTipText: 'Fetch most recently viewed replay or simulation' }); this.fetchReplayDataButton.addListener('execute', this.onClickFetchReplayData, this); controlsContainer.add(this.fetchReplayDataButton, { flex: 1 }); if (!this.replayShare.hasLastReplayData()) { this.replayShare.addListenerOnce('lastReplayDataChange', this.onLastReplayDataChanged, this); } this.watchReplayButton = new qx.ui.form.Button('Play').set({ enabled: false, toolTipText: 'Watch loaded replay' }); this.watchReplayButton.addListener('execute', this.onClickWatchReplay, this); controlsContainer.add(this.watchReplayButton, { flex: 1 }); this.uploadButton = new qx.ui.form.Button('Get link').set({ enabled: false, toolTipText: 'Get share link for loaded replay' }); this.uploadButton.addListener('execute', this.onClickUpload, this); controlsContainer.add(this.uploadButton, { flex: 1 }); }, statics: { DefaultWidth: 300, DefaultHeight: null }, members: { replayShare: null, service: null, sharePopup: null, errorMessageLabel: null, attackerFactionImage: null, attackerNameLabel: null, attackerBaseLabel: null, attackerAllianceLabel: null, defenderFactionImage: null, defenderNameLabel: null, defenderBaseLabel: null, defenderAllianceLabel: null, timeOfAttackLabel: null, fetchReplayDataButton: null, watchReplayButton: null, uploadButton: null, replay: null, initializePosition: function() { var bounds = this.replayShare.getConfig('bounds'); if (bounds === null) { var baseNavBarX = qx.core.Init.getApplication().getBaseNavigationBar().getLayoutParent().getBounds().left; bounds = { left: baseNavBarX - ReplayShare.Window.DefaultWidth - 16, top: 75, width: ReplayShare.Window.DefaultWidth, height: ReplayShare.Window.DefaultHeight }; } this.moveTo(bounds.left, bounds.top); this.setWidth(bounds.width); this.setHeight(bounds.height); }, /** * @param {qx.event.type.Data} event */ onWindowMove: function(event) { this.replayShare.setConfig('bounds', event.getData()); }, onLastReplayDataChanged: function() { this.fetchReplayDataButton.setEnabled(true); }, onClickFetchReplayData: function() { var replayData = this.replayShare.getLastReplayData(); replayData = JSON.parse(JSON.stringify(replayData)); // clone if (this.replay === null || !this.replay.equals(replayData)) { this.setReplay(new Replay().setData(replayData)); } }, onClickWatchReplay: function() { var replayData = this.replay.getData(); if (!this.replayShare.tryPlay(replayData)) { this.errorMessageLabel.setValue('Error: Invalid replay data'); this.errorMessageLabel.show(); } else { this.errorMessageLabel.exclude(); } }, onClickUpload: function() { this.openSharePopup(); if (this.replay.isNew()) { var context = this; this.service.save(this.replay, { success: function(replay) { context.sharePopup.setLinkURL(context.formatLinkUrl(replay.getId())); }, error: function(replay, error) { context.sharePopup.setError(error.message); } }); } else { this.sharePopup.setLinkURL(this.formatLinkUrl(this.replay.getId())); } }, /** * @param {String} id * @returns {String} */ formatLinkUrl: function scope(id) { if (scope.baseUrl === undefined) { // Start serving links to new host on 2016-11-01 12:00:00 UTC scope.baseUrl = Date.now() < Date.UTC(2016, 11, 1, 12, 0, 0) ? 'https://replayshare.parseapp.com/' : 'https://replayshare.petui.net/'; } return scope.baseUrl + id; }, /** * @param {Object} replayData */ setDetailsFromReplayData: function(replayData) { var isForgottenAttacker = replayData.af !== ClientLib.Base.EFactionType.GDIFaction && replayData.af !== ClientLib.Base.EFactionType.NODFaction; this.attackerFactionImage.setSource(phe.cnc.gui.util.Images.getFactionIcon(replayData.af)); this.attackerFactionImage.show(); this.attackerNameLabel.setValue(isForgottenAttacker ? this.tr('tnf:mutants') : replayData.apn); this.attackerBaseLabel.setValue(this.getReplayAttackerBaseName(replayData)); this.attackerAllianceLabel.setValue(isForgottenAttacker ? this.tr('tnf:mutants') : (replayData.aan || '-')); var isForgottenDefender = replayData.df !== ClientLib.Base.EFactionType.GDIFaction && replayData.df !== ClientLib.Base.EFactionType.NODFaction; this.defenderFactionImage.setSource(phe.cnc.gui.util.Images.getFactionIcon(isForgottenDefender ? ClientLib.Base.EFactionType.FORFaction : replayData.df)); this.defenderFactionImage.show(); this.defenderNameLabel.setValue(isForgottenDefender ? this.tr('tnf:mutants') : replayData.dpn); this.defenderBaseLabel.setValue(this.getReplayDefenderBaseName(replayData)); this.defenderAllianceLabel.setValue(isForgottenDefender ? this.tr('tnf:mutants') : (replayData.dan || '-')); this.timeOfAttackLabel.setValue(phe.cnc.Util.getDateTimeString(new Date(replayData.toa))); }, /** * @param {Object} replayData * @returns {String} */ getReplayAttackerBaseName: function(replayData) { var attackerBaseName; switch (replayData.af) { case ClientLib.Base.EFactionType.FORFaction: case ClientLib.Base.EFactionType.NPCBase: case ClientLib.Base.EFactionType.NPCCamp: case ClientLib.Base.EFactionType.NPCOutpost: case ClientLib.Base.EFactionType.NPCFortress: case ClientLib.Base.EFactionType.NPCEvent: attackerBaseName = this.tr(replayData.an) + ' (' + replayData.abl + ')'; break; default: attackerBaseName = replayData.an; } return attackerBaseName; }, /** * @param {Object} replayData * @returns {String} */ getReplayDefenderBaseName: function(replayData) { var defenderBaseName; switch (replayData.df) { case ClientLib.Base.EFactionType.FORFaction: case ClientLib.Base.EFactionType.NPCBase: case ClientLib.Base.EFactionType.NPCCamp: case ClientLib.Base.EFactionType.NPCOutpost: case ClientLib.Base.EFactionType.NPCFortress: case ClientLib.Base.EFactionType.NPCEvent: var defenderPlayerId = replayData.dpi; var type; switch (Math.abs(defenderPlayerId) % 100) { case ClientLib.Data.Reports.ENPCCampType.Beginner: case ClientLib.Data.Reports.ENPCCampType.Random: type = 'tnf:mutants camp'; break; case ClientLib.Data.Reports.ENPCCampType.Cluster: type = 'tnf:mutants outpost'; break; case ClientLib.Data.Reports.ENPCCampType.Fortress: type = 'tnf:centerhub short'; break; case ClientLib.Data.Reports.ENPCCampType.Event: type = 'tnf:event camp'; break; default: type = 'tnf:mutants base'; } defenderBaseName = this.tr(type) + ' (' + Math.floor(Math.abs(defenderPlayerId) / 100) + ')'; break; default: defenderBaseName = replayData.dn; } return defenderBaseName; }, /** * @param {String} id */ download: function(id) { this.errorMessageLabel.exclude(); this.resetFields('Loading...'); var context = this; this.service.get(id, { success: function(replay) { context.setReplay(replay); }, error: function(replay, error) { context.errorMessageLabel.setValue('Error: ' + error.message); context.errorMessageLabel.show(); if (context.replay !== null) { context.setDetailsFromReplayData(context.replay.getData()); } else { context.resetFields(null); } } }); }, /** * @param {Replay} replay */ setReplay: function(replay) { this.replay = replay; this.setDetailsFromReplayData(replay.getData()); this.watchReplayButton.setEnabled(true); this.uploadButton.setEnabled(true); }, openSharePopup: function() { if (this.sharePopup === null) { var bounds = this.getBounds(); this.sharePopup = new ReplayShare.Window.ShareLink(); this.sharePopup.moveTo( bounds.left - (this.sharePopup.getWidth() - bounds.width) / 2, bounds.top - (this.sharePopup.getHeight() - bounds.height) / 2 ); } this.sharePopup.open(); }, /** * @param {String} label */ resetFields: function(label) { this.attackerFactionImage.exclude(); this.attackerFactionImage.setSource(null); this.attackerNameLabel.setValue(label); this.attackerBaseLabel.setValue(label); this.attackerAllianceLabel.setValue(label); this.defenderFactionImage.exclude(); this.defenderFactionImage.setSource(null); this.defenderNameLabel.setValue(label); this.defenderBaseLabel.setValue(label); this.defenderAllianceLabel.setValue(label); this.timeOfAttackLabel.setValue(label); } } }); qx.Class.define('ReplayShare.Window.ShareLink', { extend: qx.ui.window.Window, construct: function() { qx.ui.window.Window.call(this); this.set({ caption: 'Share link', allowMaximize: false, allowMinimize: false, showMinimize: false, showMaximize: false, showClose: true, resizable: false, padding: 1, textColor: '#aaa', width: 378, height: 98 }); this.setLayout(new qx.ui.layout.VBox()); this.add(new qx.ui.basic.Label('Copy the link to share this replay with others')); this.add(this.linkField = new qx.ui.form.TextField().set({ readOnly: true, focusable: true, placeholder: 'Loading...' })); this.add(this.errorMessageLabel = new qx.ui.basic.Label().set({ textColor: '#e44', visibility: 'excluded' })); this.linkField.addListener('click', this.linkField.selectAllText, this.linkField); this.addListener('changeActive', this.onChangeActive, this); }, members: { linkField: null, errorMessageLabel: null, /** * @param {qx.event.type.Data} event */ onChangeActive: function(event) { if (!event.getData()) { this.close(); } }, open: function() { this.linkField.setValue(null); this.errorMessageLabel.exclude(); qx.ui.window.Window.prototype.open.call(this); }, /** * @param {String} url */ setLinkURL: function(url) { this.linkField.setValue('[url]' + url + '[/url]'); new qx.util.DeferredCall(function() { this.linkField.focus(); this.linkField.selectAllText(); }, this).schedule(); }, /** * @param {String} error */ setError: function(error) { this.errorMessageLabel.setValue(error); this.errorMessageLabel.show(); } } }); qx.Class.define('ReplayShare.ConfirmationWidget', { extend: webfrontend.gui.CustomWindow, construct: function(url, callback) { webfrontend.gui.CustomWindow.call(this); this.callback = callback; this.url = url; this.set({ caption: 'Open link', allowMaximize: false, allowMinimize: false, showMaximize: false, showMinimize: false, showClose: false, resizable: false, modal: true }); this.setLayout(new qx.ui.layout.VBox(10)); this.addListenerOnce('resize', this.center, this); this.add(new qx.ui.basic.Label('Would you like to open this link with ReplayShare?').set({ rich: true, maxWidth: 360, wrap: true, textColor: 'white' })); var buttonContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(10).set({ alignX: 'right' })); var yesDontAskButton = new webfrontend.ui.SoundButton('Yes and don\'t ask again'); yesDontAskButton.addListener('execute', this.openReplayShareAndDontAsk, this); var yesButton = new webfrontend.ui.SoundButton('Yes'); yesButton.addListener('execute', this.openReplayShare, this); var noButton = new webfrontend.ui.SoundButton('No'); noButton.addListener('execute', this.openExternal, this); var cancelButton = new webfrontend.ui.SoundButton('Cancel'); cancelButton.addListener('execute', this.close, this); buttonContainer.add(yesDontAskButton); buttonContainer.add(yesButton); buttonContainer.add(noButton); buttonContainer.add(cancelButton); this.add(buttonContainer); }, members: { callback: null, url: null, openExternal: function() { this.close(); qx.core.Init.getApplication().showExternal(this.url); }, openReplayShareAndDontAsk: function() { this.close(); this.callback.call(null, true); }, openReplayShare: function() { this.close(); this.callback.call(null, false); } } }); qx.Class.define('ReplayShare.Service', { extend: qx.core.Object, members: { /** * @param {String} id * @param {Object} options */ get: function(id, options) { var requestOptions = { method: 'GET', accept: 'application/json' }; if (options.success !== undefined) { requestOptions.success = function(data) { options.success(new Replay() .setId(id) .setData(data.data) ); }; } if (options.error !== undefined) { requestOptions.error = function(error) { options.error(null, { message: error }); }; } this._sendRequest('https://replayshare.petui.net/' + id, requestOptions); }, /** * @param {Replay} replay * @param {Object} options */ save: function(replay, options) { var requestOptions = { method: 'PUT', accept: 'application/json', contentType: 'application/json', data: JSON.stringify({ data: replay.getData(), rev: PerforceChangelist || null }) }; if (options.success !== undefined) { requestOptions.success = function(data) { options.success(replay.setId(data.id)); }; } if (options.error !== undefined) { requestOptions.error = function(error) { options.error(replay, { message: error }); }; } this._sendRequest('https://replayshare.petui.net', requestOptions); }, /** * @param {String} url * @param {Object} options */ _sendRequest: function(url, options) { var request = new qx.io.request.Xhr(url); request.setMethod(options.method || 'GET'); request.setTimeout(options.timeout || 10000); if (options.accept !== undefined) { request.setAccept(options.accept); } if (options.contentType !== undefined) { request.setRequestHeader('Content-Type', options.contentType); } if (options.data !== undefined) { request.setRequestData(options.data); } if (options.success !== undefined) { request.addListener('success', function(event) { options.success(event.getTarget().getResponse()); }, this); } if (options.error !== undefined) { request.addListener('error', function(event) { options.error('Unknown error'); }); request.addListener('statusError', function(event) { var response = event.getTarget().getResponse(); if (response.error !== undefined) { options.error(response.error.message || response.error.statusText); } else { options.error('Unknown server error'); } }); request.addListener('timeout', function(event) { options.error('Request timed out'); }); } request.send(); } } }); } function waitForGame() { try { if (typeof qx !== 'undefined' && qx.core.Init.getApplication() && qx.core.Init.getApplication().initDone) { createReplayShare(); ReplayShare.getInstance().initialize(); } else { setTimeout(waitForGame, 1000); } } catch (e) { console.log('ReplayShare: ', e.toString()); } } setTimeout(waitForGame, 1000); }; var script = document.createElement('script'); script.innerHTML = '(' + main.toString() + ')();'; script.type = 'text/javascript'; document.getElementsByTagName('head')[0].appendChild(script); })();