UNPKG

gameguard-client

Version:

The client-side companion to the GameGuard that communicates with the server to manage the player and their data.

885 lines (748 loc) 22.5 kB
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _classCallCheck$1(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } var classCallCheck = _classCallCheck$1; function _defineProperties$1(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass$1(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties$1(Constructor.prototype, protoProps); if (staticProps) _defineProperties$1(Constructor, staticProps); return Constructor; } var createClass = _createClass$1; function _defineProperty$1(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } var defineProperty = _defineProperty$1; var Task = /*#__PURE__*/function () { /** * The method to be called when processing this task. * * @property {Function} */ /** * Indicates whether this task will only run once before being deleted or not. * * @private * * @property {boolean} */ /** * If true this indicates to Hypergiant that it needs to be deleted on the next pass. * * @private * * @property {boolean} */ /** * The number of times that this task has been called. * * @private * * @property {number} */ /** * Indicates whether this task is currently paused or not. * * @property {boolean} */ /** * @param {Function} fn The method to attach to this task. * @param {boolean} once Indicates whether this task will only run once before being deleted or not. */ function Task(fn, once) { classCallCheck(this, Task); defineProperty(this, "fn", void 0); defineProperty(this, "_once", void 0); defineProperty(this, "_delete", false); defineProperty(this, "_timesCalled", 0); defineProperty(this, "paused", false); this.fn = fn; this._once = once; } /** * Returns whether the task should run only once or not. * * @returns {boolean} */ createClass(Task, [{ key: "run", /** * Runs the method associated with this task. * * @param {...*} args Any other data that should be passed to this task. */ value: function run() { if (this.paused) return; this.fn.apply(this, arguments); this._timesCalled++; if (this._once) this._delete = true; } }, { key: "once", get: function get() { return this._once; } /** * Returns whether the task should be deleted or not. * * @returns {boolean} */ }, { key: "delete", get: function get() { return this._delete; } /** * Returns the number of times that this task has been called. * * @returns {number} */ }, { key: "timesCalled", get: function get() { return this._timesCalled; } }]); return Task; }(); /** * Hypergiant is used to create signals that run a task when emitted. * * One of the biggest advtantages that signals have over native JavaScript * events is that they don't rely on correct typing. */ var Hypergiant = /*#__PURE__*/function () { function Hypergiant() { classCallCheck(this, Hypergiant); defineProperty(this, "_tasks", new Array()); } createClass(Hypergiant, [{ key: "add", /** * Add a new signal. * * @param {Function} fn The method that should be called when the signal is dispatched. * @param {boolean} [once=false] Indicates whether this signal should only be dispatched once and then deleted. * * @returns {Hypergiant} Returns this for chaining. */ value: function add(fn) { var once = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; this._tasks.push(new Task(fn, once)); return this; } /** * Dispatch this Hypergiant event and run all of the tasks associated * with it along with any data passed to it. * * @param {...*} args Any other data that should be passed to the tasks associated with this Hypergiant instance. */ }, { key: "dispatch", value: function dispatch() { for (var i = 0; i < this.tasks.length; ++i) { var task = this.tasks[i]; // For each task we run it with th eprovided arguments. task.run.apply(task, arguments); // If the task is set to be deleted, then we have to get the index of the current // task and then splice it. if (task["delete"]) this.tasks.splice(i, 1); } } /** * Removes a task from this signal by name. * * @param {Function} task The task to remove. * * @returns {Hypergiant} Returns this for chaining. */ }, { key: "remove", value: function remove(fn) { this._tasks = this.tasks.filter(function (task) { return task.fn.toString() != fn.toString(); }); return this; } /** * Removes all tasks from this signal. * * @returns {Hypergiant} Returns this for chaining. */ }, { key: "removeAll", value: function removeAll() { this._tasks = []; return this; } /** * Pauses a task attached to this signal until it is unpaused. * * This means that the paused task will not be called and just be silent until the `enable` method is called * on it returning it back to its normal state. * * @param {Function} task The task to pause. * * @returns {Hypergiant} Returns this for chaining. */ }, { key: "pause", value: function pause(fn) { var taskToPause = this.tasks.find(function (task) { return !task.paused && fn.toString() === task.fn.toString(); }); if (taskToPause) taskToPause.paused = true; return this; } /** * Resumes a task from a paused state. * * @param {Function} task The paused task. * * @returns {Hypergiant} Returns this for chaining. */ }, { key: "resume", value: function resume(fn) { var taskToResume = this.tasks.find(function (task) { return task.paused && fn.toString() === task.fn.toString(); }); if (taskToResume) taskToResume.paused = false; return this; } /** * Makes a task a noop function. * * @param {Function} task The task to make noop. * * @returns {Hypergiant} Returns this for chaining. */ }, { key: "noop", value: function noop(fn) { var taskToNoop = this.tasks.find(function (task) { return fn.toString() === task.fn.toString(); }); if (taskToNoop) taskToNoop.fn = function () {}; return this; } }, { key: "tasks", /** * Returns the tasks created for this signal. * * @returns {Array<Task>} */ get: function get() { return this._tasks; } /** * Returns the number of tasks currently assigned to this signal. * * @returns {number} */ }, { key: "numTasks", get: function get() { return this._tasks.length; } }]); return Hypergiant; }(); var Message = /** * The type of message the message is. * * @property {string} */ /** * The contents of the message. * * @property {string} */ /** * The timestamp of when the message was created. * * @property {number} */ /** * @param {string} type The type of message that is being sent. * @param {string} contents The actual contents of the message. */ function Message(type, contents) { _classCallCheck(this, Message); _defineProperty(this, "type", void 0); _defineProperty(this, "contents", void 0); _defineProperty(this, "timestamp", void 0); this.type = type; this.contents = contents; this.timestamp = +new Date(); }; var Options = /** * Indicates whether the websocket will connect to the server with a secure connection or not. * * @property {boolean} * * @default false */ /** * @param {Object} options The options passed to GameGuardClient on initialization. * @param {boolean} [options.useSecure=false] Indicates whether the websocket will connect to the server with a secure connection or not. */ function Options(options) { _classCallCheck(this, Options); _defineProperty(this, "useSecure", false); Object.assign(this, options); }; function createCommonjsModule(fn, basedir, module) { return module = { path: basedir, exports: {}, require: function (path, base) { return commonjsRequire(path, (base === undefined || base === null) ? module.path : base); } }, fn(module, module.exports), module.exports; } function commonjsRequire () { throw new Error('Dynamic requires are not currently supported by @rollup/plugin-commonjs'); } var js_cookie = createCommonjsModule(function (module, exports) { (function (factory) { var registeredInModuleLoader; { module.exports = factory(); registeredInModuleLoader = true; } if (!registeredInModuleLoader) { var OldCookies = window.Cookies; var api = window.Cookies = factory(); api.noConflict = function () { window.Cookies = OldCookies; return api; }; } }(function () { function extend () { var i = 0; var result = {}; for (; i < arguments.length; i++) { var attributes = arguments[ i ]; for (var key in attributes) { result[key] = attributes[key]; } } return result; } function decode (s) { return s.replace(/(%[0-9A-Z]{2})+/g, decodeURIComponent); } function init (converter) { function api() {} function set (key, value, attributes) { if (typeof document === 'undefined') { return; } attributes = extend({ path: '/' }, api.defaults, attributes); if (typeof attributes.expires === 'number') { attributes.expires = new Date(new Date() * 1 + attributes.expires * 864e+5); } // We're using "expires" because "max-age" is not supported by IE attributes.expires = attributes.expires ? attributes.expires.toUTCString() : ''; try { var result = JSON.stringify(value); if (/^[\{\[]/.test(result)) { value = result; } } catch (e) {} value = converter.write ? converter.write(value, key) : encodeURIComponent(String(value)) .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent); key = encodeURIComponent(String(key)) .replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent) .replace(/[\(\)]/g, escape); var stringifiedAttributes = ''; for (var attributeName in attributes) { if (!attributes[attributeName]) { continue; } stringifiedAttributes += '; ' + attributeName; if (attributes[attributeName] === true) { continue; } // Considers RFC 6265 section 5.2: // ... // 3. If the remaining unparsed-attributes contains a %x3B (";") // character: // Consume the characters of the unparsed-attributes up to, // not including, the first %x3B (";") character. // ... stringifiedAttributes += '=' + attributes[attributeName].split(';')[0]; } return (document.cookie = key + '=' + value + stringifiedAttributes); } function get (key, json) { if (typeof document === 'undefined') { return; } var jar = {}; // To prevent the for loop in the first place assign an empty array // in case there are no cookies at all. var cookies = document.cookie ? document.cookie.split('; ') : []; var i = 0; for (; i < cookies.length; i++) { var parts = cookies[i].split('='); var cookie = parts.slice(1).join('='); if (!json && cookie.charAt(0) === '"') { cookie = cookie.slice(1, -1); } try { var name = decode(parts[0]); cookie = (converter.read || converter)(cookie, name) || decode(cookie); if (json) { try { cookie = JSON.parse(cookie); } catch (e) {} } jar[name] = cookie; if (key === name) { break; } } catch (e) {} } return key ? jar[key] : jar; } api.set = set; api.get = function (key) { return get(key, false /* read as raw */); }; api.getJSON = function (key) { return get(key, true /* read as json */); }; api.remove = function (key, attributes) { set(key, '', extend(attributes, { expires: -1 })); }; api.defaults = {}; api.withConverter = init; return api; } return init(function () {}); })); }); /** * Encodes a message from a message object to an ArrayBuffer. * * @param {Message} message The message to encode. * * @returns {ArrayBuffer} Returns the message as an ArrayBuffer. */ function messageToBuffer(message) { var encoder = new TextEncoder(); var type = message.type, contents = message.contents, timestamp = message.timestamp; var stringified = JSON.stringify({ type: type, contents: contents, timestamp: timestamp }); return encoder.encode(stringified); } /** * Checks to see if a player is an existing player by checking for a cookie * containing their player id. If no existing player id is found, then a new * one is created for the player. * * @returns {string} Returns an existing or new player id. */ function getPlayerId() { var existingPlayerId = js_cookie.get('gameguardPlayerId'); if (existingPlayerId) return existingPlayerId; var newPlayerId = generatePlayerId(); js_cookie.set('gameguardPlayerId', newPlayerId, { expires: 365, path: '' }); return newPlayerId; } /** * Generates a v4 compliant uuid to use for player ids. * * This is based off the answer from: https://stackoverflow.com/a/2117523/4274475 * * @returns {string} Returns a valid v4 uuid. */ function generatePlayerId() { // @ts-ignore return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, function (c) { return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16); }); } /** * GameGuardClient communicates with the server to manage the player and their * data. */ var GameGuardClient = /*#__PURE__*/function () { /** * The options passed to GameGuardClient on initialization. * * @private * * @property {Options} */ /** * The GameGuardClient instance WebSocket connection. * * @private * * @property {WebSocket} */ /** * The signal that is dispatched when the client is assigned a player id. * * This signal is dispatched with the id that was assigned to this client. * * @private * * @property {Hypergiant} */ /** * The signal that is dispatched when the client receives a message from * the GameGuard server. * * This signal is dispatched with the message that was sent to the client. * * @property {Hypergiant} */ /** * The signal that is dispatched when the client's connection with the * GameGuard server is ended. * * This signal is dispatched with the close code and reason. * * @private * * @property {Hypergiant} */ /** * This client's latency to the GameGuard server, in milliseconds. * * @private * * @property {number} */ /** * @param {Object} [options] The initialization parameters passed to this instance. * @param {boolean} [options.useSecure=false] Indicates whether the websocket will connect to the server with a secure connection or not. */ function GameGuardClient() { var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; _classCallCheck(this, GameGuardClient); _defineProperty(this, "_options", void 0); _defineProperty(this, "_socket", void 0); _defineProperty(this, "_connected", new Hypergiant()); _defineProperty(this, "_messaged", new Hypergiant()); _defineProperty(this, "_disconnected", new Hypergiant()); _defineProperty(this, "_latency", 0); this._options = new Options(options); this._connectToServer(); } /** * Returns the connected signal. * * @returns {Hypergiant} */ _createClass(GameGuardClient, [{ key: "_connectToServer", /** * Create the WebSocket connection and attach the methods that respond to the * `open`, `message`, and `close` events. * * @private */ value: function _connectToServer() { var _this = this; // If the `useSecure` option is set to true when we need to make sure that // the `wss` protocol is used. var wsProtocol = this._options.useSecure ? 'wss' : 'ws'; // Create the WebSocket connection by combining the protocol from above // with the current page (since it's where the game should be). this._socket = new WebSocket("".concat(wsProtocol, "://").concat(window.location.host, "/")); // Define the methods that should run when the `open`, `message`, and // `close` events are emitted. this._socket.addEventListener('open', function () { return _this._onopen(); }); this._socket.addEventListener('message', function (message) { return _this._onmessage(message); }); this._socket.addEventListener('close', function (event) { return _this._onclose(event); }); } /** * Called when the connection to the GameGuard server is created and it sends * the player's id to the server and lastly dispatches the `connected` signal * to let the user know that they have successfully connected. * * @private */ }, { key: "_onopen", value: function _onopen() { // Get the existing player id if there is a cookie set, otherwise we create // a new player id. var playerId = getPlayerId(); // Create the player-connected message that the GameGuard server expects to // receive and send it with the player's id. var message = new Message('player-connected', playerId); this._socket.send(messageToBuffer(message)); // Finally we dispatch the `connected` signal with the player's id. this.connected.dispatch(playerId); } /** * When the client receives a message from the GameGuard server, we first * check to see if it's an internal message that we need to respond to. If * it's not a message for us, then we dispatch the `messaged` signal so that * the user can respond to the message. * * @private * * @param {MessageEvent} message The message event received from the GameGuard server. */ }, { key: "_onmessage", value: function _onmessage(message) { var _this2 = this; message.data.text().then(function (text) { // Decode the message from an ArrayBuffer to a Message object. var parsed = JSON.parse(text); var messageDecoded = new Message(parsed.type, parsed.contents); switch (messageDecoded.type) { case 'latency-ping': // The GameGuard server has sent a request for a timestamp from the GameGuardClient. // This is used to create a get a roundtrip timestamp so that we can get the latency // from the GameGuard server to the client. var latencyPongMessage = new Message('latency-pong', messageDecoded.contents); _this2._socket.send(messageToBuffer(latencyPongMessage)); break; case 'latency': // The GameGuard server has sent over the roundtrip latency so we can assign it to // the `latency` property to be used by the client. _this2._latency = parseFloat(messageDecoded.contents); break; default: // Lastly, the message is not internal and is meant for the client so we pass it on // over to them to handle it. _this2.messaged.dispatch(messageDecoded); } }); } /** * When the client's connection to the GameGuard server is closed, we dispatch * the `disconnected` signal which could be used by the user to stop the game * for the client. * * @private * * @property {Event} event The WebSocket close event Object. */ }, { key: "_onclose", value: function _onclose(event) { this.disconnected.dispatch({ code: event.code, reason: event.reason }); } }, { key: "connected", get: function get() { return this._connected; } /** * Returns the messaged signal. * * @returns {Hypergiant} */ }, { key: "messaged", get: function get() { return this._messaged; } /** * Returns the disconnected signal. * * @returns {Hypergiant} */ }, { key: "disconnected", get: function get() { return this._disconnected; } /** * Returns this client's latency to the server, in milliseconds. * * @returns {number} */ }, { key: "latency", get: function get() { return this._latency; } }]); return GameGuardClient; }(); export default GameGuardClient;