GOTA Extender

Game of Thrones Ascent Extender

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        GOTA Extender
// @namespace   gota_extender
// @author      Panayot Ivanov
// @description Game of Thrones Ascent Extender
// @include     http://gota.disruptorbeam.com/*
// @include     http://gota-www.disruptorbeam.com/*
// @include     https://gota.disruptorbeam.com/*
// @include     https://gota-www.disruptorbeam.com/*
// @include     https://games.disruptorbeam.com/gamethrones/
// @exclude     http://gota.disruptorbeam.com/users/login*
// @exclude     http://gota-www.disruptorbeam.com/users/login*
// @license     WTFPL (more at http://www.wtfpl.net/)
// @require     http://code.jquery.com/jquery-2.1.3.min.js
// @require     https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.9.0/moment.min.js
// @require     https://greasyfork.org/scripts/5279-greasemonkey-SuperValues/code/GreaseMonkey_SuperValues.js?_dc=548
// @require     https://greasyfork.org/scripts/7573-storage-prototype-extension/code/StoragePrototype_extension.js?_dc=117
// @require     https://greasyfork.org/scripts/5427-gota-extender-constants/code/GOTA_Extender_Constants.js?_dc=111
// @resource 	custom https://greasyfork.org/scripts/5426-gota-extender-custom/code/GOTA_Extender_Custom.js?_dc=222
// @resource    auxiliary https://greasyfork.org/scripts/5618-gota-extender-auxiliary/code/GOTA_Extender_Auxiliary.js?_dc=911
// @resource    original https://greasyfork.org/scripts/6702-gota-extender-original/code/GOTA_Extender_Original.js?_dc=318
// @resource    production https://greasyfork.org/scripts/7611-gota-extender-production/code/GOTA_Extender_Production.js?_dc=557
// @version     7.2.3
// @grant       unsafeWindow
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_openInTab
// @grant       GM_xmlhttpRequest
// @grant       GM_getResourceText
// @grant       GM_getResourceURL
// @grant       GM_registerMenuCommand
// ==/UserScript==

// Resolves conflicts of different jQuery versions
$ = this.$ = this.jQuery = jQuery.noConflict(true);
var cloneInto = cloneInto || warn("No function for cloning objects found. This is vital if the script runs on Firefox!", "Components");
var exportFunction = exportFunction || warn("No function for exporting functions found. This is vital if the script runs on Firefox!", "Components");
var unsafeWindow = unsafeWindow || error("This script requires access to the unsafeWindow.", "Greasemonkey");
var GM_getValue = GM_getValue || error("This script requires access to the GM_getValue.", "Greasemonkey");
var GM_setValue = GM_setValue || error("This script requires access to the GM_setValue.", "Greasemonkey");
var GM_openInTab = GM_openInTab || error("This script requires access to the GM_openInTab function.", "Greasemonkey");
var GM_xmlhttpRequest = GM_xmlhttpRequest || error("This script requires access to the GM_xmlhttpRequest.", "Greasemonkey");
var GM_getResourceText = GM_getResourceText || error("This script requires access to the GM_getResourceText function.", "Greasemonkey");
var GM_getResourceURL = GM_getResourceURL || error("This script requires access to the GM_getResourceURL function.", "Greasemonkey");
var GM_registerMenuCommand = GM_registerMenuCommand || error("This script requires access to the GM_registerMenuCommand function.", "Greasemonkey");
var MutationObserver = window.MutationObserver || window.WebKitMutationObserver || error("This script requires access to the GM_registerMenuCommand function.", "Observer");

// --> Register menu commands
(function(){
    GM_registerMenuCommand("HOME", openHome);
    function openHome() {
        GM_openInTab("https://greasyfork.org/en/scripts/3788-gota-extender");
    }

    GM_registerMenuCommand("DEBUG", enterDebugMode);
    function enterDebugMode() {
        options.debugMode = true;
        options.set("debugMode");

        alert("Debug mode has been enabled. Extender will now reload.");
        window.location.reload(true);
    }

    GM_registerMenuCommand("CHECK", checkScript);
    function checkScript() {
        options.checkScript = true;
        options.set("checkScript");

        alert("Extender will check for game function updates.\nPress OK to reload.");
        window.location.reload(true);
    }

    GM_registerMenuCommand("CLEAN", clean);
    function clean() {
        if(window.confirm("This will delete your locally stored data, but not your preferences.\nAre you sure?")){
            localStorage.clear();
            sessionStorage.clear();

            alert("Extender revived your storage.");
        }
    }
}());
// <-- End of menu commands

// --> Initialization
$(window).bind("load", function () {

    // Kill logging first prior to init
    inject.code("window.doLog = function() {};");
    inject.code('(' + overrideFinishLoader.toString() + ')()');

});

function overrideFinishLoader(){
    window.finishExtenderLoad = window.finishLoader;
    window.finishLoader = function() {
        var event = new Event('initEX');
        window.dispatchEvent(event);
    }
}

var initialization = {
    inform: function (msg, progress, info) {

        var exalert = $("h2#loadinginfo-l:visible");
        var expinfo = $("em#preloader_message:visible");
        var exprogress = $("div.loadingbar-inner:visible");
        var progresspercent = $("h2#loadinginfo-r:visible");

        if (exalert.length) { // shown
            msg &&
            exalert.html(msg + ":");

            info && expinfo &&
            expinfo.html(info);

            progress && exprogress &&
            progresspercent.html(progress + "%");
            exprogress.css("width", progress + "%");
        }

    },

    error: function (msg, type) {

        var exalert = $("h2#loadinginfo-l:visible");
        var expinfo = $("em#preloader_message:visible");

        if (!type)
            type = "extender";

        var prefix = type.toString().toUpperCase() + " - ERROR <" + new Date().toLocaleTimeString() + "> ";
        console.error(prefix + msg);

        if (expinfo.length) { // shown
            exalert.html("ERROR:");
            msg && expinfo.html(msg);
        }

        setTimeout(function(){
            unsafeWindow.finishExtenderLoad();
        }, 1E3);

    },

    begin: function () {

        window.removeEventListener("initEX", initialization.begin);
        $('#loadingbg').show();

        this.current = -1;
        initialization.next();
        initialization.inform("EXTENDER", 1, "Begin...");
    },

    next: function () {

        try {

            this.current++;

            var percent = ((this.current + 1) / this.functions.length) * 100;
            initialization.inform(false, percent, false);

            if (typeof this.functions[this.current] == "function") {

                // Invoke next init function
                this.functions[this.current]();

            } else {

                initialization.inform("DONE", 100, "happy hacking!");
                setTimeout(function(){
                    unsafeWindow.finishExtenderLoad();
                }, 1E3);

            }

        } catch (eMsg) {

            initialization.error("Fatal error, initialization failed: " + eMsg, "INITIALIZATION");
            initialization.inform("ERROR", false, "Fatal error, initialization failed: " + eMsg);

        }
    },

    current: -1,
    functions: [

        // CleanUp
        function () {
            // Clean up
            console.clear();
            initialization.next();
        },

        // Init logger
        function icnsl(retry) {

            if (retry == void 0) {
                initialization.inform("", false, "Initializing message system...");
                inject.console();
            }

            if (typeof unsafeWindow.log == "function") {
                log("Messaging system initialized successfully.", "INITIALIZATION");
                initialization.next();
            } else {
                setTimeout(function () {
                    retry
                        ? (retry < 7
                        ? icnsl(++retry)
                        : initialization.error("Messaging system failed to initialize.", "INITIALIZATION"))
                        : icnsl(1);
                }, 500);
            }

        },

        // Init options
        function () {

            // Get all GM values
            initialization.inform("", false, "Fetching preferences...");
            options.get();

            // Check script if scheduled
            if (options.checkScript) {

                initialization.inform("CHECK", 50, "Script scheduled for update check...");

                checkSource();

                initialization.inform("DONE", 100, "Reloading...");

                options.checkScript = false;
                options.set("checkScript");
                window.location.reload(true);

                return;
            }

            initialization.next();

        },

        // Init styles
        function istys(retry) {

            if (retry == void 0) {
                // Add global styles
                initialization.inform("", false, "Injecting styles...");
                styles.addAllStyles();
            }

            if ($("style#extenderStyles").length) {
                log("Styles initialized successfully.", "INITIALIZATION");
                initialization.next();
            } else {
                setTimeout(function () {
                    retry
                        ? (retry < 7
                        ? istys(++retry)
                        : initialization.error("Styles failed to initialize.", "INITIALIZATION"))
                        : istys(1);
                }, 500);
            }
        },

        // Init storage
        // prototype extensions
        function spex(retry) {

            if (retry == void 0) {
                // Inject Storage extension functions (for use in the page)
                initialization.inform("", false, "Injecting storage...");
                inject.outSource("https://greasyfork.org/scripts/7573-storage-prototype-extension/code/StoragePrototype_extension.js");
            }

            if (typeof unsafeWindow.localStorage.get == "function") {
                log("Storage initialized successfully.", "INITIALIZATION");

                console.log("Debugging Components: " +
                    "cloneInto ? ", (typeof cloneInto),
                    "exportFunction ? ", (typeof exportFunction));

                //if (options.useLocalDbOnly) {
                //
                //    if (typeof exportFunction == "function") {
                //    //if (false) {
                //        unsafeWindow.Storage.prototype.set =
                //            exportFunction(GM_SuperValue.set, unsafeWindow, {defineAs: "ExtenderSet"});
                //        unsafeWindow.Storage.prototype.get =
                //            exportFunction(GM_SuperValue.get, unsafeWindow, {defineAs: "ExtenderGet"});
                //    } else {
                //        unsafeWindow.Storage.prototype.set = GM_SuperValue.set;
                //        unsafeWindow.Storage.prototype.get = GM_SuperValue.get;
                //    }
                //
                //    log("Storage configured to use only SQLite.", "INITIALIZATION");
                //}

                initialization.next();
            } else {
                setTimeout(function () {
                    retry
                        ? (retry < 7
                        ? spex(++retry)
                        : initialization.error("Storage extension failed to initialize.", "INITIALIZATION"))
                        : spex(1);
                }, 500);
            }
        },

        // Init page override
        function aux(retry) {

            if (retry == void 0) {
                // Inject auxiliary code
                initialization.inform("", false, "Overriding page functions...");
                inject.code(GM_getResourceText("custom"));
                inject.code(GM_getResourceText("auxiliary"));
                inject.code(GM_getResourceText("production"));
            }

            if (unsafeWindow.extender != void 0) {
                log("Injection sequence successful.", "INITIALIZATION");
                initialization.next();
            } else {
                setTimeout(function () {
                    retry
                        ? (retry < 7
                        ? aux(++retry)
                        : initialization.error("Injection sequence failed.", "INITIALIZATION"))
                        : aux(1);
                }, 500);
            }
        },

        // Init observable & constants
        function oci(retry) {

            if (retry == void 0) {
                // Try an injection sequence
                initialization.inform("", false, "Injecting console & constants...");
                inject.observable();
                inject.constants();
            }

            if ($("textarea#observable").length) {
                log("Observable console & constants initialized successfully.", "INITIALIZATION");
                initialization.next();
            } else {
                setTimeout(function () {
                    retry
                        ? (retry < 7
                        ? oci(++retry)
                        : initialization.error("Observable console and/or constants failed to initialize.", "INITIALIZATION"))
                        : oci(1);
                }, 500);
            }
        },

        // Init modules
        function () {

            initialization.inform("", false, "Loading modules...");

            unsafeWindow.production.init(options.export(["queueDelay", "superiorMaterials", "doSpeedUp"]));
            unsafeWindow.bossChallenger.init(options.export(["autoBossChallenge"]));
            unsafeWindow.worldEvent.init(options.export(["weManagerEnabled", "worldEventDelay"]));

            // TODO: Uncomment when questMan ready
            //unsafeWindow.questMan.init(options.export(["questManagerDelay", "questManagerEnabled"]));

            initialization.next();
        },

        // Init game
        function ingex(retry) {

            if (retry == void 0) {
                initialization.inform("", false, "Loading extender...");
            } else {
                initialization.inform("", false, "Loading extender (" + retry + ") ...");
            }

            //console.log("Debugging extender load: ", unsafeWindow.userContext);
            if (unsafeWindow.userContext == void 0) {
                setTimeout(function () {
                    retry
                        ? (retry < 21
                        ? ingex(++retry)
                        : initialization.error("Failed to load extender after 21 retries.", "INITIALIZATION"))
                        : ingex(1);
                }, 500);

                return;
            }

            // Toggle
            toggleAll();

            // Claim
            quarterMasterDo();

            // Claim favours
            acceptAllFavors();

            // Send out gifts
            sendGifts();

            // Store all sworn swords
            getSwornSwords();

            // Sort player inventory
            unsafeWindow.sort();

            setTimeout(function () {
                initialization.next();
            }, 500);
        }
    ]
};

window.addEventListener("initEX", initialization.begin);
// <-- End of initialization

// --> Main toolbar mutations observer

// Observers construction
var mainToolbarObserver = new MutationObserver(main_toolbar_buttons_changed);

// define what element should be observed by the observer
// and what types of mutations trigger the callback
mainToolbarObserver.observe(document.getElementById("main_toolbar_buttons"), {
    //	childList: true,
    attributes: true,
    //	characterData: true,
    subtree: true,
    attributeOldValue: true
    // if attributes
    //	characterDataOldValue: true,    // if characterData
    //	attributeFilter: ["id", "dir"], // if attributes
});

function main_toolbar_buttons_changed() {
    //    log("Mutation on main toolbar buttons.");

    var menu = $("#extender-menu");
    var container = $("#navmenubox");
    if (container.length > 0 && menu.length == 0) {
        container.append(templates.menuBtn);
    }
}
// <-- End of mutations observer

// --> Page command handling
var signalObserver = new MutationObserver(signal_acknowledged);
function signal_acknowledged() {
    var observable = $("textarea#observable");

    if (!observable) {
        error("The observable DOM element was not found in the page.");
        return;
    }

    var msg = "Error: Unknown command.";
    var commandObj = JSON.parse(observable.attr("command"));
    var prefix = "COMMAND ACKNOWLEDGED" + " | " + new Date().toLocaleTimeString() + " | ";

    if (!commandObj || typeof commandObj != "object") {
        msg = "Error: Cannot parse the command object given.";
        observable.val(prefix + msg);
        error(msg);
        return;
    }

    if (typeof commandObj.name !== "string") {
        msg = "Error: Command does not have a name specified.";
        observable.val(prefix + msg);
        error(msg);
        return;
    }

    var args = commandObj.args;

    // Parse command
    switch (commandObj.name) {
        case "option":

            //console.debug(commandObj);

            // Rely on the second check only
            if (options.hasOwnProperty(args[0]) && typeof options[args[0]] === typeof args[1]) {
                options[args[0]] = args[1];
                options.set(args[0]);

                observable.val(prefix + "Option " + args[0] + " set to " + args[1] + " successfully.");
                log("Option " + args[0] + " set to " + args[1] + " successfully.", "COMMAND")
                return;
            }

            if (options.hasOwnProperty(args[0])) {

                if (typeof options[args[0]] == "object") {
                    log(args[0] + " is a composite object:", "COMMAND");
                    console.log(typeof cloneInto == "function" ? cloneInto(options[args[0]], unsafeWindow) : options[args[0]]);
                } else {
                    log(args[0] + ": " + options[args[0]], "COMMAND");
                }

                observable.val(prefix + "See console for requested option.");
                return;
            }

            msg = "Warning: Lack of or incorrect parameters passed to command.";
            observable.val(prefix + msg);
            warn(msg, "COMMAND");

            break;
        case "collect":
            log("Immediate collect attempt.", "COMMAND");
            collectTax();
            break;
        case "import":
            options.import(unsafeWindow.extender.options);
            log("Options imported successfully.", "COMMAND");
            break;
        default:
            observable.val(prefix + msg);
            error(msg, "COMMAND");

            break;
    }
}
// <-- Page command handling

// TODO: Implement..
//var PERSISTABLE =  {
//
//    get queue() {
//        return localStorage.get("productionQueue", []);
//    },
//
//    set queue(val) {
//        localStorage.set("productionQueue", val);
//    }
//};

// --> Options object
var options = {
    swornSwords: [],
    default_swornSwords: [],

    // Non-tweakable options
    lastIdChecked: 2,
    default_lastIdChecked: 1,

    doSpeedUp: true,
    default_doSpeedUp: true,

    logLevel: ["QMASTER", "FAVOR", "DAILY", "ADVENTURE"],
    default_logLevel: ["QMASTER", "FAVOR", "DAILY", "ADVENTURE"],
    outputLogAsText: false,
    default_outputLogAsText: false,
    logLevelTimestamp: "all_time",
    default_logLevelTimestamp: "all_time",
    appendLastSeen: true,
    default_appendLastSeen: true,
    debugMode: true,
    default_debugMode: true,
    checkScript: false,
    default_checkScript: false,
    baseDelay: 4,
    default_baseDelay: 4,
    queueDelay: 4,
    default_queueDelay: 4,
    autoCollectInterval: 60,
    default_autoCollectInterval: 60,
    superiorMaterials: true,
    default_superiorMaterials: true,
    queueTimerInterval: 30,
    default_queueTimerInterval: 30,
    bruteWounds: 1,
    default_bruteWounds: 1,
    bruteSwitchOff: true,
    default_bruteSwitchOff: true,
    doTooltips: false,
    default_doTooltips: false,
    neverSpendGold: true,
    default_neverSpendGold: true,
    autoReloadInterval: 6,
    default_autoReloadInterval: 6,
    boonsSortBy: "available_quantity",
    default_boonsSortBy: "available_quantity",
    boonsSortBy2: "rarity",
    default_boonsSortBy2: "rarity",
    shopSortBy: "price",
    default_shopSortBy: "price",
    shopSortBy2: "rarity",
    default_shopSortBy2: "rarity",
    sendAllAction: "all",
    default_sendAllAction: "all",
    autoBossChallenge: false,
    default_autoBossChallenge: false,

    featureTesting: false,
    default_featureTesting: false,
    hideLockedBuildings: true,
    default_hideLockedBuildings: true,

    selectedAction: null,
    default_selectedAction: null,

    autoQMaster: true,
    default_autoQMaster: true,
    avaSendSubcamps: false,
    default_avaSendSubcamps: false,

    weManagerEnabled: false,
    default_weManagerEnabled: false,
    worldEventDelay: 6,
    default_worldEventDelay: 6,

    defaultBattle: "fight",
    default_defaultBattle: "fight",
    defaultTrade: "barter",
    default_defaultTrade: "barter",
    defaultIntrigue: "spy",
    default_defaultIntrigue: "spy",

    useLocalDbOnly: false,
    default_useLocalDbOnly: false,

    get: function () {

        // --> NOTE !!! SEPARATE HOSTS ARE NO LONGER SUPPORTED !!!

        //var prefix = "";

        // Separate variable retrieval for both hosts
        //if (unsafeWindow.location.host === "gota-www.disruptorbeam.com") {
        //    prefix = "gota-";
        //}

        // <-- NOTE !!! SEPARATE HOSTS ARE NO LONGER SUPPORTED !!!

        for (var property in this) {
            if (this.hasOwnProperty(property) && property.indexOf("default_") == -1 && typeof this[property] != "function") {
                // console.debug("Retrieving " + prefix + property + " with default value of " + this["default_" + property]);
                this[property] = GM_SuperValue.get(property, this["default_" + property]);
                // console.debug("Property " + property + " has a value of " + this[property]);
            }
        }
    },

    set: function (opt) {

        // --> NOTE !!! SEPARATE HOSTS ARE NO LONGER SUPPORTED !!!

        //var prefix = "";
        //// Separate variable set for both hosts
        //if (unsafeWindow.location.host === "gota-www.disruptorbeam.com") {
        //    prefix = "gota-";
        //}

        // <-- NOTE !!! SEPARATE HOSTS ARE NO LONGER SUPPORTED !!!

        if (opt && this.hasOwnProperty(opt)) {
            GM_SuperValue.set(opt, this[opt]);
            return;
        }

        // Store all properties
        for (var prop in this) {
            if (this.hasOwnProperty(prop) && prop.indexOf("default_") > -1)
                continue;

            if (this.hasOwnProperty(prop) && typeof this[prop] != "function") {
                GM_SuperValue.set(prop, this[prop]);
            }
        }
    },

    reset: function () {
        for (var property in this) {
            if (this.hasOwnProperty(property) && property.indexOf("default_") == -1 && typeof this[property] != "function") {
                this[property] = this["default_" + property];
            }
        }

        this.set();
    },

    import: function(o){
        try {
            if (!o || typeof o !== "object") {
                error("Cannot import options. Incorrect parameter passed.");
                return;
            }

            for (var p in o) {
                if (!this.hasOwnProperty(p))
                    continue;

                //console.debug(this[p] instanceof Array, o[p] instanceof unsafeWindow.Array);
                if (this[p] instanceof Array && o[p] instanceof unsafeWindow.Array) {
                    if (this[p].length != o[p].length) {
                        //console.debug("Import ", p, " with value ", o[p]);
                        this[p] = o[p];
                        this.set(p);
                    }

                    continue;
                }

                if (this[p] !== o[p]) {
                    //console.debug("Import ", p, " with value ", o[p]);
                    this[p] = o[p];
                    this.set(p);
                }
            }
        } catch (err) {
            error("Importing failure: " + err, "OPTIONS");
        }
    },

    export: function(params){
        try {
            var exportObject = {};
            if (params == void 0) {

                for (var opt in this) {
                    if (this.hasOwnProperty(opt) && opt.indexOf("default_") == -1 && typeof this[opt] != "function") {
                        exportObject[opt] = this[opt];
                    }
                }

                return typeof cloneInto == "function"
                    ? cloneInto(exportObject, unsafeWindow) // return structured clone
                    : exportObject; // regular object (no need of cloning)
            }

            if (typeof params == "object" && params instanceof Array) {

                for (var i = 0; i < params.length; i++) {
                    var exportProperty = params[i];
                    if (this.hasOwnProperty(exportProperty) && this[exportProperty] != "function") {
                        exportObject[exportProperty] = this[exportProperty];
                    }
                }

                return typeof cloneInto == "function"
                    ? cloneInto(exportObject, unsafeWindow) // return structured clone
                    : exportObject; // regular object (no need of cloning)
            }

            // Further cases regard string only
            if (typeof params != "string") {
                warn("Cannot resolve export parameters.");
                return null;
            }

            if (this.hasOwnProperty(params) && this[params] != "function")
                return typeof cloneInto == "function"
                    ? cloneInto(this[params], unsafeWindow) // return structured clone
                    : this[params]; // regular object (no need of cloning)

            if (this.hasOwnProperty(params) && this[params] == "function")
                return typeof exportFunction == "function"
                    ? exportFunction(this[params], unsafeWindow) // return exported function
                    : this[params]; // regular function (no need of exporting)
        } catch(err) {
            error("Exporting failure: " + err, "OPTIONS");
        }
    }
};
// <-- End of options object

// --> Injection object
var inject = {

    // Constants required by the page
    constants: function () {

        // Safe string
        unsafeWindow.phraseText.shop_filter_extender = "Extender";

        // EXTENDER :: Modification - add custom filter
        if (unsafeWindow.shopFilters.indexOf("extender") == -1) {
            log("Injecting extender filter...");
            unsafeWindow.shopFilters.push("extender");
        }

        unsafeWindow.extender.options = options.export();

        //Inject structured clone (for Mozilla)
        if (typeof (cloneInto) == "function") {
        //    unsafeWindow.extender_queueDelay = cloneInto(options.queueDelay, unsafeWindow);
        //    unsafeWindow.extender_confirmSuperiorMaterials = cloneInto(options.superiorMaterials, unsafeWindow);
        //    unsafeWindow.extender_bruteWounds = cloneInto(options.bruteWounds, unsafeWindow);
        //    unsafeWindow.extender_bruteSwitchOff = cloneInto(options.bruteSwitchOff, unsafeWindow);
        //    unsafeWindow.extender_debugMode = cloneInto(options.debugMode, unsafeWindow);
        //    unsafeWindow.extender_baseDelay = cloneInto(options.baseDelay, unsafeWindow);
        //    unsafeWindow.extender_neverSpendGold = cloneInto(options.neverSpendGold, unsafeWindow);
        //
        //    unsafeWindow.extender_boonsSortBy = cloneInto(options.boonsSortBy, unsafeWindow);
        //    unsafeWindow.extender_boonsSortBy2 = cloneInto(options.boonsSortBy2, unsafeWindow);
        //    unsafeWindow.extender_shopSortBy = cloneInto(options.shopSortBy, unsafeWindow);
        //    unsafeWindow.extender_shopSortBy2 = cloneInto(options.shopSortBy2, unsafeWindow);
        //
        //    unsafeWindow.extender_sendAllAction = cloneInto(options.sendAllAction, unsafeWindow);
        //    //unsafeWindow.extender_autoBossChallenge = cloneInto(options.autoBossChallenge, unsafeWindow);
        //
            unsafeWindow.userContext.tooltipsEnabled = cloneInto(options.doTooltips, unsafeWindow);
        //
        } else {
        //    //unsafeWindow.extender_queueDelay = options.queueDelay;
        //    //unsafeWindow.extender_confirmSuperiorMaterials = options.superiorMaterials;
        //    unsafeWindow.extender_bruteWounds = options.bruteWounds;
        //    unsafeWindow.extender_bruteSwitchOff = options.bruteSwitchOff;
        //    unsafeWindow.extender_debugMode = options.debugMode;
        //    unsafeWindow.extender_baseDelay = options.baseDelay;
        //    unsafeWindow.extender_neverSpendGold = options.neverSpendGold;
        //
        //    unsafeWindow.extender_boonsSortBy = options.boonsSortBy;
        //    unsafeWindow.extender_boonsSortBy2 = options.boonsSortBy2;
        //    unsafeWindow.extender_shopSortBy = options.shopSortBy;
        //    unsafeWindow.extender_shopSortBy2 = options.shopSortBy2;
        //
        //    unsafeWindow.extender_sendAllAction = options.sendAllAction;
        //    //unsafeWindow.extender_autoBossChallenge = options.autoBossChallenge;
        //
            unsafeWindow.userContext.tooltipsEnabled = options.doTooltips;
        }

        log("Constants injected successfully.", "INJECTOR");
    },

    // Inject code
    code: function (code) {

        var script = document.createElement('script');
        script.type = "text/javascript";
        script.innerHTML = code;
        document.head.appendChild(script);

        log("Code injected successfully.", "INJECTOR");
    },

    outSource: function (src, delay) {

        var script = document.createElement('script');
        script.type = "text/javascript";
        script.src = src;

        delay ? setTimeout(function () {
            document.head.appendChild(script);
        }, (options.baseDelay / 4) * 1000) : document.head.appendChild(script);

        log("Script from outer source injected.", "INJECTOR");
    },

    // Injects a DOM object and starts observing it
    observable: function () {

        $("div#outerwrap div.footer").prepend(templates.observable);
        signalObserver.observe(document.getElementById("observable"), {
            //	childList: true,
            attributes: true,
            //	characterData: true,
            //  subtree: true,
            attributeOldValue: true
            // if attributes
            //	characterDataOldValue: true,    // if characterData
            //	attributeFilter: ["id", "dir"], // if attributes
        });

        log("Observable injected successfully.", "INJECTOR");
    },

    // Inject console and alert handling separately once
    console: function () {
        if (typeof exportFunction == "function") {
            exportFunction(log, unsafeWindow, {defineAs: "log"});
            exportFunction(warn, unsafeWindow, {defineAs: "warn"});
            exportFunction(error, unsafeWindow, {defineAs: "error"});

            exportFunction(clientLog, unsafeWindow, {defineAs: "clientLog"});

            exportFunction(inform, unsafeWindow, {defineAs: "inform"});
            //exportFunction(progress, unsafeWindow, {defineAs: "progress"});

        } else {
            unsafeWindow.log = log;
            unsafeWindow.warn = warn;
            unsafeWindow.error = error;

            unsafeWindow.clientLog = clientLog;

            unsafeWindow.inform = inform;
            //unsafeWindow.progress = progress;
        }

        log("Messaging system injected successfully.", "INJECTOR");
    }
};
// <-- End of injection object

// --> Message handling & log
function log(message, type) {

    if (typeof message == "object") {
        clientLog(message, type);
        return;
    }

    if (options && options.debugMode && console && console.log
        && typeof (console.log) == "function") {
        if (!type)
            type = "extender";

        var prefix = type.toString().toUpperCase() + " <" + new Date().toLocaleTimeString() + "> ";
        console.log(prefix + message);
    }
}

function error(message, type) {
    if (console && console.error && typeof (console.error) == "function") {
        if (!type)
            type = "extender";

        var prefix = type.toString().toUpperCase() + " - ERROR <" + new Date().toLocaleTimeString() + "> ";
        console.error(prefix + message);
    }
}

function warn(message, type) {
    if (console && console.warn && typeof (console.warn) == "function") {
        if (!type)
            type = "extender";

        var prefix = type.toString().toUpperCase() + " - WARNING <" + new Date().toLocaleTimeString() + "> ";
        console.warn(prefix + message);
    }
}

function inform(msg, progress, info) {

    if (unsafeWindow && typeof unsafeWindow.doAlert == "function") {

        var exalert = $("div#exalert.exrow:visible");
        var expinfo = $("div#expinfo.exrow:visible");
        var exprogress = $("div#exprogress.progstretch-inner:visible");

        if(exalert.length) { // alert shown
            msg &&
                exalert.html(msg);

            info && expinfo &&
                expinfo.html(info);

            progress && exprogress &&
                exprogress.css("width", progress + "%");

            return;
        }

        // Construct alert
        unsafeWindow.doAlert("EXTENDER", templates.formatAlert(msg, progress, info))

    } else if (alert && typeof alert == "function")
        alert("Progress: " + progress + " : " + info + "\n\n" + msg);
}

function clientLog(data, type) {
    //console.debug("Debugging arguments: ", arguments);

    if (options.logLevel.indexOf(type) > -1) {

        //console.debug("Debugging data: ", data);

        var clientEntries = sessionStorage.get("clientEntries", []);
        if(data.message != void 0){
            var msg = {
                message: data.message,
                type: type,
                timestamp: moment().format()
            };

            clientEntries.push(msg);

        } else {
            var stored = clientEntries.filter(function (item) {
                return item.symbol === data.symbol;
            })[0];  // check if it the logged item is in the array

            if (stored != void 0) {   // already in storage
                stored.quantity += data.quantity;
                stored.type = "";
                stored.timestamp = "";
            } else {                // new entry
                var entry = {
                    symbol: data.symbol,
                    type: type,
                    quantity: data.quantity,
                    timestamp: moment().format()
                };

                clientEntries.push(entry);
            }
        }

        sessionStorage.set("clientEntries", clientEntries);
    }
}
// <-- Message handling

// --> Loops handling
function toggleAll() {
    toggleAutoCollect();
    toggleQueueTimer();
    toggleReloadWindow();
}

var autoCollectLoop;
function toggleAutoCollect() {
    if (options.autoCollectInterval > 0) {
        autoCollectLoop = setInterval(collectTax, options.autoCollectInterval * 60 * 1000);
        log("Auto collect loop set to: " + options.autoCollectInterval + "min.");

        log("Immediate collect attempt.", "EXTENDER");
        collectTax();

    } else {
        autoCollectLoop = clearInterval(autoCollectLoop);
        log("Auto collect loop disabled.");
    }
}

var queueTimer;
function toggleQueueTimer() {
    if (options.queueTimerInterval > 0) {
        queueTimer = setInterval(unsafeWindow.production.attempt, options.queueTimerInterval * 60 * 1000);
        log("Queue timer interval set to: " + options.queueTimerInterval + "min.");

        log("Immediate production attempt.", "EXTENDER");
        unsafeWindow.production.attempt();
        
    } else {
        queueTimer = clearInterval(queueTimer);
        log("Queue timer disabled.");
    }
}

var reloadWindowTimeout;
function toggleReloadWindow() {
    if (options.autoReloadInterval > 0) {
        setTimeout(function () {
            //saveProductionQueue();
            window.location.reload(true);
        }, options.autoReloadInterval * 60 * 60 * 1000);
        log("Auto reload interval set to: " + options.autoReloadInterval + "h.");
    } else {
        reloadWindowTimeout = clearTimeout(reloadWindowTimeout);
        log("Auto reloading cancelled.");
    }

}

function acceptAllFavors() {
    ajax({
        url: "/play/accept_favor",
        success: function (r) {
            //console.debug(r, r.accepted);

            r.silver_reward && log({
                symbol: "silver_coins",
                quantity: r.silver_reward
            }, "FAVOR");

            if(!$.isEmptyObject(r.accepted)){
                for(var item in r.accepted){
                    if(!r.accepted.hasOwnProperty(item))
                        continue;

                    var value = r.accepted[item];
                    r.silver_reward && log({
                        symbol: item,
                        quantity: value
                    }, "FAVOR");

                    //log("Accepted: " + value + " x " + item, "FAVOR", true);
                }
            } else {
                log("All favors have been claimed.");
            }
        }
    });
}

function quarterMasterDo(status) {

    if (!status) {
        ajax({
            url: "/play/quartermaster_status",
            success: function (response) {
                quarterMasterDo(response);
            }
        });

        return;
    }

    if (options.autoQMaster && status.total_keys) {
        openBox();
        return;
    }

    if (status.available_daily_key) {
        claimDailyQuarterMaster();
        return;
    }

    if (status.available_bonus_key) {
        claimBonusQuarterMaster();
        return;
    }

    log("All quartermaster rewards have been taken.");

    unsafeWindow.claimDaily();
    log("Daily reward claimed.")
}

function claimDailyQuarterMaster() {

    ajax({
        url: "/play/quartermaster_claim_daily",
        success: function (r) {
            quarterMasterDo(r.status);
        }
    });
}

function claimBonusQuarterMaster() {

    ajax({
        url: "/play/quartermaster_claim_bonus",
        success: function (r) {
            quarterMasterDo(r.status);
        }
    });
}

function openBox() {
    if(!options.autoQMaster){
        log("Feature has been disabled.", "QUARTERMASTER");
        return;
    }

    ajax({
        url: "/play/quartermaster_open_chest/?bribes=0&nonce=" + unsafeWindow.userContext.purchase_nonce,
        success: function (r) {
            if(!r.purchase_nonce) {
                error("Could not retrieve a purchase nonce. Process terminated...");
                return;
            }

            unsafeWindow.userContext.purchase_nonce = r.purchase_nonce;

            if(!r.rewards) {
                error("No rewards retrieved. Process terminated...");
                console.debug("Server responded: ", r);
                return;
            }

            //console.debug(r.rewards);

            for(var i = 0; i < r.rewards.length; i++){
                var reward = r.rewards[i];
                //var clientEntry = "Reward claimed: " + reward.item_symbol + ', ' +
                //    'quantity: ' + reward.quantity;

                log({
                    symbol: reward.item_symbol,
                    quantity: reward.quantity
                }, "QMASTER");

                //log(clientEntry, "QUARTERMASTER", true);
            }

            quarterMasterDo();
        }
    });
}

function sendGifts(){
    ajax({
        url: "/play/gifts",
        success: function (r) {
            var bestGift = r.favors.sort(function(a, b){
                return a.description < b.description;
            })[0].symbol;
        }
    });
}

function collectTax() {
    try {

        var itemId = unsafeWindow.buildingBySymbol('counting_house').item_id;
        unsafeWindow.doCollect(itemId);
        log("Silver collected.");

    } catch (err) {
        error(err);
    }
}

// <-- Loops handling

// --> Settings handling
$("#main_toolbar_buttons").on('click', "#extender-menu", showSettings);
function showSettings(e) {
    e.preventDefault();

    try {

        // Clear loading screen if any
        $("#loadingbg:visible").hide();

        // Construct header and place a container for the content
        $("#credits_page").empty().append(templates.optionsHeader).append(templates.tabContent);

        // Construct tabs
        $("#extenderTabMenu").find(".charactertabs")
            .append(templates.optionsTab("logTab", "LOG"))
            .append(templates.optionsTab("mainTab", "MAIN"))
            .append(templates.optionsTab("queueTab", "QUEUE"))
            .append(templates.optionsTab("bruteTab", "BRUTING"))
            .append(options.featureTesting && templates.optionsTab("weTab", "WE"));

        $("#mainTab").trigger('click');

        optionsBottom(); // Construct the bottom of the options window

        $("#credits_roll").show();

    } catch (err) {
        error(err, "show_settings");
    }
}

function optionsBottom() {
    var saveBtn = $("#saveOptions");
    var container = $("#creditsclose");
    if (saveBtn.length == 0 && container.length > 0) {
        container.before(templates.saveOptionsBtn);
    }

    container.attr('onclick', '(function(){ $("#credits_page").empty(); $("#credits_roll").hide(); })()');

    var resetBtn = $("#resetOptions");
    if (resetBtn.length == 0 && container.length > 0) {
        container.after(templates.resetOptionsBtn);
    }
}

$("#credits_roll").on("click", ".inventorytabwrap", tab_onchange);
function tab_onchange(e) {
    e.preventDefault();

    $(".inventorytab.active").removeClass("active");
    $(this).find(".inventorytab").addClass("active");

    var tabContent = $("#extenderTabContent");
    if(!tabContent.length)
    {
        error("Cannot locate tab content!", "tab_onchange")
        return;
    }

    switch (this.id) {
        case "logTab":
            tabContent.html(templates.logTab(options));
            break;
        case "mainTab":
            tabContent.html(templates.mainTab(options));
            break;
        case "queueTab":
            tabContent.html(templates.queueTab(options));
            unsafeWindow.production.render();
            break;
        case "bruteTab":
            getSwornSwords();
            tabContent.html(templates.bruteTab(options));
            break;
        case "weTab":
            tabContent.html(templates.weTab(options));
            break;
        default:
            warn("Not a known tab or in development.");
            break;
    }
}

$("#credits_roll").on('click', "#saveOptions", saveOptions_click);
function saveOptions_click(e) {
    e.preventDefault();

    try {

        if ($("#credits_roll").is(":hidden")) {
            return;
        }

        var tab = $(".inventorytab.active:visible").parents(".inventorytabwrap").attr("id");

        switch (tab) {
            case "mainTab":
                saveMainTab();
                break;
            case "queueTab":
                saveQueueTab();
                break;
            case "bruteTab":
                saveBruteTab();
                break;
            case "weTab":
                saveWeTab();
                break;
            case "logTab":
                saveLogTab();
                break;
            default:
                warn("Not a known tab or in development.");
                return;
        }

        //options.set();
        //inject.constants();
        //unsafeWindow.sort();
        //toggleAll();

        $("#credits_page").empty();
        $("#credits_roll").hide();
        inform("Settings saved.");

    } catch (e) {
        inform(e);
    }
}

function saveLogTab(){
    var $logLevelTimestamp = $("#logLevelTimestamp");
    if($logLevelTimestamp.length){
        options.logLevelTimestamp = $logLevelTimestamp.val();
        options.outputLogAsText = $("#outputLogAsText").hasClass("checked");

        var index = -1;
        if($("#logAdventures").hasClass("checked")){
            if(options.logLevel.indexOf("ADVENTURE") == -1)
                options.logLevel.push("ADVENTURE");
        } else {
            index = options.logLevel.indexOf("ADVENTURE");
            index && options.logLevel.splice(index, 1);
        }

        if($("#logQMaster").hasClass("checked")){
            if(options.logLevel.indexOf("QMASTER") == -1)
                options.logLevel.logLevel.push("QMASTER");
        } else {
            index = options.logLevel.indexOf("QMASTER");
            index && options.logLevel.splice(index, 1);
        }

        if($("#logDaily").hasClass("checked")){
            if(options.logLevel.indexOf("DAILY") == -1)
                options.logLevel.push("DAILY");
        } else {
            index = options.logLevel.indexOf("DAILY");
            index && options.logLevel.splice(index, 1);
        }

        if($("#logFavors").hasClass("checked")){
            if(options.logLevel.indexOf("FAVOR") == -1)
                options.logLevel.push("FAVOR");
        } else {
            index = options.logLevel.indexOf("FAVOR");
            index && options.logLevel.splice(index, 1);
        }

        options.set();
    }
}

function saveWeTab(){
    var wed = parseInt($("#worldEventDelay").text());
    if (!isNaN(wed) && options.worldEventDelay != wed) {
        options.worldEventDelay = wed;
    }

    options.weManagerEnabled = $("#weManagerEnabled").hasClass("checked");
    unsafeWindow.worldEvent.config(options.export(["weManagerEnabled", "worldEventDelay"]));
    unsafeWindow.worldEvent.dispatch();

    options.set();
}

function saveMainTab() {

    var bd = parseInt($("#baseDelay").text());
    if (!isNaN(bd) && options.baseDelay != bd) {
        options.baseDelay = bd;
    }

    options.hideLockedBuildings = $("#toggleLockedBuildings").hasClass("checked");
    //options.useLocalDbOnly = $("#toggleLocalDbOnly").hasClass("checked");

    options.debugMode = $("#toggleDebugModes").hasClass("checked");
    options.doTooltips = $("#toggleTooltips").hasClass("checked");
    options.appendLastSeen = $("#toggleLastSeen").hasClass("checked");
    options.neverSpendGold = $("#neverSpendGold").hasClass("checked");
    options.autoBossChallenge = $("#autoBossChallenge").hasClass("checked");
    options.autoQMaster = $("#autoQMaster").hasClass("checked");
    unsafeWindow.bossChallenger.config(options.export(["autoBossChallenge"]));

    var ari = parseInt($("#autoReloadInterval").val());
    if (!isNaN(ari) && options.autoReloadInterval !== ari) {
        options.autoReloadInterval = ari;
        toggleReloadWindow();
    }

    var aci = parseInt($("#autoCollectInterval").val());
    if (!isNaN(aci) && options.autoCollectInterval !== aci) {
        options.autoCollectInterval = aci;
        toggleAutoCollect();
    }

    options.avaSendSubcamps = $("#avaSendSubcamps").hasClass("checked");

    options.boonsSortBy = $("#boonsSortBy").val();
    options.boonsSortBy2 = $("#boonsSortBy2").val();

    options.shopSortBy = $("#shopSortBy").val();
    options.shopSortBy2 = $("#shopSortBy2").val();

    options.sendAllAction = $("#sendAllAction").val();

    options.defaultBattle = $("#default_battle").val();
    options.defaultTrade = $("#default_trade").val();
    options.defaultIntrigue = $("#default_intrigue").val();

    options.set();
    inject.constants();
    unsafeWindow.sort();
}

function saveQueueTab() {
    if ($("#credits_roll").is(":hidden")) {
        return;
    }

    options.superiorMaterials = $("#toggleSuperiorMaterials").hasClass("checked");
    options.doSpeedUp = $("#toggleDoSpeedUp").hasClass("checked");

    var qd = parseInt($("#queueDelay").text());
    if (!isNaN(qd) && options.queueDelay !== qd) {
        options.queueDelay = qd;
    }

    var qti = parseInt($("#queueTimerInterval").val());
    if (!isNaN(qti) && options.queueTimerInterval !== qti) {
        options.queueTimerInterval = qti;
        toggleQueueTimer();
    }

    unsafeWindow.production.config(options.export(["queueDelay", "superiorMaterials", "doSpeedUp"]));

    options.set();
    inject.constants();
}

function saveBruteTab() {
    if ($("#credits_roll").is(":hidden")) {
        return;
    }

    var bWounds = parseInt($("#bruteWounds").text());
    if (!isNaN(bWounds)) {
        options.bruteWounds = bWounds;
    }

    var bSwitch = $("#bruteSwitchOff").find("a.btngold");
    options.bruteSwitchOff = bSwitch.text() == "switch off";

    var attack = $("#selectedAction").val();
    if(attack) options.selectedAction = attack == "none" ? null : attack;

    options.set();
    inject.constants();
    //unsafeWindow.sort();
}

$("#credits_roll").on('click', "#infoBtn", info_onclick);
function info_onclick() {
    getSwornSwords();
    $("#extenderTabContent").html(templates.ssInfo(options.swornSwords));
}

$("#credits_roll").on('click', "#configLogBtn", config_onclick);
function config_onclick() {
    log("Configure log level.");

    $("#logContent").html(templates.configLog(options));
}

$("#credits_roll").on('click', "#resetOptions", resetOptions_click);
function resetOptions_click(e) {
    e.preventDefault();

    options.reset();
    inject.constants();
    unsafeWindow.sort();
    toggleAll();

    $("#credits_page").empty();
    $("#credits_roll").hide();
    inform("Options reset.");
}
// <-- Settings handling

//--> Brute force adventure
$("#modal_dialogs_top").on("click", "#speedupbtn", viewAdventure_onclick);
function viewAdventure_onclick() {
    log("View adventure details.");

    var vBtn = $(this).find("a.btngold");
    if (!vBtn || vBtn.text() != "View Results!") {
        return;
    }

    setTimeout(function () {

        var btn = $("#bruteBtn");
        var container = $(".challengerewards .challengerewarditems:first");
        if (container.length > 0 && btn.length == 0) {
            container.after(templates.bruteBtn);
        }
    }, (options.baseDelay / 2) * 1000);
}

$("#quests_container").on("click", "span#bruteBtn.btnwrap.btnmed", brute_onclick);
$("#credits_roll").on("click", "span#bruteBtn.btnwrap.btnmed", brute_onclick);
$("#credits_roll").on("click", "span#bruteAllBtn.btnwrap.btnmed", brute_onclick);

function brute_onclick() {
    //    log("Brute!");

    // Save settings first
    saveBruteTab();

    // Find button text
    var b = $(this).find("a.btngold");

    if (!b || b.length == 0) {
        warn("Cannot find brute button!");
    }

    unsafeWindow.brutingImmediateTermination = false;
    b.text("Bruting...");

    if (this.id == "bruteAllBtn") {
        // Brute all sworn swords adventure...
        unsafeWindow.bruteSendAll();
    } else {
        // Else, brute adventure...
        unsafeWindow.bruteForce(true);
    }

}

function getSwornSwords() {

    try {
        var ss = [];
        var pi = unsafeWindow.playerInventory;
        for (var i = 0; i < pi.length; i++) {
            var s = pi[i];
            if (s.slot == "Sworn Sword") {
                ss.push(s);
            }
        }

        if (ss.length > 0) {
            options.swornSwords = ss;
            options.set("swornSwords");
        }
    } catch(err) {
        error('Sworn swords retrieval failed: ' + err);
    }
}
// <-- Brute force adventure

// Do adventures anytime <DEPRECATED>
//$("#modal_dialogs_top").on("click", ".adventurebox .adventuremenu .adventurescroll .adventureitem.locked", lockedAdventure_onclick);
//function lockedAdventure_onclick(e) {
//    log("Trying to unlock adventure.");
//
//    try {
//        e.preventDefault();
//        e.stopPropagation();
//
//        var id = this.id;
//        var aid = id.replace("adventure_", "");
//
//        // console.debug(id, aid);
//
//        unsafeWindow.chooseAdventure(aid);
//    } catch (e) {
//        error(e);
//    }
//
//}

$("#modal_dialogs_top").on('click', ".inventorytabwrap", building_tab_onclick);
function building_tab_onclick(){
    //$("#buildingQueueTab_inner").addClass("active");
    if(this.id == "buildingQueueTab"){
        $("#building_tab_main").hide();
        $("#upgradetab_inner").removeClass("active");
        $("#building_tab_prod").hide();
        $("#productiontab_inner").removeClass("active");
        $("#building_tab_queue").show();
        $("#buildingQueueTab_inner").addClass("active");

        unsafeWindow.production.render(unsafeWindow.userContext.activeBuildingPanel);

    } else if ($("#building_tab_queue").is(":visible")){
        $("#building_tab_queue").hide();
        $("#buildingQueueTab_inner").removeClass("active");
    }

}

$("#building_items").on('click', ".unlocked", upgradetab_changed);
$("#modal_dialogs_top").on('click', ".buildingupgradetree .upgradeicon", upgradetab_changed);
function upgradetab_changed() {
    //    log('Upgrade description changed.');

    if(this.id == "bc_619")
        return;

    var btn = $("#upgradeQueue");
    var container = $("#selected_upgrade");
    if (container.length > 0 && btn.length == 0) {
        container.append(templates.queueUpgradeBtn);

        //if(!$("#exUpgradeContainer").length) {
        //    var html = $(".upgradeinfobg").html();
        //    html = '<div id="exUpgradeContainer" style="height: 250px; overflow-y: scroll;">' + html + '</div>';
        //    $(".upgradeinfobg").html(html);
        //}
    }

    var tab = $("#buildingQueueTab");
    container = $("div#chartabmenu div.charactertabs");
    if (container.length > 0 && tab.length == 0) {
        container.append(templates.buildingQueueTabBtn);
        $("#building_tab_prod").after(templates.buildingTabQueue());
    }
}

$("#modal_dialogs_top").on('click', ".production .productionrow", productiontab_onchange);
function productiontab_onchange() {
    //    log('Production view changed.');

    var btns = $(".production .craftbox .statviewbtm:visible .btnwrap.btnmed.equipbtn.queue:visible");
    var container = $(".production .craftbox .statviewbtm:visible");
    if (container.length > 0 && btns.length == 0) {
        container.prepend(templates.queue5Btn);
        container.prepend(templates.queueBtn);
    }
}

//$("#modals_container").on("click", "#hudchatbtn", warmap_onclick);
$("#modals_container").on("click", ".messagetab", warmap_onclick);
//$("#modals_container").on("click", "div.avaregions a.avaregion", warmap_onclick);

function warmap_onclick(e) {
    e.preventDefault();
    e.stopPropagation();

    if (this.id !== "messagetab-wars") {
        $("#ex_search_row").remove();
        return;
    }

    setTimeout(function () {

        var row = $("#ex_search_row");
        var container = $("div#alliance_main_content");
        if (container.length > 0 && row.length == 0) {
            container.after(templates.searchAllianceBtn);
        }
    }, (options.baseDelay / 2) * 1000);
}

$("#modal_dialogs_top").on('click', "[onclick*='pvpIncomingAttacks']", inspectIncoming);
function inspectIncoming(e) {
    e.preventDefault();

    ajax({
        method: "GET",
        url: "/play/incoming_attacks",
        success: function (a) {
            try {
                // console.debug(a);
                $('div.perkscroll div.achiev-content').each(function () {
                    var id = /[0-9]+/.exec($(this).find('div.increspond').attr('onclick'));

                    var attack = a.attacks.filter(function (e) {
                        return e.camp_attack_id === null ? e.pvp_id == id : e.camp_attack_id == id;
                    })[0];

                    if (!attack)
                        return;

                    $(this).find("span.charname").attr("onclick", "return characterMainModal(" + attack.attacker.user_id + ")");
                    $(this).find("span.charportrait").attr("onclick", "return characterMainModal(" + attack.attacker.user_id + ")");
                    //$(this).find("span.targetalliancename").attr("onclick", "return allianceInfo(" + attack.alliance_id + ")");

                    if(!options.appendLastSeen)
                        return;

                    var text = 'Last seen: ' + moment(attack.attacker.updated_at,"YYYY-MM-DD HH:mm:ss Z").local().format('MMMM Do YYYY, h:mm:ss a');
                    $(this).append('<div class="ex_attack_timestamp">' + text + '</div>');
                });

                unsafeWindow.$(".scroll-pane:visible").jScrollPane();

            } catch (e) {
                error(e);
            }
        }
    });
}

$("#modal_dialogs_top").on('click', "[onclick*='pvpOutgoingAttacks']", inspectOutgoing);
function inspectOutgoing(e) {
    e.preventDefault();

    ajax({
        method: "GET",
        url: "/play/outgoing_attacks",
        success: function (a) {
            try {
                // console.debug(a);
                $('div.perkscroll div.achiev-content:visible').each(function () {
                    var id = /[0-9]+/.exec($(this).find('div.increspond').attr('onclick'));

                    var attack = a.attacks.filter(function (e) {
                        return e.camp_attack_id === null ? e.pvp_id == id : e.camp_attack_id == id;
                    })[0];

                    if (!attack || !attack.defender)
                        return;

                    $(this).find("span.charname").attr("onclick", "return characterMainModal(" + attack.defender.user_id + ")");
                    $(this).find("span.charportrait").attr("onclick", "return characterMainModal(" + attack.defender.user_id + ")");
                    //$(this).find("span.targetalliancename").attr("onclick", "return allianceInfo(" + attack.alliance_id + ")");

                    if(!options.appendLastSeen)
                        return;

                    var text = 'Last seen: ' + moment(attack.defender.updated_at,"YYYY-MM-DD HH:mm:ss Z").local().format('MMMM Do YYYY, h:mm:ss a');
                    $(this).append('<div class="ex_attack_timestamp">' + text + '</div>');
                });

                unsafeWindow.$(".scroll-pane:visible").jScrollPane();

            } catch (e) {
                error(e);
            }
        }
    });
}
$("#modal_dialogs_top").on('click', "[onclick*='pvpStartWithTarget']", inspectTarget);
$("#modal_dialogs_top").on('click', "[onclick*='pvpTargetSelected']", inspectTarget);
function inspectTarget(e) {
    e.preventDefault();
    //console.debug(e, this, $(this));

    var func = $(this).attr("onclick");
    var id = func.indexOf("pvpTargetSelected") > -1
        ? func.substring(func.indexOf(",") + 1, func.indexOf(")"))
        : func.substring(func.indexOf("(") + 1, func.indexOf(")"));

    if(!id){
        warn("Could not retrieve player id. Exiting...");
        return;
    }

    ajax({
        method: "GET",
        url: "/play/character_pvp/" + id,
        success: function (a) {
            try {
                // console.debug(a);

                var lastUpdated =
                    a.defender.armor ? a.defender.armor.updated_at
                        : a.defender.weapon ? a.defender.weapon.updated_at
                        : a.defender.companion ? a.defender.companion.updated_at
                        : null;

                if (!lastUpdated) {
                    warn("Could not establish when was the user last seen.");
                    return;
                }

                var $h2 = $(".infobar:visible:first").find("h2");
                if($h2.length){
                    $h2.css("padding-left", "140px");
                    $h2.html('Last seen: ' + moment(lastUpdated, "YYYY-MM-DD HH:mm:ss Z").local().format('MMMM Do YYYY, h:mm:ss a'));
                }

            } catch (e) {
                error(e);
            }
        }
    });
}
$("#modals_container").on("click", "[onclick*='campActivity']", campactivity_onclick);
function campactivity_onclick(){
    log("Camp activity requested.");

    var oncl = $(this).attr("onclick");

    var startIdx = oncl.indexOf('campActivity(') + 13;
    var endIdx = oncl.lastIndexOf(');');

    var params = oncl.substring(startIdx, endIdx).split(',');
    var campId = params[0];
    var category = params[1];   // 1 - provisions, // 2 - action, // 3 - garrison
    if(!campId || !category){
        error("Failed resolving camp id and category.", "CAMP");
        return;
    }

    //console.debug("Parameters resolved, camp id: ", campId, " category: ", category);
    switch(category){
        case '2':
            analyzeActions(campId);
            break;
        case '1':
        case '3':
        default:
            log("No actions implemented for this category.");
            return;
    }
}

function analyzeActions(camp){
    ajax({
        url: "/play/camp_messages/" + camp + "?category=2",
        success: function(a) {
            var contents = $(".achiev-box.action-battle .achiev-content:visible");
            if(contents.length !== a.length){
                warn("Wrong response format.", "CAMP");
                return;
            }

            var i = 0;
            contents.each(function(){
                var initiator = $(this).find(".activityinitiator h3 span");
                var target = $(this).find(".activitytarget h3 span");

                if(initiator.length){
                    initiator.attr("onclick", "return allianceInfo(" + a[i].camp_attack.alliance_id + ")");
                    initiator.before('<span onclick="return characterMainModal(' + a[i].user_id + ')">' + a[i].name + '</span>');

                    // Assign dirrect assault link to their camp
                    $(this).find(".activityinitiatorcamp h3 span").attr("onclick", "campChoseTarget(" + a[i].camp_attack.region + ", " + a[i].camp_attack.subregion + ", " + a[i].camp_attack.alliance_id + ")" );

                } else if (target.length) {
                    target.attr("onclick", "return allianceInfo(" + a[i].camp_attack.target_alliance_id + ")");
                    //target.before('<span onclick="return characterMainModal(' + a[i].user_id + ')">' + a[i].name + '</span>');

                    // Assign dirrect assault link to their camp
                    $(this).find(".activitytargetcamp h3 span").attr("onclick", "campChoseTarget(" + a[i].camp_attack.region + ", " + a[i].camp_attack.subregion + ", " + a[i].camp_attack.target_alliance_id + ")" );
                }

                i++;
            });

        }
    })
}

$("#modals_container").on("click", "#ex_alliance_search", searchAlliance_onclick);
function searchAlliance_onclick(e) {
    e.preventDefault();
    unsafeWindow.showSpinner();

    window.setTimeout(function () {

        var keys = $("#ex_alliance_search_input").val();
        if (!keys || keys.length == 0) {
            return;
        }

        var keysArray = keys.split(" ");
        var c = keysArray[0];
        for (var i = 1; i < keysArray.length; i++) {
            c += "+" +
            keysArray[i];
        }

        //console.debug("Sending data: ", c);

        GM_xmlhttpRequest({
            method: "GET",
            url: "/play/alliance_search/?tags=0&name=" + c,
            onload: function (a) {
                unsafeWindow.hideSpinner();

                if (a.error) {
                    var e = "Something went awry with your search. Please try again.";
                    "query-error" == e.error && (e = "Alliance Search by name requires more than 3 characters.");
                    unsafeWindow.doAlert("Alliance Search Error!", e);
                } else {
                    //console.debug("Raw response: ", a);
                    var response = JSON.parse(a.responseText);
                    //console.debug("Response text parsed: ", response);
                    displayResults(response.alliances);
                }
            }
        });

    }, (options.baseDelay / 2) * 1000);
}

function displayResults(a) {
    console.debug("Alliances to be displayed: ", a);

    $("#ex_alliance_search_input").val("");

    if (!(a instanceof Array) || a.length == 0) {
        $("#ex_alliance_search_input").attr("placeholder", "No alliances found");
        return;
    }

    // Clean table
    $(".avaranking:visible:first tr:not(:first)").empty();

    // Fill table
    var l = a.length > 4 ? 4 : a.length;
    for (var i = 0; i < l; i++) {
        $(".avaranking:visible:first tr:first").after(templates.allianceRow(a[i], unsafeWindow.userContext.activeRegion));
    }

}

$("#modal_dialogs_top").on('click', "[onclick*='chooseAdventure']", adventureItem_onclick);
function adventureItem_onclick() {
    //log("Adventures display.");

    setTimeout(function () {

        var btn = $("#adventureSendAll");
        var container = $("div.infobar span#backbtn.btnwrap.btnsm:visible");
        if (container.length > 0 && btn.length == 0) {
            container.after(templates.sendAllBtn);
        }
    }, (options.baseDelay / 2) * 1000);
}

$("#modals_container").on('click', "[onclick*='pvpTargetSelected']", pvpStart_onclick);
$("#modals_container").on('click', "[onclick*='pvpStartWithTarget']", pvpStart_onclick);
function pvpStart_onclick() {
    //log('Displaying pvp dialog with a target specified.');

    setTimeout(function () {

        var btn = $("#pvpSendAll");
        var container = $("div.infobar span#backbtn.btnwrap.btnsm:visible");
        if (container.length > 0 && btn.length == 0) {
            container.after(templates.pvpSendAllBtn);
        }
    }, (options.baseDelay / 2) * 1000);
}

$(document).on('click', "[onclick*='campChoseTarget']", avaStart_onclick);
function avaStart_onclick() {
    log('Displaying ava dialog with a target specified.');

    setTimeout(function () {

        var btn = $("#avaSendAll");
        var container = $("div.infobar span#backbtn.btnwrap.btnsm:visible");
        if (container.length > 0 && btn.length == 0) {
            container.after(templates.avaSendAllBtn);
        }
    }, (options.baseDelay / 2) * 1000);
}

$('#modal_dialogs').on('click', '#shop_offer_btn', function(e){
    log('Displaying offer code dialog.');

    console.debug(options.lastIdChecked)
    setTimeout(function () {

        var btn = $("#find_gifts");
        var container = $("div#modals_container_alerts div.alertcontents div.alertbox div.alertboxinner div.alertinput");
        if (container.length > 0 && btn.length == 0) {
            container.after(templates.findGifts(options.lastIdChecked, 100));
        }
    }, (options.baseDelay / 2) * 1000);
});

function checkSource() {
    console.log("-------------------------------------\\");
    console.log("Script scheduled for update check.");
    console.log("-------------------------------------/");

    var lastUpdateCheck = GM_SuperValue.get("lastUpdateCheck", "never");
    console.log("Function definitions updated: " + lastUpdateCheck);
    console.log("Source control check for integrity initiated...");
    var updateRequired = false;

    eval(GM_getResourceText("original"));
    if (typeof original == "undefined") {
        error("Cannot find original function data.");
        return;
    }

    try {

        for (var fn in original) {
            console.log("Current function: " + fn);

            if (!original.hasOwnProperty(fn)) {
                console.error("Function does not have a stored value!");
                continue;
            }

            console.log("Retrieving page function...");

            if (!unsafeWindow[fn]) {
                console.error("No such function on page!");
                continue;
            }

            var pageFn = unsafeWindow[fn].toString();
            console.log("Function retrieved. Comparing...");

            if (pageFn !== original[fn]) {
                console.warn("Changes detected! Please revise: " + fn);

                updateRequired = true;
                continue;
            }

            console.log("No changes were detected. Continuing...");
        }
    } catch (e) {
        alert("Source control encountered an error: " + e);
        return;
    }

    console.log("-------------------------------------|");
    console.log("-------------------------------------| > End of script update check");

    if (!updateRequired) {
        GM_SuperValue.set("lastUpdateCheck", new Date());
    }

    alert("Source control resolved that " +
    (updateRequired ? "an update is required." : "no changes are necessary.") +
    "\nSee the console log for details.\nPress OK to reload again.");
}

// jQuery ajax
function ajax(params) {
    if (typeof params != "object") {
        error("The request requires object with parameters.");
        return;
    }

    // Required
    if (!params.url) {
        error("Request url was not passed.");
        return;
    }

    // Required
    if (!params.onload && !params.success) {
        error("Callback handler missing. Cannot execute.");
        return;
    }

    if (!params.type) {
        params.type = "GET";
    }

    if (!params.timeout) {
        params.timeout = 3E4;
    }

    if (!params.onerror) {
        params.onerror = function (gme) {
            error("Error occurred while running the request. Details:");
            console.debug("Original ajax request parameters: ", params);
            console.debug("GM_XmlHttpRequest error response: ", gme);
        }
    }

    if (!params.onload) {
        params.onload = function (gmr) {

            var response;
            if (!gmr.response) {
                params.error ? params.error(gmr) : params.onerror(gmr);
            } else {
                //var headerString = gmr.responseHeaders;
                //var headers = headerString.split('\n');

                //console.debug("Debugging response headers: ", headers);
                if (gmr.responseHeaders.indexOf("Content-Type: application/json;") > -1) {
                    response = JSON.parse(gmr.responseText);
                    params.success(response);
                } else {
                    params.success(gmr.responseText);
                }
            }

            if (params.complete)
                params.complete(response);
        };
    }

    if (!params.ontimeout) {
        params.ontimeout = function (gmt) {
            warn("The request timed out. Details:");
            console.debug("Original ajax request parameters: ", params);
            console.debug("Grease monkey error response: ", gmt);
        };
    }

    window.setTimeout(function () {
        GM_xmlhttpRequest({
            //binary: false,
            //context: {},
            //data: "",
            //headers: {},
            method: params.type,
            //onabort: params.onabort,
            onerror: params.onerror,
            onload: params.onload,
            //onprogress: params.onprogress,
            //onreadystatechange: params.onreadystatechange,
            ontimeout: params.ontimeout,
            //overrideMimeType: "",
            //password: "",
            //synchronous: false,
            timeout: params.timeout,
            //upload: {},
            url: params.url
            //user: ""
        });
    }, 1E3);
}

$("div#page-wrap").on('click', 'a#navlink-buildings', function(){
    log("Navigated to buildings sub-menu.");
    if(options.hideLockedBuildings){
        $(".locked").hide();
    } else {
        $(".locked:hidden").show();
    }
});