Source: dew.js

/**
 * Creates a new DewError object which represents an ElDewrito communication error.
 *
 * @class
 * @param {string} message - The error message.
 * @param {DewErrorCode} [code] - The error code.
 * @param {string} [method] - The internal method name.
 */
function DewError(message, code, method) {
    Error.captureStackTrace(this, this.constructor);

    /**
     * @member {string}
     */
    this.name = this.constructor.name;

    /**
     * The error message.
     * @member {string}
     */
    this.message = message || "";

    /**
     * The error code.
     * @member {DewErrorCode}
     */
    this.code = (typeof code === "number") ? code : DewErrorCode.UNKNOWN_ERROR;

    /**
     * The internal method name.
     * @member {string}
     */
    this.method = method || "";
}

/**
 * Gets the name of the error's code.
 *
 * @returns {string} A string representing the error's code.
 */
DewError.prototype.getCodeName = function () {
    for (var name in DewErrorCode) {
        if (DewErrorCode.hasOwnProperty(name) && DewErrorCode[name] === this.code) {
            return name;
        }
    }
    return this.code.toString();
}

/**
 * Error codes.
 *
 * @readonly
 * @enum {number}
 */
DewErrorCode = {
    /**
     * The method ran successfully.
     */
    OK: 0,

    /**
     * An unknown error occurred while running the method.
     */
    UNKNOWN_ERROR: 1,

    /**
     * A JavaScript exception occurred while running the method.
     */
    JS_EXCEPTION: 2,

    /**
     * A game communication error occurred.
     */
    BAD_QUERY: 3,

    /**
     * The method is not supported by the game.
     */
    UNSUPPORTED_METHOD: 4,

    /**
     * An invalid argument was passed to the method.
     */
    INVALID_ARGUMENT: 5,

    /**
     * An error occurred while performing a network operation.
     */
    NETWORK_ERROR: 6,

    /**
     * The requested information is not available.
     */
    NOT_AVAILABLE: 7,

    /**
     * The console command failed to execute successfully.
     */
    COMMAND_FAILED: 8
};

/**
 * Multiplayer event audiences.
 *
 * @readonly
 * @enum {number}
 */
MPEventAudience = {
    CAUSE_PLAYER: 0,
    CAUSE_TEAM: 1,
    EFFECT_PLAYER: 2,
    EFFECT_TEAM: 3,
    ALL: 4
};

/**
 * Multiplayer event categories.
 *
 * @readonly
 * @enum {number}
 */
MPEventCategory = {
    GENERAL: 0,
    FLAVOR: 1,
    SLAYER: 2,
    CTF: 3,
    ODDBALL: 4,
    FORGE: 5,
    KOTH: 6,
    VIP: 7,
    JUGGERNAUT: 8,
    TERRITORIES: 9,
    ASSAULT: 10,
    INFECTION: 11,
    SURVIVAL: 12,
    WP: 13
};

/**
 * Game modes.
 *
 * @readonly
 * @enum {number}
 */
GameMode = {
    NONE: 0,
    CTF: 1,
    SLAYER: 2,
    ODDBALL: 3,
    KOTH: 4,
    FORGE: 5,
    VIP: 6,
    JUGGERNAUT: 7,
    TERRITORIES: 8,
    ASSAULT: 9,
    INFECTION: 10
};

/**
 * Console command and variable types.
 *
 * @readonly
 * @enum {number}
 */
CommandType = {
    /**
     * A command.
     */
    COMMAND: 0,

    /**
     * An integer variable.
     */
    INT: 1,

    /**
     * A 64-bit integer variable.
     */
    INT64: 2,

    /**
     * A floating-point variable.
     */
    FLOAT: 3,

    /**
     * A string variable.
     */
    STRING: 4
};

(function () {
    window.dew = window.dew || {};

    // Default 1:1 result mapping.
    function defaultResultMapping(result) {
        return result;
    }

    // JSON result mapping.
    function jsonResultMapping(result) {
        return JSON.parse(result);
    }

    // Calls a native method and returns a Promise for it.
    dew.callMethod = function (method, args, resultMapping) {
        resultMapping = resultMapping || defaultResultMapping;
        args = args || {};
        return new Promise(function (resolve, reject) {
            // Ensure that we have a function to send queries to ED
            if (!window.dewQuery) {
                reject(new DewError("Unsupported method: window.dewQuery() is not available", DewErrorCode.UNSUPPORTED_METHOD, method));
                return;
            }

            // If args is a function, run it to get the actual arguments
            // This is useful in order to ensure that JS exceptions cause the promise to fail.
            if (typeof args === "function") {
                args = args();
            }

            // Send a CEF query
            window.dewQuery({
                request: JSON.stringify({
                    method: method,
                    args: args
                }),
                persistent: false,
                onSuccess: function (resultStr) {
                    // Try to map the result string and resolve the promise with it
                    var value;
                    try {
                        value = resultMapping(resultStr);
                    } catch (e) {
                        reject(e);
                        return;
                    }
                    resolve(value);
                },
                onFailure: function (code, message) {
                    if(method=="command"){
                        method = method+": "+args.command;
                    }
                    reject(new DewError(message, code, method));
                }
            });
        });
    }

    // Registers an event handler for a UI event.
    function registerEvent(name, callback) {
        window.addEventListener("message", function (event) {
            var origin = event.origin;
            if (!origin.startsWith("dew:")) {
                return; // Only recognize messages sent by the UI
            }
            var data = event.data;
            if (typeof data !== "object" || !("event" in data)) {
                return;
            }
            if (data.event === name) {
                callback(data);
            }
        }, false);
    }

    // Posts a message to the UI.
    function postUiMessage(message, data) {
        // If this is running in the UI layer, then post to the current window, otherwise post to the parent
        var targetWindow = (window.location.href.startsWith("dew://ui/")) ? window : window.parent;
        if (targetWindow) {
            targetWindow.postMessage({
                message: message,
                data: data
            }, "*");
        }
    }

    // Disable text selection by default since most screens won't want it
    window.addEventListener("load", function (event) {
        document.body.style["-webkit-user-select"] = "none";
    });

    /**
     * Methods for interfacing with ElDewrito.
     *
     * @namespace dew
     */

    /**
     * A promise made by an asynchronous ElDewrito method.
     *
     * If the promise is rejected for a reason related to ElDewrito, it will be rejected with a {@link DewError} object.
     * This object includes an [error code]{@link DewErrorCode} which can be used to easily figure out what went wrong.
     *
     * If the promise is rejected for any other reason, it may be rejected with a generic Error.
     * Therefore, you must use `instanceof DewError` to check that the error is actually an ElDewrito error before getting specific information.
     *
     * @typedef {Promise<*|Error>} DewPromise
     */

    /**
     * (ASYNCHRONOUS) Retrieves the current version of ElDewrito.
     *
     * @returns {DewPromise<string>} - A promise for the version string.
     */
    dew.getVersion = function () {
        return dew.callMethod("version");
    }

    /**
     * Requests to show a screen.
     *
     * If the requested screen is still loading, it will not be shown until it finishes.
     *
     * This will always send a [show]{@link event:show} event to the screen, even if it is already visible.
     * You can use this as a simple means of sending messages between screens.
     *
     * @param {string} [id] - The ID of the screen to show. If this is null or omitted, the current screen will be shown.
     * @param {object} [data] - Data to pass to the screen's [show]{@link event:show} event.
     */
    dew.show = function (id, data) {
        postUiMessage("show", {
            screen: id || null,
            data: data || {}
        });
    }

    /**
     * Requests to hide a screen.
     *
     * This will send a [hide]{@link event:hide} event to the screen if it is visible.
     *
     * @param {string} [id] - The ID of the screen to hide. If this is null or omitted, the current screen will be hidden.
     */
    dew.hide = function (id) {
        postUiMessage("hide", {
            screen: id || null
        });
    }
	
	/**
     * Sends a dew event to another screen
     *
	 * @param {string} [eventName] - The name of the event for another screen to receive.
	 * @param {object} [data] - Data to pass to the screen.
     */
	dew.notify = function(eventName, data){
		postUiMessage(eventName, {
			data: data || {}
		});
	}

    /**
     * Requests to change this screen's input capture state.
     *
     * @param {boolean} capture - true to capture mouse and keyboard input, false to release.
     */
    dew.captureInput = function (capture) {
        postUiMessage("captureInput", {
            capture: !!capture
        });
    }
	
	/**
     * Requests to change this screen's pointer capture state.
     * This is overridden by dew.captureInput
     *
     * @param {boolean} capture - true to only capture mouse input, false to release.
     */
    dew.capturePointer = function (capture) {
        postUiMessage("capturePointer", {
            capture: !!capture
        });
    }

    /**
     * (ASYNCHRONOUS) Runs a console command.
     *
     * If the command does not run successfully, the promise will be rejected with a [DewErrorCode.COMMAND_FAILED]{@link DewErrorCode} error.
     * The error message will be the command output.
     *
     * @param {string} command - The command and its arguments, separated by spaces.
     * @param {object} [options] - Additional options that control how the command should run.
     * @param {boolean} [options.internal=false] - If set to true, then internal commands can be executed.
     * @returns {DewPromise<string>} A promise for the command output.
     */
    dew.command = function (command, options) {
        return dew.callMethod("command", function () {
            options = options || {};
            return {
                command: command.toString(),
                internal: (typeof options.internal === "boolean") ? options.internal : false
            };
        });
    }

    /**
     * (ASYNCHRONOUS) Pings a server.
     *
     * The screen will receive a [pong]{@link event:pong} event if the server responds.
     *
     * **This method is currently broken and will report ping times that are much higher than they should be.**
     *
     * @param {string} address - The IPv4 address of the server to ping. Must not include a port number.
     * @returns {DewPromise} A promise that will be resolved once the ping is sent.
     */
	 dew.ping = function (address, port=11774) {
        return dew.callMethod("ping", function () {
            return {
                address: address.toString(),
				port: port
            };
        });
    }

    /**
     * (ASYNCHRONOUS) Gets info about the current map variant.
     *
     * If map variant info is not available, the promise will be rejected with a [DewErrorCode.NOT_AVAILABLE]{@link DewErrorCode} error.
     *
     * @returns {DewPromise<MapVariantInfo>} A promise for the map variant info.
     */
    dew.getMapVariantInfo = function () {
        return dew.callMethod("mapVariantInfo", {}, jsonResultMapping);
    }

    /**
     * Contains information about a map variant.
     *
     * @typedef {object} MapVariantInfo
     * @property {string} name - The name. Can be empty.
     * @property {string} description - The description. Can be empty.
     * @property {string} author - The author. Can be empty.
     * @property {number} mapId - The map ID.
     * @see dew.getMapVariantInfo
     */

    /**
     * (ASYNCHRONOUS) Gets info about the current game variant.
     *
     * If game variant info is not available, the promise will be rejected with a [DewErrorCode.NOT_AVAILABLE]{@link DewErrorCode} error.
     *
     * @returns {DewPromise<GameVariantInfo>} A promise for the game variant info.
     */
    dew.getGameVariantInfo = function () {
        return dew.callMethod("gameVariantInfo", {}, jsonResultMapping);
    }

    /**
     * Contains information about a game variant.
     *
     * @typedef {object} GameVariantInfo
     * @property {GameMode} mode - The base game mode.
     * @property {string} name - The name. Can be empty.
     * @property {string} description - The description. Can be empty.
     * @property {string} author - The author. Can be empty.
     * @property {boolean} teams - `true` if teams are enabled.
     * @property {number} timeLimit - The time limit in minutes (0 if unlimited).
     * @property {number} rounds - The number of rounds.
     * @property {number} scoreToWin - The score-to-win (-1 if unlimited).
     * @see dew.getGameVariantInfo
     */

    /**
     * (ASYNCHRONOUS) Gets a list of available console commands.
     *
     * @returns {DewPromise<ConsoleCommand[]>} A promise for the list of available console commands.
     */
    dew.getCommands = function () {
        return dew.callMethod("commands", {}, jsonResultMapping);
    }

    /**
     * Contains information about a console command and its current state.
     *
     * @typedef {object} ConsoleCommand
     * @property {CommandType} type - The type of the command or variable.
     * @property {string} module - The module name.
     * @property {string} name - The name of the command or variable. This includes the module prefix.
     * @property {string} shortName - The short name of the command or variable.
     * @property {string} description - A description to display in a help listing.
     * @property {*} value - The current value of the variable. For commands, this will be `null`.
     * @property {*} defaultValue - The default value of the variable. For commands, this will be `null`.
     * @property {*} minValue - The minimum value of the variable. For string variables and commands, this will be `null`.
     * @property {*} maxValue - The maximum value of the variable. For string variables and commands, this will be `null`.
     * @property {boolean} replicated - `true` if the variable should be synchronized to clients.
     * @property {boolean} archived - `true` if the variable should be saved when the config file is written.
     * @property {boolean} hidden - `true` if the command or variable should be omitted from a help listing.
     * @property {boolean} hostOnly - `true` if the command or variable can only be used by the game host.
     * @property {boolean} hideValue - `true` if the variable's value should be omitted from a help listing.
     * @property {boolean} internal - `true` if the command or variable can only be set internally.
     * @property {string[]} arguments - A list of arguments for the command. Each string will contain a value name, a space, and then a description. For variables, this will be empty.
     * @see dew.getCommands
     */

    /**
     * (ASYNCHRONOUS) Sends a chat message.
     *
     * @param {string} message - The chat message to send.
     * @param {boolean} teamChat - If true the message is sent to team chat instead of global.
     * @returns {DewPromise<boolean>} A promise for the success of sending the message.
     */
    dew.sendChat = function (message, teamChat) {
        return dew.callMethod("sendChat", function () {
            return {
                message: message.toString(),
                teamChat: teamChat
            };
        });
     }

    /**
     * (ASYNCHRONOUS) Gets information about the current game multiplayer session.
     *
     * @param {string} message - The chat message to send.
     * @param {boolean} teamChat - If true the message is sent to team chat instead of global.
     * @returns {DewPromise<SessionInfo>} A promise for the game multiplayer session information.
     */
    dew.getSessionInfo = function () {
        return dew.callMethod("sessionInfo", {}, jsonResultMapping);
    }

    /**
     * Contains information about the current game multiplayer session
     *
     * @typedef {object} SessionInfo
     * @property {boolean} established - `true` if a session is established.
     * @property {boolean} hasTeams - `true` if the current session has teams.
     * @property {boolean} isHost - `true` if the player is the host of the session.
     * @property {string} mapName - Name of the currently loaded map.
     * @see dew.getSessionInfo
     */

    /**
     * (ASYNCHRONOUS) Gets information about a player's stats.
     *
     * @param {string} playerName - The name of the player.
     * @returns {DewPromise<StatsInfo>} A promise for the player's stats.
     */
     dew.getStats = function (playerName) {
         return dew.callMethod("stats", { playerName: playerName }, jsonResultMapping);
     }

    /**
     * Contains information about the player's stats
     *
     * @typedef {object} StatsInfo
     * @property {number[]} medals - The medals earned during this game.
     * @property {WeaponStats[]} weapons - Information about the weapons used during this game.
     * @see dew.getStats
     */

    /**
     * Information about a weapon's use in the current game
     *
     * @typedef {object} WeaponStats
     * @property {number} BetrayalsWith - How many times the player used this weapon to betray their teammates.
     * @property {number} HeadshotsWith - How many headshots the player got with this weapon.
     * @property {number} KilledBy - How many times the player was killed by this weapon.
     * @property {number} Kills - How many kills the player got with this weapon.
     * @property {number} SuicidesWith - How many times the player killed themselves with this weapon.
     * @see StatsInfo
     */

	/**		
     * (ASYNCHRONOUS) Gets information about the current game's scoreboard.		
     *		
     * @returns {DewPromise<ScoreboardInfo>} A promise for the scoreboard information.		
     */	
	 
    dew.getScoreboard = function () {		
       return dew.callMethod("scoreboard", {}, jsonResultMapping);		
    }		
	
   /**		
    * Contains information about the scoreboard.		
    *		
    * @typedef {object} ScoreboardInfo		
    * @property {boolean} hasTeams - `true` if the current session has teams.		
    * @property {number[]} teamScores - The scores of all of the teams in the game.		
    * @property {string} gameType - The gamemode type.		
    * @property {ScoreboardPlayer[]} players - Players listed on the scoreboard.		
    * @see dew.getScoreboard		
    */		
	
   /**		
    * Contains information about a player on the scoreboard.		
    *		
    * @typedef {object} ScoreboardPlayer		
    * @property {string} name - The player's name.		
    * @property {number} team - The player's team index.		
    * @property {string} color - The player's primary armor color.		
    * @property {string} UID - The player's UID.		
    * @property {number} kills - The number of kills.		
    * @property {number} assists - The number of assists.		
    * @property {number} deaths - The number of deaths.		
    * @property {number} score - The player's score.		
    * @see ScoreboardInfo		
    */
	
    /**
     * (ASYNCHRONOUS) Requests to submit the currently-active virtual keyboard.
     * 
     * @param {string} value - The value to submit.
     * @returns {DewPromise} A promise that will be resolved once the keyboard is submitted.
     */
    dew.submitVirtualKeyboard = function (value) {
        return dew.callMethod("submitVirtualKeyboard", function () {
            return {
                value: value.toString(),
            };
        });
    }

    /**
     * (ASYNCHRONOUS) Requests to cancel the currently-active virtual keyboard.
     * 
     * @returns {DewPromise} A promise that will be resolved once the keyboard is cancelled.
     */
    dew.cancelVirtualKeyboard = function (value) {
        return dew.callMethod("cancelVirtualKeyboard");
    }

    /**
     * Registers a callback to be run when an event occurs.
     *
     * @name dew.on
     * @function
     * @param {string} event - The name of the event to register a callback for (e.g. "show").
     * @param {EventCallback} callback - The callback to register.
     * @see event:show
     * @see event:hide
     * @see event:pong
     * @see event:console
     * @see event:mpevent
     * @see event:loadprogress
	 * @see event:signal-ready
     */
    dew.on = function (event, callback) {
        registerEvent(event, callback);
    }
	
	/**
     * (ASYNCHRONOUS) Queues a game action to be pressed in-game
     *
     * @param {integer} actionIndex - The index of the game action to be pressed.
     */
	dew.gameaction = function(actionIndex) {
		return dew.callMethod("gameaction", {
			key: actionIndex
		});
	}
	
	//This is a workaround for a bug in jsdoc. Event definitions must be inside a named function
	dew.functionforevents = function() {
		/**
		 * A callback function for responding to events.
		 *
		 * @callback EventCallback
		 * @param {object} event - The event information.
		 * @param {string} event.event - The name of the event.
		 * @param {string} event.screen - The ID of the screen the event was sent to.
		 * @param {string} event.data - Event-specific data.
		 */

		/**
		 * Fired after the current screen is shown.
		 * The data passed to this event is the data that was passed to {@link dew.show}.
		 *
		 * @event show
		 * @type {object}
		 */

		/**
		 * Fired after the current screen is hidden.
		 *
		 * @event hide
		 * @type {object}
		 */

		/**
		 * Fired when a server replies to a [ping]{@link dew.ping}.
		 * This can only be received while the screen is active.
		 *
		 * @event pong
		 * @type {object}
		 * @property {string} address - The IP address of the server.
		 * @property {number} latency - The round-trip time of the ping in milliseconds.
		 * @property {number} timestamp - A timestamp value representing when the ping was sent.
		 */

		/**
		 * Fired when a line is written to the in-game console.
		 *
		 * @event console
		 * @type {object}
		 * @property {string} line - The line that was written. **Make sure to escape this properly before displaying it.**
		 */

		/**		
		 * Fires when the scoreboard is updated		
		 *		
		 * @event scoreboard		
		 * @type {ScoreboardInfo}		
		 */
	 
		/**
		 * Fired when a multiplayer event occurs that affects the local player.
		 *
		 * @event mpevent
		 * @type {object}
		 * @property {string} name - The internal name of the event.
		 * @property {MPEventCategory} category - The event's category.
		 * @property {MPEventAudience} audience - The audience that the event is intended for.
		 */

		/**
		 * Fired when the loading screen is active and the loading progress changes.
		 *
		 * This event is only sent to visible screens.
		 *
		 * @event loadprogress
		 * @type {object}
		 * @property {number} currentBytes - The current number of bytes that have been decompressed.
		 * @property {number} totalBytes - The total number of bytes that need to be decompressed.
		 */

		/**
		 * Fired when a chat message is received.
		 *
		 * @event chat
		 * @type {object}
		 * @property {string} message - The chat message. **Make sure to escape this properly before displaying it.**
		 * @property {string} sender - The username that sent this message. **Make sure to escape this properly before displaying it.**
		 * @property {string} chatType - The type of message that was received. Can be "GLOBAL", "TEAM", "WHISPER", or "SERVER".
		 * @property {number} teamIndex - The team index of the player. Not included when chatType is "SERVER".
		 * @property {string} UID - The UID of the player. Not included when chatType is "SERVER".
		 * @property {string} color - The hex color of the player's primary armor color. Not included when chatType is "SERVER".
		 */

		 /**
		  * Fired when attempting to connect to a server and when the connection succeeds or fails.
		  *
		  * @event serverconnect
		  * @type {object}
		  * @property {boolean} connecting - True if the client is currently trying to connect to the server.
		  * @property {boolean} success - True if the client successfully connected to the server. Will always be false if connecting is true.
		  */
		  
		 /**
		  * Fired when the game client receives the websocket connection information packet.
		  * 
		  * @event signal-ready
		  * @type {object}
		  * @property {string} server - The ip + port of the signal server in the format ip:port
		  * @property {string} password - The password assigned for the client to connect with
		  */
	}
})();