Tiberium Alliances Tweaks

A collection of more or less useful features that attempt to improve the gaming experience.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name           Tiberium Alliances Tweaks
// @version        1.2.0
// @namespace      https://openuserjs.org/users/petui
// @license        GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @author         petui
// @description    A collection of more or less useful features that attempt to improve the gaming experience.
// @include        http*://prodgame*.alliances.commandandconquer.com/*/index.aspx*
// ==/UserScript==
'use strict';

(function() {
	var main = function() {
		'use strict';

		function createTweaks() {
			console.log('Tweaks loaded');

			qx.Class.define('Tweaks', {
				type: 'singleton',
				extend: qx.core.Object,
				statics: {
					Category: {
						Useful: 1,
						Bugfix: 2,
						Script: 3,
						Uncategorized: 4,
						Self: 5
					},
					CategoryNames: {}
				},
				defer: function(statics) {
					statics.CategoryNames[Tweaks.Category.Useful] = 'Useful stuff';
					statics.CategoryNames[Tweaks.Category.Bugfix] = 'Game bugfixes';
					statics.CategoryNames[Tweaks.Category.Script] = '3rd party script tuning';
					statics.CategoryNames[Tweaks.Category.Uncategorized] = 'Uncategorized';
					statics.CategoryNames[Tweaks.Category.Self] = 'Settings';
				},
				construct: function() {
					this.features = {};
					this.configs = this.loadSettings();
					this.settingsWindow = new Tweaks.SettingsWindow(this, Tweaks.CategoryNames);
					this.settingsWindow.addListener('change', this.onSettingsChange, this);
				},
				events: {
					initialize: 'qx.event.type.Event',
					addFeature: 'qx.event.type.Data',
					saveSettings: 'qx.event.type.Event'
				},
				members: {
					settingsWindow: null,
					features: null,
					configs: null,
					initialized: false,

					initialize: function() {
						this.initializeEntryPoint();
						this.initialized = true;
						this.fireEvent('initialize');

						if (!this.hasSavedSettings()) {
							webfrontend.gui.MessageBox.messageBox({
								modal: false,
								textRich: true,
								title: 'Would you like to configure Tweaks?',
								text: 'Looks like this is your first time running Tweaks in this server. Open settings now?<br/><br/>'
									+ 'You can always access it later in the navigation bar under <i>Scripts</i>.',
								okText: 'Yes',
								cancelText: 'No',
								executeOk: this.openSettingsWindow,
								callbackContext: this
							});

							// Save empty settings so user won't be asked again
							this.saveSettings({});
						}
					},

					initializeEntryPoint: function() {
						var scriptsButton = qx.core.Init.getApplication().getMenuBar().getScriptsButton();
						scriptsButton.Add('Tweaks', 'FactionUI/icons/icon_mode_repair.png');

						var children = scriptsButton.getMenu().getChildren();
						var lastChild = children[children.length - 1];
						lastChild.addListener('execute', this.openSettingsWindow, this);
						lastChild.getChildControl('icon').set({
							scale: true,
							width: 18,
							height: 18
						});
					},

					/**
					 * @param {qx.event.type.Data} event
					 */
					onSettingsChange: function(event) {
						var encounteredError = false;
						var settings = event.getData();

						for (var classname in settings) {
							var isEnabled = settings[classname].enabled;
							var details = this.features[classname];
							var wasEnabled = this.hasConfig(details.instance) ? this.getConfig(details.instance).enabled : false;
							this.configs[details.options.configKey] = settings[classname];

							try {
								if (isEnabled) {
									details.instance.activate(wasEnabled);
								}
								else {
									details.instance.deactivate(wasEnabled);
								}
							}
							catch (e) {
								encounteredError = true;
								this.settingsWindow.addError(details.container, (isEnabled ? 'Activation' : 'Deactivation') + ' failed');
								qx.event.GlobalError.handleError(e);
							}
						}

						this.saveSettings(this.configs);

						try {
							this.fireEvent('saveSettings');
						}
						catch (e) {
							qx.event.GlobalError.handleError(e);
						}

						if (encounteredError) {
							this.settingsWindow.open();
						}
					},

					/**
					 * @returns {Boolean}
					 */
					hasSavedSettings: function() {
						return localStorage.getItem('Tweaks') !== null;
					},

					/**
					 * @returns {Object}
					 */
					loadSettings: function() {
						return JSON.parse(localStorage.getItem('Tweaks')) || {};
					},

					/**
					 * @param {Object} settings
					 */
					saveSettings: function(settings) {
						localStorage.setItem('Tweaks', JSON.stringify(settings));
					},

					/**
					 * @param {Tweaks.Feature.IFeature} featureConstructor
					 * @param {Object} options
					 */
					registerFeature: function(featureConstructor, options) {
						qx.Interface.assert(featureConstructor, Tweaks.Feature.IFeature, true);

						if (featureConstructor.classname in this.features) {
							throw new Error('Feature "' + featureConstructor.classname + '" is already registered');
						}

						var instance = null;
						var normalizedOptions = {
							name: options.name || featureConstructor.classname,
							description: options.description || null,
							category: options.category || Tweaks.Category.Uncategorized,
							configKey: options.configKey || featureConstructor.classname,
							disabled: options.disabled || false
						};

						var featureKey = featureConstructor.classname;
						this.features[featureKey] = {
							construct: featureConstructor,
							container: null,
							instance: null,
							options: normalizedOptions
						};

						if (!normalizedOptions.disabled) {
							try {
								instance = new featureConstructor;
								this.features[featureKey].instance = instance;
							}
							catch (e) {
								qx.event.GlobalError.handleError(e);
							}
						}

						var config = this.getConfig(featureConstructor);
						var container = this.settingsWindow.addFeature(instance, normalizedOptions, config);
						this.features[featureKey].container = container;

						if (normalizedOptions.disabled) {
							this.settingsWindow.addMessage(container, 'Disabled', normalizedOptions.disabled);
						}
						else if (instance === null) {
							this.settingsWindow.addError(container, 'Failed to instantiate');
						}
						else if (config.enabled) {
							try {
								instance.activate(false);
							}
							catch (e) {
								this.settingsWindow.addError(container, 'Activation failed');
								qx.event.GlobalError.handleError(e);
							}
						}

						try {
							this.fireDataEvent('addFeature', this.shallowClone(this.features[featureKey]));
						}
						catch (e) {
							qx.event.GlobalError.handleError(e);
						}
					},

					/**
					 * @returns {Object}
					 */
					getAllFeatures: function() {
						return this.shallowClone(this.features);
					},

					/**
					 * @param {Tweaks.Feature.IFeature} feature
					 * @returns {Boolean}
					 */
					hasConfig: function(feature) {
						return feature.classname in this.features
							&& this.features[feature.classname].options.configKey in this.configs;
					},

					/**
					 * @param {Tweaks.Feature.IFeature} feature
					 * @returns {Object}
					 */
					getConfig: function(feature) {
						if (!(feature.classname in this.features)) {
							throw new Error('Feature "' + feature.classname + '" is not registered');
						}

						var options = this.features[feature.classname].options;

						return options.configKey in this.configs
							? this.shallowClone(this.configs[options.configKey])
							: {};
					},

					openSettingsWindow: function() {
						this.settingsWindow.open();
					},

					/**
					 * @param {Object} object
					 * @returns {Object}
					 */
					shallowClone: function(object) {
						var clone = new object.constructor;

						for (var key in object) {
							if (object.hasOwnProperty(key)) {
								clone[key] = object[key];
							}
						}

						return clone;
					}
				}
			});

			qx.Class.define('Tweaks.SettingsWindow', {
				extend: qx.ui.window.Window,
				statics: {
					IndentStep: 20
				},
				construct: function(core, categories) {
					qx.ui.window.Window.call(this);
					this.core = core;

					this.set({
						caption: 'Tweaks',
						icon: 'FactionUI/icons/icon_mode_repair.png',
						layout: new qx.ui.layout.VBox(18),
						contentPaddingTop: 0,
						contentPaddingBottom: 5,
						contentPaddingRight: 6,
						contentPaddingLeft: 6,
						showMaximize: false,
						showMinimize: false,
						allowMaximize: false,
						allowMinimize: false,
						textColor: 'text-region-tooltip',
						resizable: [true, false],
						width: 500
					});
					this.getChildControl('icon').set({
						scale: true,
						width: 20,
						height: 20,
						alignY: 'middle'
					});

					var mainContainer = qx.core.Init.getApplication().getMainContainer();
					mainContainer.addListener('resize', function(event) {
						this.setMaxHeight(event.getData().height);
					}, this);
					this.setMaxHeight(mainContainer.getBounds().height);

					var mainOverlayBounds = qx.core.Init.getApplication().getMainOverlay().getBounds();
					this.moveTo(
						mainOverlayBounds.left + mainOverlayBounds.width - this.getWidth() - 120,
						mainOverlayBounds.top + 100
					);

					var scrollContainer = new qx.ui.container.Composite(new qx.ui.layout.VBox());
					var scroller = new qx.ui.container.Scroll(scrollContainer);
					scrollContainer.addListener('resize', function(event) {
						this.setHeight(event.getData().height);
					}, scroller);
					this.add(scroller, { flex: 1 });

					this.categoryContainers = {};
					this.features = [];

					for (var categoryId in categories) {
						var container = this.categoryContainers[categoryId] = new qx.ui.container.Composite(new qx.ui.layout.VBox(5)).set({
							marginLeft: 2,
							visibility: 'excluded'
						});
						container.add(new qx.ui.basic.Label(categories[categoryId]).set({
							font: 'font_size_13',
							textColor: 'text-region-value'
						}));
						scrollContainer.add(container);
					}

					var cancelButton = new qx.ui.form.Button('Cancel').set({
						paddingLeft: 10,
						paddingRight: 10
					});
					cancelButton.addListener('execute', this.onCancelClick, this);
					var saveButton = new qx.ui.form.Button('Save').set({
						paddingLeft: 10,
						paddingRight: 10
					});
					saveButton.addListener('execute', this.onSaveClick, this);
					var controlsContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(4));
					controlsContainer.add(cancelButton, { flex: 1 });
					controlsContainer.add(saveButton, { flex: 1 });
					this.add(controlsContainer);
				},
				events: {
					change: 'qx.event.type.Data'
				},
				members: {
					core: null,
					configs: null,
					categoryContainers: null,
					features: null,

					onCancelClick: function() {
						var encounteredError = false;

						for (var i = 0; i < this.features.length; i++) {
							var details = this.features[i];
							var config = this.core.getConfig(details.instance);
							details.checkbox.setValue(config.enabled || false);

							try {
								details.instance.onReset(config);
							}
							catch (e) {
								encounteredError = true;
								this.addError(details.container, 'Failed to reset settings');
								qx.event.GlobalError.handleError(e);
							}
						}

						if (!encounteredError) {
							this.close();
						}
					},

					onSaveClick: function() {
						var encounteredError = false;
						var configs = {};

						for (var i = 0; i < this.features.length; i++) {
							var details = this.features[i];
							var isEnabled = details.checkbox.getValue();
							var config = { enabled: isEnabled };

							try {
								details.instance.onSaveConfig(config);
							}
							catch (e) {
								encounteredError = true;
								this.addError(details.container, 'Failed to save settings');
								qx.event.GlobalError.handleError(e);
							}

							configs[details.instance.classname] = config;
						}

						if (!encounteredError) {
							this.close();
						}

						this.fireDataEvent('change', configs);
					},

					/**
					 * @param {Tweaks.Feature.IFeature} feature
					 * @param {Object} options
					 * @param {Object} config
					 * @returns {qx.ui.container.Composite}
					 */
					addFeature: function(feature, options, config) {
						var checkbox = new qx.ui.form.CheckBox(options.name).set({
							value: config.enabled || false
						});
						var label = options.description ? new qx.ui.basic.Label(options.description).set({ rich: true }) : null;
						var renderFailed = false;
						var container = null;

						if (feature !== null) {
							try {
								var temp = feature.onRender(checkbox, label, config);

								if (temp) {
									qx.core.Assert.assertInstance(temp, qx.ui.container.Composite);
									container = temp;
								}
							}
							catch (e) {
								checkbox.setEnabled(false);
								renderFailed = true;
								qx.event.GlobalError.handleError(e);
							}
						}
						else {
							checkbox.set({
								enabled: false,
								value: false
							});
						}

						if (!container) {
							container = new qx.ui.container.Composite(new qx.ui.layout.VBox());
							container.add(checkbox);

							if (label !== null) {
								container.add(label);
							}
						}

						container.setMarginLeft(10);
						var categoryContainer = this.categoryContainers[options.category];
						categoryContainer.add(container);

						if (!categoryContainer.isVisible()) {
							categoryContainer.show();
						}

						if (renderFailed) {
							this.addError(container, 'Failed to render settings');
						}
						else if (feature !== null) {
							this.features.push({
								checkbox: checkbox,
								container: container,
								instance: feature
							});
						}

						return container;
					},

					/**
					 * @param {qx.ui.container.Composite} container
					 * @param {String} message
					 */
					addError: function(container, message) {
						this.addMessage(container, 'Error', message);
					},

					/**
					 * @param {qx.ui.container.Composite} container
					 * @param {String} title
					 * @param {String} message
					 */
					addMessage: function(container, title, message) {
						container.add(new qx.ui.basic.Label().set({
							rich: true,
							value: '<span style="color: #f00;">' + title + ': ' + message + '</span>'
						}));
					}
				}
			});

			qx.Class.define('Tweaks.NotificationButton', {
				extend: qx.ui.form.Button,
				construct: function(label, icon, command) {
					qx.ui.form.Button.call(this, label, icon, command);

					this.set({
						margin: 1,
						padding: [0, 0, 2]
					});
					this._setLayout(new qx.ui.layout.Canvas());
					this.getContentElement().setStyle('overflow', 'visible');
				},
				members: {
					/** @inheritDoc */
					_createChildControlImpl: function(id) {
						var child;

						switch (id) {
							case 'label':
								child = qx.ui.form.Button.prototype._createChildControlImpl.apply(this, arguments).set({
									backgroundColor: 'white',
									font: 'font_size_13_bold',
									padding: [1, 6],
									textColor: 'black'
								});
								child.setLayoutProperties({
									right: -2,
									top: -2
								});

								var containerElement = PerforceChangelist >= 430398 ? child.getContentElement() : child.getContainerElement();
								containerElement.setStyle('border-radius', '8px');
								break;
							case 'icon':
								child = qx.ui.form.Button.prototype._createChildControlImpl.apply(this, arguments).set({
									margin: [4, 6, 6]
								});
								break;
						};

						return child || qx.ui.form.Button.prototype._createChildControlImpl.apply(this, arguments);
					},
					_applyCenter: function() {},
					_applyGap: function() {},
					_applyIconPosition: function() {}
				}
			});

			qx.Interface.define('Tweaks.Feature.IFeature', {
				members: {
					/**
					 * Called when the feature is about to be rendered in settings window.
					 * Return an instance of a qx.ui.container.Composite to change the appearance.
					 * 
					 * @param {qx.ui.form.CheckBox} checkbox
					 * @param {qx.ui.basic.Label} label
					 * @param {Object} config
					 * @returns {qx.ui.container.Composite}
					 */
					onRender: function(checkbox, label, config) {},
					/**
					 * Called when settings are being reseted.
					 * 
					 * @param {Object} config
					 */
					onReset: function(config) {},
					/**
					 * Called when settings are being saved.
					 * 
					 * @param {Object} config
					 */
					onSaveConfig: function(config) {},
					/**
					 * Called when the feature is to be (re)activated.
					 * 
					 * @param {Boolean} wasActive
					 */
					activate: function(wasActive) {},
					/**
					 * Called when the feature is to be (re)deactivated.
					 * 
					 * @param {Boolean} wasActive
					 */
					deactivate: function(wasActive) {}
				}
			});

			qx.Class.define('Tweaks.Feature.AlternativeErrorHandler', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Useful,
						name: 'Alternative error handler',
						description: 'A non-blocking error handler to replace the error reporting dialog.',
						configKey: 'AlternativeErrorHandler'
					});
				},
				construct: function() {
					if (typeof qx.event.GlobalError.getErrorHandler !== 'function') {
						var source = qx.event.GlobalError.handleError.toString();
						var matches = source.match(/this\.([A-Za-z_]+)\.call\(this\.([A-Za-z_]+),[A-Za-z]+\);/);
						var callbackMemberName = matches[1];
						var contextMemberName = matches[2];

						qx.event.GlobalError.getErrorHandler = eval('(function(){return {callback:this.' + callbackMemberName + ',context:this.' + contextMemberName + '};})');
					}
				},
				members: {
					openWindowOnErrorCheckbox: null,
					desktopButton: null,
					window: null,
					errorCount: 0,
					originalErrorHandler: null,
					shouldOpenWindowOnError: null,

					/** @inheritDoc */
					onRender: function(checkbox, label, config) {
						var container = new qx.ui.container.Composite(new qx.ui.layout.VBox());
						container.add(checkbox);
						container.add(this.openWindowOnErrorCheckbox = new qx.ui.form.CheckBox().set({
							label: 'Open error log automatically when an error is encountered',
							marginLeft: Tweaks.SettingsWindow.IndentStep,
							value: config.openWindowOnError || false
						}));
						container.add(label);

						checkbox.bind('value', this.openWindowOnErrorCheckbox, 'enabled');

						return container;
					},

					/** @inheritDoc */
					onReset: function(config) {
						this.openWindowOnErrorCheckbox.setValue(config.openWindowOnError || false);
					},

					/** @inheritDoc */
					onSaveConfig: function(config) {
						config.openWindowOnError = this.openWindowOnErrorCheckbox.getValue();
					},

					/** @inheritDoc */
					activate: function(wasActive) {
						this.shouldOpenWindowOnError = this.openWindowOnErrorCheckbox.getValue();

						if (wasActive) {
							return;
						}

						this.originalErrorHandler = qx.event.GlobalError.getErrorHandler();
						qx.event.GlobalError.setErrorHandler(this.handleError, this);
					},

					deactivate: function(wasActive) {
						if (!wasActive) {
							return;
						}

						if (this.errorCount > 0 && this.originalErrorHandler.context === qx.core.Init.getApplication()) {
							qx.event.GlobalError.setErrorHandler(null, null);
						}
						else {
							qx.event.GlobalError.setErrorHandler(this.originalErrorHandler.callback, this.originalErrorHandler.context);
							this.originalErrorHandler = null;
						}
					},

					onClickDesktopButton: function() {
						this.getWindow().open();
					},

					/**
					 * @param {Error} error
					 */
					handleError: function(error) {
						console.error(error.stack ? error.stack : error);

						this.errorCount++;
						var desktopButton = this.getDesktopButton();
						desktopButton.setLabel(this.errorCount.toString());
						desktopButton.show();

						var window = this.getWindow();
						window.push(error);

						if (this.shouldOpenWindowOnError) {
							window.open();
						}
					},

					/**
					 * @returns {Tweaks.NotificationButton}
					 */
					getDesktopButton: function() {
						if (this.desktopButton === null) {
							this.desktopButton = new Tweaks.NotificationButton().set({
								appearance: 'button-standard-nod',
								icon: 'webfrontend/ui/icons/icn_show_combat_active.png',
								toolTipText: 'Click to open error log'
							});
							this.desktopButton.getChildControl('icon').set({
								scale: true,
								width: 36,
								height: 32
							});

							this.desktopButton.addListener('execute', this.onClickDesktopButton, this);
							qx.core.Init.getApplication().getDesktop().add(this.desktopButton, {
								right: 125,
								top: 40
							});
						}

						return this.desktopButton;
					},

					/**
					 * @returns {Tweaks.Feature.AlternativeErrorHandler.ErrorWindow}
					 */
					getWindow: function() {
						if (this.window === null) {
							this.window = new Tweaks.Feature.AlternativeErrorHandler.ErrorWindow();
							var baseNavBarX = qx.core.Init.getApplication().getBaseNavigationBar().getLayoutParent().getBounds().left;
							this.window.moveTo(baseNavBarX - this.window.getWidth() - 60, 40);
						}

						return this.window;
					}
				}
			});

			qx.Class.define('Tweaks.Feature.AlternativeErrorHandler.ErrorWindow', {
				extend: qx.ui.window.Window,
				construct: function() {
					qx.ui.window.Window.call(this);

					this.set({
						caption: 'Errors',
						icon: 'webfrontend/ui/common/icon_moral_alert_red.png',
						layout: new qx.ui.layout.VBox(4),
						width: 450,
						height: 200,
						contentPaddingTop: 0,
						contentPaddingBottom: 6,
						contentPaddingRight: 6,
						contentPaddingLeft: 6,
						showMaximize: false,
						showMinimize: false,
						allowMaximize: false,
						allowMinimize: false,
						resizable: true,
						visibility: 'excluded',
						textColor: '#bfbfbf'
					});
					this.getChildControl('icon').set({
						scale: true,
						width: 18,
						height: 17,
						alignY: 'middle',
						marginLeft: 8
					});

					this.add(this.logContainer = new qx.ui.container.Composite(new qx.ui.layout.VBox()));
					this.add(new qx.ui.core.Spacer(), { flex: 1 });

					var reportContainer = new qx.ui.container.Composite(new qx.ui.layout.VBox());
					reportContainer.add(this.reportButton = new qx.ui.form.Button('Report first error').set({
						alignX: 'center',
						allowGrowX: false,
						enabled: false,
						toolTipText: 'Click to open the error reporting dialog'
					}));
					this.reportButton.addListener('execute', this.onClickReportButton, this);
					this.add(reportContainer);
				},
				members: {
					logContainer: null,
					reportButton: null,

					/**
					 * @param {Error} error
					 */
					push: function(error) {
						if (!this.reportButton.isEnabled() && !this.logContainer.getChildren().length) {
							this.reportButton.setUserData('error', error);
							this.reportButton.setEnabled(true);
						}

						this.logContainer.add(new qx.ui.basic.Label(
							phe.cnc.Util.getDateTimeString(new Date) + ' ' + error.toString()
						));
					},

					onClickReportButton: function() {
						this.reportButton.setEnabled(false);
						var error = this.reportButton.getUserData('error');

						var app = qx.core.Init.getApplication();
						var errorHandler = qx.event.GlobalError.getErrorHandler();
						app.handleError(error);

						if (errorHandler.context !== app) {
							// Restore error handler that webfrontend.Application.prototype.handleError removed
							qx.event.GlobalError.setErrorHandler(errorHandler.callback, errorHandler.context);
						}
					}
				}
			});

			qx.Class.define('Tweaks.Feature.ShrinkableWindows', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Useful,
						name: 'Shrinkable windows',
						description: 'Click the minimize button to shrink windows. Note that overlays look similar to windows, but they are not shrinkable.',
						configKey: 'ShrinkableWindows'
					});
				},
				members: {
					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var context = this;
						var root = qx.core.Init.getApplication().getRoot();
						var windows = root.getWindows();

						root._addWindow = function(window) {
							context.enableWindowShrink(window);
							return qx.ui.root.Application.prototype._addWindow.apply(this, arguments);
						};
						root._removeWindow = function(window) {
							context.disableWindowShrink(window);
							return qx.ui.root.Application.prototype._removeWindow.apply(this, arguments);
						};

						for (var i = 0; i < windows.length; i++) {
							this.enableWindowShrink(windows[i]);
						}
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (!wasActive) {
							return;
						}

						var root = qx.core.Init.getApplication().getRoot();
						var windows = root.getWindows();

						root._addWindow = qx.ui.root.Application.prototype._addWindow;
						root._removeWindow = qx.ui.root.Application.prototype._removeWindow;

						for (var i = 0; i < windows.length; i++) {
							this.disableWindowShrink(windows[i]);
						}
					},

					/**
					 * @param {qx.ui.window.Window} window
					 */
					enableWindowShrink: function(window) {
						if (window.getUserData('Tweaks.Shrinkable')) {
							return;
						}

						window.setUserData('Tweaks.Shrinkable', true);
						window.setUserData('Tweaks.AllowMinimize', window.getAllowMinimize());
						window.setUserData('Tweaks.ShowMinimize', window.getShowMinimize());

						window.setAllowMinimize = function(value) {
							this.setUserData('Tweaks.AllowMinimize', value);
						};
						window.setShowMinimize = function(value) {
							this.setUserData('Tweaks.ShowMinimize', value);
						};

						window.constructor.prototype.setAllowMinimize.call(window, true);
						window.constructor.prototype.setShowMinimize.call(window, true);

						window.addListener('beforeMinimize', this.onBeforeWindowMinimize, this);
						window.addListener('disappear', this.onWindowDisappear, this);
					},

					/**
					 * @param {qx.ui.window.Window} window
					 */
					disableWindowShrink: function(window) {
						if (!window.getUserData('Tweaks.Shrinkable')) {
							return;
						}

						window.removeListener('beforeMinimize', this.onBeforeWindowMinimize, this);
						window.removeListener('disappear', this.onWindowDisappear, this);

						window.setAllowMinimize = window.constructor.prototype.setAllowMinimize;
						window.setShowMinimize = window.constructor.prototype.setShowMinimize;

						window.setAllowMinimize(window.getUserData('Tweaks.AllowMinimize'));
						window.setShowMinimize(window.getUserData('Tweaks.ShowMinimize'));
						window.setUserData('Tweaks.AllowMinimize', undefined);
						window.setUserData('Tweaks.ShowMinimize', undefined);
						window.setUserData('Tweaks.Shrinkable', undefined);

						this.restoreWindowContent(window);
					},

					/**
					 * @param {qx.event.type.Event} event
					 */
					onBeforeWindowMinimize: function(event) {
						event.preventDefault();

						var window = event.getTarget();
						var pane = window.getChildrenContainer();

						if (window.getUserData('Tweaks.Dimensions') === null) {
							window.setUserData('Tweaks.Dimensions', {
								height: window.getHeight(),
								minHeight: window.getMinHeight(),
								width: window.getWidth()
							});
							pane.exclude();
							window.set({
								height: null,
								minHeight: null,
								width: window.getBounds().width
							});
						}
						else {
							this.restoreWindowContent(window);
						}
					},

					/**
					 * @param {qx.event.type.Event} event
					 */
					onWindowDisappear: function(event) {
						var window = event.getTarget();
						this.restoreWindowContent(window);
					},

					/**
					 * @param {qx.ui.window.Window} window
					 */
					restoreWindowContent: function(window) {
						var dimensions = window.getUserData('Tweaks.Dimensions');

						if (dimensions !== null) {
							window.set({
								height: dimensions.height,
								minHeight: dimensions.minHeight,
								width: dimensions.width
							});
							window.getChildrenContainer().show();
							window.setUserData('Tweaks.Dimensions', undefined);
						}
					}
				}
			});

			qx.Class.define('Tweaks.Feature.MovableMessageComposingWindow', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Useful,
						name: 'Movable message composing window',
						description: 'Replaces the message composing overlay with its window equivalent. Also enables shrinking if that option is activated.',
						configKey: 'MovableMessageComposingWindow'
					});
				},
				construct: function() {
					var source = webfrontend.gui.mail.MailOverlay.prototype.onNewMessage.toString();
					this.mailOverlayMessageMemberName = source.match(/this\.([A-Za-z_]+)\.open/)[1];
				},
				members: {
					mailOverlayMessageMemberName: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var mailMessageWindow = new webfrontend.gui.mail.MailMessageWindow();

						mailMessageWindow.open = function(returnWidget, writeType) {
							webfrontend.gui.mail.MailMessageWindow.prototype.open.call(this, null, writeType);
						};

						// Use methods from MailMessageOverlay to fix reply and forward
						mailMessageWindow.setSubject = webfrontend.gui.mail.MailMessageOverlay.prototype.setSubject;
						mailMessageWindow.setHistoryEntries = webfrontend.gui.mail.MailMessageOverlay.prototype.setHistoryEntries;

						webfrontend.gui.mail.MailOverlay.getInstance()[this.mailOverlayMessageMemberName] = mailMessageWindow;
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (!wasActive) {
							return;
						}

						webfrontend.gui.mail.MailOverlay.getInstance()[this.mailOverlayMessageMemberName] = new webfrontend.gui.mail.MailMessageOverlay();
					}
				}
			});

			qx.Class.define('Tweaks.Feature.ExtendedChatHistory', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Useful,
						name: 'Extended chat history',
						description: 'Increases chat history length.',
						configKey: 'ExtendedChatHistory'
					});
				},
				construct: function() {
					var source = webfrontend.gui.chat.ChatWidget.prototype.setTab.toString();
					this.tabViewMemberName = source.match(/this\.([A-Za-z_]+)\.setSelection\(/)[1];

					// MaelstromTools overwrites webfrontend.gui.chat.ChatWidget.recvbufsize, so use hardcoded value instead.
					this.defaultLength = /*webfrontend.gui.chat.ChatWidget.recvbufsize*/64;
				},
				members: {
					tabViewMemberName: null,
					defaultLength: null,
					lengthSpinner: null,

					/** @inheritDoc */
					onRender: function(checkbox, label, config) {
						var rowContainer = new qx.ui.container.Composite(new qx.ui.layout.HBox(25));
						rowContainer.add(checkbox);
						rowContainer.add(this.lengthSpinner = new qx.ui.form.Spinner().set({
							minimum: this.defaultLength,
							maximum: 512,
							value: config.length || this.defaultLength
						}));
						checkbox.bind('value', this.lengthSpinner, 'enabled');

						var container = new qx.ui.container.Composite(new qx.ui.layout.VBox());
						container.add(rowContainer);
						container.add(label);

						return container;
					},

					/** @inheritDoc */
					onReset: function(config) {
						this.lengthSpinner.setValue(config.length || this.defaultLength);
					},

					/** @inheritDoc */
					onSaveConfig: function(config) {
						config.length = this.lengthSpinner.getValue();
					},

					/** @inheritDoc */
					activate: function(wasActive) {
						var length = this.lengthSpinner.getValue();
						this.setChatHistoryLength(length);
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (wasActive) {
							this.setChatHistoryLength(this.defaultLength);
						}
					},

					/**
					 * @param {Number} length
					 */
					setChatHistoryLength: function(length) {
						var tabPages = qx.core.Init.getApplication().getChat().getChatWidget()[this.tabViewMemberName].getChildren();

						for (var i = 0; i < tabPages.length; i++) {
							tabPages[i].messages.resize(length);
						}
					}
				}
			});

			qx.Class.define('Tweaks.Feature.AmbientSoundVolumeFix', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Bugfix,
						name: 'Fix ambient audio volume',
						description: 'Fixes ambient audio volume always resetting to 20% on login.',
						configKey: 'AmbientSoundVolumeFix'
					});
				},
				members: {
					previousPlayAreaSoundVolume: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var config = phe.cnc.config.Config.getInstance();
						config.addListener('changeAudio', this.onChangeAudio, this);

						var battleground = ClientLib.Vis.VisMain.GetInstance().get_Battleground();
						this.previousPlayAreaSoundVolume = battleground.get_SoundVolume();
						battleground.set_SoundVolume(config.getAudioAmbientLevel() / 100);
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (!wasActive || this.previousPlayAreaSoundVolume === null) {
							return;
						}

						phe.cnc.config.Config.getInstance().removeListener('changeAudio', this.onChangeAudio, this);
						ClientLib.Vis.VisMain.GetInstance().get_Battleground().set_SoundVolume(this.previousPlayAreaSoundVolume);
						this.previousPlayAreaSoundVolume = null;
					},

					onChangeAudio: function() {
						phe.cnc.config.Config.getInstance().removeListener('changeAudio', this.onChangeAudio, this);
						this.previousPlayAreaSoundVolume = null;
					}
				}
			});

			qx.Class.define('Tweaks.Feature.NotificationSidebarFix', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Bugfix,
						name: 'Fix notification sidebar error',
						description: 'Fixes a common script error caused by a bug in the notification sidebar. '
							+ '<a href="http://forum.alliances.commandandconquer.com/showthread.php?tid=32553" style="color:' + webfrontend.gui.util.BBCode.clrLink + ';" target="_blank">Read more</a>',
						configKey: 'NotificationSidebarFix',
						disabled: PerforceChangelist >= 425395 ? 'Obsolete. Bug fixed in patch 15.2' : false
					});
				},
				construct: function() {
					var source = webfrontend.gui.bars.NotificationBar.prototype._onNotificationUpdated.toString();
					this.addToSidebarMethodName = source.match(/this\.([A-Za-z_]+)\([A-Za-z]+\);/)[1];

					source = webfrontend.gui.bars.NotificationBar.prototype[this.addToSidebarMethodName].toString();
					var matches = source.match(/this\.([A-Za-z_]+)\.removeAt\(webfrontend\.gui\.bars\.NotificationBar\.MaxNumberOfNotifications\);this\.([A-Za-z_]+)--;\}\s*;var [A-Za-z]+=this.([A-Za-z_]+)\([A-Za-z]+\);/);
					this.sidebarNotificationContainerMemberName = matches[1];
					this.sidebarNotificationCountMemberName = matches[2];
					this.createSidebarItemMethodName = matches[3];

					source = webfrontend.gui.bars.NotificationBar.prototype._onNotificationRemoved.toString();
					matches = source.match(/var ([A-Za-z]+)=([A-Za-z]+)\.get_Id\(\);var ([A-Za-z]+)=\2\.get_IdOnlineOnly\(\);if\(\1>0&&this\.([A-Za-z_]+)\[\1\]!=null\)\{.+?\}\s*else if\(\1==0&&this\.([A-Za-z_]+)\[\3\]!=null\)\{/);
					this.sidebarNotificationMapByIdMemberName = matches[4];
					this.sidebarNotificationMapByIdOnlineOnlyMemberName = matches[5];
				},
				members: {
					addToSidebarMethodName: null,
					sidebarNotificationContainerMemberName: null,
					sidebarNotificationCountMemberName: null,
					createSidebarItemMethodName: null,
					sidebarNotificationMapByIdMemberName: null,
					sidebarNotificationMapByIdOnlineOnlyMemberName: null,
					removedNotifications: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var source = webfrontend.gui.bars.NotificationBar.prototype[this.addToSidebarMethodName].toString();
						var rewrittenFunctionBody = source.replace(
							/while\(this\.[A-Za-z_]+>webfrontend\.gui\.bars\.NotificationBar\.MaxNumberOfNotifications\)\{.+?\}\s*;/,
							'this.truncateSidebarNotifications(this);'
						);

						var notificationBar = qx.core.Init.getApplication().getNotificationBar();

						if (notificationBar.truncateSidebarNotifications === undefined) {
							notificationBar.truncateSidebarNotifications = this.truncateSidebarNotifications.bind(this);
						}

						notificationBar[this.addToSidebarMethodName] = eval('(' + rewrittenFunctionBody + ')');

						this.removedNotifications = this.cleanupNotificationMap(notificationBar, notificationBar[this.sidebarNotificationMapByIdMemberName])
							.concat(this.cleanupNotificationMap(notificationBar, notificationBar[this.sidebarNotificationMapByIdOnlineOnlyMemberName]));
					},

					deactivate: function(wasActive) {
						if (!wasActive) {
							return;
						}

						var notificationBar = qx.core.Init.getApplication().getNotificationBar();
						notificationBar[this.addToSidebarMethodName] = notificationBar.constructor.prototype[this.addToSidebarMethodName];

						if (this.removedNotifications !== null) {
							for (var i = 0; i < this.removedNotifications.length; i++) {
								// Create sidebar items and add them to id maps, but don't add items to sidebar
								notificationBar[this.createSidebarItemMethodName](this.removedNotifications[i]);
							}

							this.removedNotifications = null;
						}
					},

					/**
					 * @param {webfrontend.gui.bars.NotificationBar} notificationBar
					 * @param {Object} map
					 * @returns {Array}
					 */
					cleanupNotificationMap: function(notificationBar, map) {
						var notificationContainer = notificationBar[this.sidebarNotificationContainerMemberName];
						var removed = [];

						for (var id in map) {
							var sidebarItem = map[id];

							if (sidebarItem !== null && notificationContainer.indexOf(sidebarItem) === -1) {
								var notification = this.getSidebarItemNotification(notificationBar, sidebarItem);

								if (notification !== null) {
									// Add missing items to sidebar so webfrontend.gui.bars.NotificationBar.prototype._onNotificationRemoved can remove them properly
									notificationContainer.add(sidebarItem);
									notificationBar[this.sidebarNotificationCountMemberName]++;

									notificationBar._onNotificationRemoved(notification);
									removed.push(notification);
								}
							}
						}

						return removed;
					},

					/**
					 * @param {webfrontend.gui.bars.NotificationBar} notificationBar
					 */
					truncateSidebarNotifications: function(notificationBar) {
						var notificationContainer = notificationBar[this.sidebarNotificationContainerMemberName];

						while (notificationBar[this.sidebarNotificationCountMemberName] > webfrontend.gui.bars.NotificationBar.MaxNumberOfNotifications) {
							var sidebarItem = notificationContainer.getChildren()[webfrontend.gui.bars.NotificationBar.MaxNumberOfNotifications];
							var notification = this.getSidebarItemNotification(notificationBar, sidebarItem);

							if (notification !== null) {
								notificationBar._onNotificationRemoved(notification);
								this.removedNotifications.push(notification);
							}
							else {
								notificationContainer.removeAt(webfrontend.gui.bars.NotificationBar.MaxNumberOfNotifications);
								notificationBar[this.sidebarNotificationCountMemberName]--;
							}
						}
					},

					/**
					 * @param {webfrontend.gui.bars.NotificationBar} notificationBar
					 * @param {qx.ui.container.Composite} sidebarItem
					 * @returns {ClientLib.Data.Notification}
					 */
					getSidebarItemNotification: function(notificationBar, sidebarItem) {
						var clickListeners = qx.event.Registration.getManager(sidebarItem).getListeners(sidebarItem, 'click');

						for (var i = 0; i < clickListeners.length; i++) {
							if (clickListeners[i].handler === notificationBar._onClick) {
								return clickListeners[i].context.notification;
							}
						}

						return null;
					}
				}
			});

			qx.Class.define('Tweaks.Feature.NotificationTickerFix', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Bugfix,
						name: 'Fix notification ticker error',
						description: 'Fixes a common script error caused by a bug in the notification ticker. '
							+ '<a href="http://forum.alliances.commandandconquer.com/showthread.php?tid=32553" style="color:' + webfrontend.gui.util.BBCode.clrLink + ';" target="_blank">Read more</a>',
						configKey: 'NotificationTickerFix',
						disabled: PerforceChangelist >= 436669 ? 'Obsolete. Bug fixed in patch 15.3' : false
					});
				},
				construct: function() {
					var source = webfrontend.gui.notifications.Ticker.prototype._onTick.toString();
					var matches = source.match(/this\.([A-Za-z_]+)\.removeChild\(this\.([A-Za-z_]+)\[i\]\.getElement\(\)\);/);
					this.tickerDomContainerMemberName = matches[1];
					this.tickerItemArrayMemberName = matches[2];
				},
				members: {
					tickerDomContainerMemberName: null,
					tickerItemArrayMemberName: null,
					removedTickerItems: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var ticker = qx.core.Init.getApplication().getMessagingTicker();
						var domContainer = ticker[this.tickerDomContainerMemberName];
						var tickerItems = ticker[this.tickerItemArrayMemberName];

						if (domContainer === null) {
							ticker._onTick();
							domContainer = ticker[this.tickerDomContainerMemberName];
						}

						if (tickerItems.length > domContainer.children.length) {
							if (this.removedTickerItems === null) {
								this.removedTickerItems = [];
							}

							for (var i = tickerItems.length - 1; i >= 0; i--) {
								if (!domContainer.contains(tickerItems[i].getElement())) {
									this.removedTickerItems.push(tickerItems.splice(i, 1)[0]);
								}
							}
						}
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (this.removedTickerItems === null) {
							return;
						}

						var ticker = qx.core.Init.getApplication().getMessagingTicker();
						var tickerItems = ticker[this.tickerItemArrayMemberName];

						// Shove back the errorneus notifications that were removed on activation. This will effectively return the ticker to its broken state.
						for (var i = 0; i < this.removedTickerItems.length; i++) {
							tickerItems.push(this.removedTickerItems[i]);
						}

						this.removedTickerItems = null;
					}
				}
			});

			qx.Class.define('Tweaks.Feature.ReportsLoadFix', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Bugfix,
						name: 'Fix report load error',
						description: 'Fixes a common script error that occurs when opening a tab in the reports overlay. Also fixes a similar problem in the base info window causing reports not to load. '
							+ '<a href="http://forum.alliances.commandandconquer.com/showthread.php?tid=33706" style="color:' + webfrontend.gui.util.BBCode.clrLink + ';" target="_blank">Read more</a>',
						configKey: 'ReportsLoadFix'
					});
				},
				construct: function() {
					var source = ClientLib.Data.Reports.Reports.prototype.RequestReportHeaderDataAll.toString();
					this.onResponseReportHeaderDataAllMethodName = source.match(/\(new \$I\.[A-Z]{6}\)\.[A-Z]{6}\(this,this\.([A-Z]{6})\)/)[1];

					source = ClientLib.Data.Reports.Reports.prototype.RequestReportHeaderDataBase.toString();
					this.onResponseReportHeaderDataBaseMethodName = source.match(/\(new \$I\.[A-Z]{6}\)\.[A-Z]{6}\(this,this\.([A-Z]{6})\)/)[1];
				},
				members: {
					onResponseReportHeaderDataAllMethodName: null,
					onResponseReportHeaderDataBaseMethodName: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var reports = ClientLib.Data.MainData.GetInstance().get_Reports();

						var source = ClientLib.Data.Reports.Reports.prototype[this.onResponseReportHeaderDataAllMethodName].toString();
						var rewrittenFunctionBody = source.replace(
							/(var ([A-Za-z]+)=)null;if\([A-Za-z]+\.length==[A-Za-z]+\)\{\2=(.+?\})\}/,
							'$1$3'
						);
						reports[this.onResponseReportHeaderDataAllMethodName] = eval('(' + rewrittenFunctionBody + ')');

						source = ClientLib.Data.Reports.Reports.prototype[this.onResponseReportHeaderDataBaseMethodName].toString();
						rewrittenFunctionBody = source.replace(
							/if\([A-Za-z]+\.length==[A-Za-z]+\.NumReportsRequested\)\{(.+?\})\}/,
							'$1'
						);
						reports[this.onResponseReportHeaderDataBaseMethodName] = eval('(' + rewrittenFunctionBody + ')');
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (!wasActive) {
							return;
						}

						var reports = ClientLib.Data.MainData.GetInstance().get_Reports();
						reports[this.onResponseReportHeaderDataAllMethodName] = ClientLib.Data.Reports.Reports.prototype[this.onResponseReportHeaderDataAllMethodName];
						reports[this.onResponseReportHeaderDataBaseMethodName] = ClientLib.Data.Reports.Reports.prototype[this.onResponseReportHeaderDataBaseMethodName];
					}
				}
			});

			qx.Class.define('Tweaks.Feature.PointerEvent', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Bugfix,
						name: 'Fix pointer event duplication',
						description: 'Fixes mouse clicks being handled twice. Most notably allows opening of the Scripts menu. '
							+ '<a href="http://forum.alliances.commandandconquer.com/showthread.php?tid=49151" style="color:' + webfrontend.gui.util.BBCode.clrLink + ';" target="_blank">Read more</a>',
						configKey: 'PointerEvent',
						disabled: statics.isBrowserAffected() ? false : 'Your browser is not affected'
					});
				},
				statics: {
					isBrowserAffected: function() {
						return 'PointerEvent' in window && !qx.bom.client.Event.getMsPointer();
					}
				},
				members: {
					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (!wasActive) {
							this.__getPointerEventHandler()._stopObserver();
						}
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (wasActive) {
							var handler = this.__getPointerEventHandler();
							handler._initObserver(handler._onMouseEvent);
						}
					},

					/**
					 * @returns {qx.event.handler.Pointer}
					 */
					__getPointerEventHandler: function() {
						return qx.event.Registration.getManager(document).getHandler(qx.event.handler.Pointer);
					}
				}
			});

			qx.Class.define('Tweaks.Feature.MaelstromToolsButtons', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Script,
						name: 'Fix MaelstromTools button logic',
						description: 'Tested with MaelstromTools 0.1.3.2',
						configKey: 'MaelstromToolsButtons'
					});
				},
				members: {
					fixRepairAllCheckbox: null,
					fixCollectPackagesCheckbox: null,
					wrapperInstalled: false,

					/** @inheritDoc */
					onRender: function(checkbox, label, config) {
						var container = new qx.ui.container.Composite(new qx.ui.layout.VBox());
						container.add(checkbox);
						container.add(this.fixRepairAllCheckbox = new qx.ui.form.CheckBox().set({
							label: 'Don\'t show "Repair all" button for ghost bases',
							marginLeft: Tweaks.SettingsWindow.IndentStep,
							value: config.fixRepairAll || false
						}));
						container.add(this.fixCollectPackagesCheckbox = new qx.ui.form.CheckBox().set({
							label: 'Don\'t show "Collect packages" button for ghost or locked bases',
							marginLeft: Tweaks.SettingsWindow.IndentStep,
							value: config.fixCollectPackages || false
						}));
						container.add(label);

						checkbox.bind('value', this.fixRepairAllCheckbox, 'enabled');
						checkbox.bind('value', this.fixCollectPackagesCheckbox, 'enabled');

						return container;
					},

					/** @inheritDoc */
					onReset: function(config) {
						this.fixRepairAllCheckbox.setValue(config.fixRepairAll || false);
						this.fixCollectPackagesCheckbox.setValue(config.fixCollectPackages || false);
					},

					/** @inheritDoc */
					onSaveConfig: function(config) {
						config.fixRepairAll = this.fixRepairAllCheckbox.getValue();
						config.fixCollectPackages = this.fixCollectPackagesCheckbox.getValue();
					},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (this.wrapperInstalled) {
							return;
						}

						if (window.MaelstromTools === undefined) {
							// If MaelstromTools is not yet loaded, create a construct that activates the fixes once it has loaded
							var context = this;
							this.wrapperInstalled = true;

							window.MaelstromTools = {
								set Wrapper(value) {
									delete this.Wrapper;
									this.Wrapper = value;
									context.wrapperInstalled = false;
									context.applyFixes(false);
									return value;
								}
							};
						}
						else {
							this.applyFixes(false);
						}
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (wasActive) {
							if (this.wrapperInstalled) {
								delete window.MaelstromTools;
								this.wrapperInstalled = false;
							}
							else {
								this.applyFixes(true);
							}
						}
					},

					/**
					 * @param {Boolean} deactivating
					 */
					applyFixes: function(deactivating) {
						if (MaelstromTools.Wrapper.CanRepairAll_Original === undefined) {
							MaelstromTools.Wrapper.CanRepairAll_Original = MaelstromTools.Wrapper.CanRepairAll;
						}

						if (!deactivating && this.fixRepairAllCheckbox.getValue()) {
							MaelstromTools.Wrapper.CanRepairAll = function(city, viewMode) {
								if (city.get_IsGhostMode()) {
									return false;
								}

								return this.CanRepairAll_Original(city, viewMode);
							};
						}
						else {
							MaelstromTools.Wrapper.CanRepairAll = MaelstromTools.Wrapper.CanRepairAll_Original;
						}

						if (!deactivating && this.fixCollectPackagesCheckbox.getValue()) {
							var source = MaelstromTools.Base.prototype.checkForPackages.toString();
							var rewrittenFunctionBody = source.replace(
								/(([A-Za-z_]+)\.get_CityBuildingsData\(\)\.get_HasCollectableBuildings\(\))/,
								'!$2.get_IsGhostMode() && !$2.get_IsLocked() && $1'
							).replace(
								/(MT_Cache)/,
								'var $1 = MaelstromTools.Cache.getInstance(); $1'
							);
							MaelstromTools.Base.getInstance().checkForPackages = eval('(' + rewrittenFunctionBody + ')');
						}
						else {
							MaelstromTools.Base.getInstance().checkForPackages = MaelstromTools.Base.prototype.checkForPackages;
						}
					}
				}
			});

			qx.Class.define('Tweaks.Feature.NotifyAboutNewFeatures', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Self,
						name: 'Notify me about new features',
						description: 'Displays an icon when new features are added and highlights them in this window.',
						configKey: 'NotifyAboutNewFeatures'
					});
				},
				members: {
					desktopButton: null,
					alteredContainers: null,
					newFeatureCount: 0,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						var core = Tweaks.getInstance();
						core.addListener('addFeature', this.onFeatureAdd, this);
						core.addListener('saveSettings', this.onSettingsChange, this);

						if (!core.initialized) {
							core.addListenerOnce('initialize', this.onSettingsChange, this);
						}
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (wasActive) {
							if (this.desktopButton !== null) {
								this.desktopButton.exclude();
							}

							var core = Tweaks.getInstance();
							core.removeListener('addFeature', this.onFeatureAdd, this);
							core.removeListener('saveSettings', this.onSettingsChange, this);
							this.restoreContainers();
						}
					},

					/**
					 * @param {qx.event.type.Data} event
					 */
					onFeatureAdd: function(event) {
						var details = event.getData();

						if (!Tweaks.getInstance().hasConfig(details.construct)) {
							this.highlightContainer(details.container);
							this.newFeatureCount++;

							var desktopButton = this.getDesktopButton();
							desktopButton.setLabel(this.newFeatureCount.toString());
							desktopButton.show();
						}
					},

					onSettingsChange: function() {
						this.restoreContainers();

						var core = Tweaks.getInstance();
						var features = core.getAllFeatures();
						this.newFeatureCount = 0;

						for (var id in features) {
							var feature = features[id];

							if (!core.hasConfig(feature.construct) && !feature.options.disabled) {
								this.highlightContainer(feature.container);
								this.newFeatureCount++;
							}
						}

						if (this.newFeatureCount > 0) {
							var desktopButton = this.getDesktopButton();
							desktopButton.setLabel(this.newFeatureCount.toString());
							desktopButton.show();
						}
						else if (this.desktopButton !== null) {
							this.desktopButton.exclude();
						}
					},

					onClickDesktopButton: function() {
						Tweaks.getInstance().openSettingsWindow();
					},

					/**
					 * @returns {Tweaks.NotificationButton}
					 */
					getDesktopButton: function() {
						if (this.desktopButton === null) {
							this.desktopButton = new Tweaks.NotificationButton().set({
								appearance: 'button-standard-gdi',
								icon: 'webfrontend/ui/icons/icn_show_combat_active.png',
								toolTipText: 'New features in Tweaks'
							});
							this.desktopButton.getChildControl('icon').set({
								scale: true,
								width: 36,
								height: 32
							});

							this.desktopButton.addListener('execute', this.onClickDesktopButton, this);
							qx.core.Init.getApplication().getDesktop().add(this.desktopButton, {
								right: 125,
								top: 90
							});
						}

						return this.desktopButton;
					},

					/**
					 * @param {qx.ui.container.Composite} container
					 */
					highlightContainer: function(container) {
						if (this.alteredContainers === null) {
							this.alteredContainers = [];
						}

						var containerElement = (PerforceChangelist >= 430398)
							? container.getContentElement()
							: container.getContainerElement();

						this.alteredContainers.push({
							container: container,
							backgroundColor: container.getBackgroundColor(),
							marginLeft: container.getMarginLeft(),
							paddingLeft: container.getPaddingLeft(),
							textColor: container.getTextColor(),
							borderRadius: containerElement.getStyle('border-radius')
						});
						container.set({
							backgroundColor: '#3c7c3c',
							marginLeft: container.getMarginLeft() - 4,
							paddingLeft: container.getPaddingLeft() + 4,
							textColor: '#333'
						});
						containerElement.setStyle('border-radius', '8px');
					},

					restoreContainers: function() {
						if (this.alteredContainers === null) {
							return;
						}

						for (var i = 0; i < this.alteredContainers.length; i++) {
							var info = this.alteredContainers[i];
							info.container.set({
								backgroundColor: info.backgroundColor,
								marginLeft: info.marginLeft,
								paddingLeft: info.paddingLeft,
								textColor: info.textColor
							});

							var containerElement = PerforceChangelist >= 430398
								? info.container.getContentElement()
								: info.container.getContainerElement();

							containerElement.setStyle('border-radius', info.borderRadius);
						}

						this.alteredContainers = null;
					}
				}
			});

			qx.Class.define('Tweaks.Feature.PlayerBasePlateColoring', {
				extend: qx.core.Object,
				implement: [Tweaks.Feature.IFeature],
				defer: function(statics, members) {
					Tweaks.getInstance().registerFeature(members.constructor, {
						category: Tweaks.Category.Script,
						name: 'Enable player base plate coloring',
						description: 'Allows scripts like TACS to color player base plates.',
						configKey: 'PlayerBasePlateColoring'
					});
				},
				construct: function() {
					var regionCity$ctorMemberName = null;

					for (var key in ClientLib.Vis.Region.RegionCity.prototype) {
						if (typeof ClientLib.Vis.Region.RegionCity.prototype[key] === 'function' && ClientLib.Vis.Region.RegionCity.prototype[key].toString().indexOf('region_city_owner') !== -1) {
							regionCity$ctorMemberName = key;
							break;
						}
					}

					if (regionCity$ctorMemberName === null) {
						throw new Error('Unable to locate ClientLib.Vis.Region.RegionCity.prototype.$ctor');
					}

					var source = ClientLib.Vis.Region.RegionCity.prototype[regionCity$ctorMemberName].toString();
					var matches = source.match(PerforceChangelist >= 443425
						? /this\.([A-Z]{6})=\(new \$I\.([A-Z]{6})\)\.([A-Z]{6})\(h, this\.[A-Z]{6}, this\.[A-Z]{6}, this\.[A-Z]{6}\);/
						: /this\.([A-Z]{6})=\(new \$I\.([A-Z]{6})\)\.([A-Z]{6})\(\$I\.[A-Z]{6}\.Black/
					);
					var basePlateMemberName = matches[1];
					this.playerBasePlateClassName = matches[2];
					var playerBasePlate$ctorMemberName = matches[3];

					if (typeof ClientLib.Vis.Region.RegionCity.prototype.get_BasePlate !== 'function') {
						ClientLib.Vis.Region.RegionCity.prototype.get_BasePlate = eval('(function(){return this.' + basePlateMemberName + ';})');
					}

					source = $I[this.playerBasePlateClassName].prototype[playerBasePlate$ctorMemberName].toString();
					matches = source.match(/\$I\.([A-Z]{6})\.prototype\.([A-Z]{6})\.call/);
					var basePlateClassName = matches[1];
					var basePlate$ctorMemberName = matches[2];

					source = $I[basePlateClassName].prototype[basePlate$ctorMemberName].toString();
					matches = source.match(/\$I\.([A-Z]{6})\.prototype\.([A-Z]{6})\.call/);
					var parentBasePlateClassName = matches[1];
					var parentBasePlate$ctorMemberName = matches[2];

					source = $I[parentBasePlateClassName].prototype[parentBasePlate$ctorMemberName].toString();
					matches = source.match(/this\.([A-Z]{6})=a.+this\.([A-Z]{6})\(\)/);
					var plateColorMemberName = matches[1];
					var initMethodName = matches[2];

					if (typeof $I[this.playerBasePlateClassName].prototype.setPlateColor !== 'function') {
						$I[parentBasePlateClassName].prototype.setPlateColor = eval('(function(a){this.' + plateColorMemberName + '=a;this.' + initMethodName + '();})');
					}

					source = ClientLib.Vis.Region.RegionCity.prototype.VisUpdate.toString();
					this.playerBasePlateUpdateMethodName = source.match(/this\.[A-Z]{6}\.([A-Z]{6})\(this\.[A-Z]{6},this\.[A-Z]{6}\);/)[1];

					source = $I[this.playerBasePlateClassName].prototype[this.playerBasePlateUpdateMethodName].toString();
					var rewrittenFunctionBody = source.replace(
						/\$I\.[A-Z]{6}\.Black/,
						'this.' + plateColorMemberName
					);

					this.rewrittenPlayerbasePlateUpdateMethod = eval('(' + rewrittenFunctionBody + ')');
				},
				members: {
					playerBasePlateClassName: null,
					playerBasePlateUpdateMethodName: null,
					rewrittenPlayerbasePlateUpdateMethod: null,
					originalPlayerbasePlateUpdateMethod: null,

					onRender: function(checkbox, label, config) {},
					onReset: function(config) {},
					onSaveConfig: function(config) {},

					/** @inheritDoc */
					activate: function(wasActive) {
						if (wasActive) {
							return;
						}

						this.originalPlayerbasePlateUpdateMethod = $I[this.playerBasePlateClassName].prototype[this.playerBasePlateUpdateMethodName];
						$I[this.playerBasePlateClassName].prototype[this.playerBasePlateUpdateMethodName] = this.rewrittenPlayerbasePlateUpdateMethod;
					},

					/** @inheritDoc */
					deactivate: function(wasActive) {
						if (!wasActive || this.originalPlayerbasePlateUpdateMethod === null) {
							return;
						}

						$I[this.playerBasePlateClassName].prototype[this.playerBasePlateUpdateMethodName] = this.originalPlayerbasePlateUpdateMethod;
					}
				}
			});
		}

		function waitForGame() {
			try {
				if (typeof qx !== 'undefined' && qx.core.Init.getApplication() && qx.core.Init.getApplication().initDone) {
					createTweaks();
					Tweaks.getInstance().initialize();
				}
				else {
					setTimeout(waitForGame, 1000);
				}
			}
			catch (e) {
				console.log('Tweaks: ', e.toString());
			}
		}

		setTimeout(waitForGame, 1000);
	};

	var script = document.createElement('script');
	script.innerHTML = '(' + main.toString() + ')();';
	script.type = 'text/javascript';
	document.getElementsByTagName('head')[0].appendChild(script);
})();