├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CHANGELOGS.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── WebAppStart.js ├── api ├── background │ └── OpenLoop.js ├── controllers │ ├── AnalyticsController.js │ ├── DocumentationController.js │ ├── GdEndpointController.js │ ├── GlobalRequestController.js │ ├── LevelSaveController.js │ └── ValueNameController.js ├── converters │ ├── LowerCaseWordToTitleWord.js │ ├── UrlNavigationToResponseEntity.js │ ├── UrlToGlobalUrlArray.js │ ├── UrlToParams.js │ ├── documentation │ │ ├── MdToHtml.js │ │ └── ParamsToRoute.js │ ├── gdEndpoint │ │ ├── BigResponseToJson.js │ │ └── SmallGdResponseToJson.js │ ├── level │ │ └── LevelDataToJson.js │ └── save │ │ ├── EncodedLevelSaveParametersToJson.js │ │ ├── EncodedSaveToAcceptedJson.js │ │ ├── EncodedSaveToXmlString.js │ │ ├── LevelArraysXmlToJson.js │ │ └── LevelSaveToLevelArray.js ├── crypto │ └── MainCrypto.js ├── loggers │ ├── ErrorLogs.js │ └── StartupLogs.js ├── services │ ├── analytics │ │ ├── AnalyticsDataService.js │ │ └── AnalyticsIndexService.js │ ├── documentation │ │ ├── DocumentationDataService.js │ │ └── DocumentationIndexService.js │ ├── gdEndpoint │ │ ├── GdEndpointDataService.js │ │ └── GdEndpointIndexService.js │ ├── save │ │ ├── LevelSaveIndexService.js │ │ └── SelectiveLevelSaveService.js │ └── valueName │ │ └── ValueNameDataService.js └── values │ ├── endpointProperties.json │ ├── songs.json │ └── valueForName.json ├── config.json ├── jsconfig.json ├── package.json ├── resources ├── docs │ └── general │ │ └── welcome.md ├── pages │ ├── analytics │ │ └── base.html │ ├── docs │ │ └── base.html │ ├── endpoints │ │ └── base.html │ └── levelSave │ │ ├── base.html │ │ └── dragBase.html └── static │ ├── css │ ├── analytics.css │ ├── docs.css │ ├── endpoints.css │ ├── levels.css │ └── levelsDrag.css │ ├── images │ └── upload.png │ ├── scripts │ ├── analytics.js │ ├── docs.js │ ├── endpoint.js │ └── handleData.js │ └── synthighlights │ ├── highlight.pack.js │ └── styles │ └── default.css ├── start.js └── testData └── CCLocalLevels.dat /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'SMJSGaming' 7 | 8 | --- 9 | 10 | # Describe the bug 11 | A clear and concise description of what the bug is 12 | 13 | ## Steps to reproduce 14 | 1. Go to '...' 15 | 2. Do '...' 16 | 3. Next to that do '...' 17 | 3. See error 18 | 19 | ## Expected behavior 20 | A clear and concise description of what you expected to happen 21 | 22 | ## Screenshots 23 | If applicable, add screenshots to help explain your problem in the format `![alt text][ImageLink]` 24 | 25 | ## Browser info 26 | - Browser 27 | - Browser version 28 | 29 | ### Additional context 30 | Add any other context about the problem here 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'feature request' 6 | assignees: 'SMJSGaming' 7 | 8 | --- 9 | 10 | # A short title name for your feature 11 | A short description explaining what the feature is 12 | ## Arguments 13 | * Arguments to why this feature should be implemented 14 | * Extra info 15 | * Additional arguments 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode -------------------------------------------------------------------------------- /CHANGELOGS.md: -------------------------------------------------------------------------------- 1 | # Changelogs 2 | ## 0.1.0 3 | The first update introducing the first features of the API. 4 | * Added: 5 | * The levels save decoding and translations. 6 | * The GD endpoint requesting and parsing. 7 | * The first value translations. 8 | * The express web server support. 9 | * Changed: 10 | * none 11 | 12 | ## 0.1.5 13 | A small bug fix and cleanup update. 14 | * Added: 15 | * Issue templates 16 | * Changed: 17 | * Some messy codes 18 | ## 0.2.0 19 | A big clean update. 20 | * Added: 21 | * The base for the docs. 22 | * Changed 23 | * A lot of the codes to follow code standards 24 | * More comments for certain complex processes 25 | * Some parts where made mor efficient 26 | ## 0.3.0 27 | The analytics update which also started following the three tier architecture and had a huge code style redesign along with jsDocs for all files. 28 | * Added: 29 | * A value containing all info about the application. 30 | * The Code of Conduct. 31 | * JsDocs for all files. 32 | * The analytics endpoint. 33 | * The configuration. 34 | * The Contributing guidelines. 35 | * Changed 36 | * A complete redo on the code style. 37 | * A redo on the read. 38 | * The file ordering -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | ## 1. Purpose 3 | The purpose of the GD NodeJS API is to easily gain info from the game Geometry Dash and/or any private servers running on this game and give this info in translated formats to the client and/or software. 4 | 5 | ## 2. Expected Behavior 6 | The following behaviors are expected from all users: 7 | 8 | * Use the API in its original format with only minor changes if truly needed. 9 | * Report any security risks to [SMJS](https://github.com/SMJSGaming). 10 | * Use the API only for its original purpose. 11 | 12 | ## 3. Unacceptable Behavior 13 | The following behaviors are considered breaking the Code of Conduct: 14 | 15 | * Turning the API into a weapon meant to ddos, raid or any other form of damaging the target server. 16 | * Making support for any data sending game endpoints. 17 | * Logging any private data. 18 | * Not following all the conditions from the [license](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/LICENSE). 19 | 20 | ## 4. Consequences of Unacceptable Behavior 21 | Unacceptable behavior from any user including: sponsors, contributors, average users and collaborators and excluding: people with permissions from [SMJS](https://github.com/SMJSGaming) will not be accepted. 22 | 23 | Anyone asked to stop unacceptable behavior is expected to comply immediately. 24 | 25 | If a user ignores the warning they will be banned from the project and if deemed necessary can be taken to court. 26 | 27 | ## 5. Reporting Guidelines 28 | If you notice any user violating the Code of Conduct you are expected to address this to [SMJS](https://github.com/SMJSGaming) with proof. 29 | 30 | ## 6. Addressing Grievances 31 | If you think you have been falsely accused of violating the Code of Conduct you should notify [SMJS](https://github.com/SMJSGaming) with info about the accusation and your reason why you think it's false. 32 | 33 | ## 7. Contact Info 34 | In case you need to contact the owner of the API, do that via: 35 | 36 | * Discord: SMJS#3044 37 | * WhatsApp/SMS: [+31 6 55550071](tel:+31655550071) 38 | 39 | Abusing any of the contact info will be seen as violating the Code of Conduct. 40 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution guidelines 2 | ## How to contribute 3 | 4 | Follow [these steps](https://codeburst.io/a-step-by-step-guide-to-making-your-first-github-contribution-5302260a2940) 5 | After you pull requested your branch it will be reviewed by me ([SMJS](https://github.com/SMJSGaming)) or any future collaborator on the project. 6 | If we think the pull request is up to the standards we want it will be added, otherwise it will be declined. 7 | If your pull request was accepted and merged you will be added to the [Credits](https://github.com/SMJSGaming/GD-NodeJS-API#credits). 8 | 9 | ## The standards 10 | 11 | * All files should be exported as a class. 12 | * All files should have additional [jsDocs](https://jsdoc.app/) in the same format as all files. 13 | * All classes which are called new multiple times should be made singletons as done in [the converter classes](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/api/converters). 14 | * All updates on values and methods should have a version assigned. 15 | * Detailed info about the update must be provided in [the changelogs](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/CHANGELOGS.md). 16 | * All updates should follow the [Code of Conduct](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/CODE_OF_CONDUCT.md). 17 | * Casing: 18 | * File names should be PascalCase. 19 | * Class names should be PascalCase. 20 | * Folder names should be camelCase. 21 | * Value and method names should be camelCase. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 SMJSGaming 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GD NodeJS API 2 | 3 | [![](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)‌‌ ‌ 4 | [![](https://img.shields.io/badge/Version-0.3.0-brightgreen.svg)](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/package.json#L3) ‌ 5 | ![](https://img.shields.io/badge/Progress-60%25-blue.svg) ‌ 6 | [![](https://img.shields.io/badge/Node_Version-13.7.0-026E00.svg?logo=Node.js)](https://nodejs.org/en/)‌‌ ‌ 7 | [![](https://img.shields.io/badge/Donations-Paypal-1546A0.svg?logo=PayPal)](https://www.paypal.me/smjsgaming)‌‌ ‌ 8 | [![](https://img.shields.io/badge/Discord-Support-7289DA.svg?logo=Discord)](https://discord.gg/RRgWMyt)‌‌ ‌ 9 | 10 | A NodeJS Geometry Dash API meant to purpose as one of the easiest to extend, update and use APIs in the game. 11 | 12 | ## Features 13 | 14 | * Full endpoint request API. 15 | * CCLocalLevels.dat save file to level selection. 16 | * CCLocalLevels.dat save file to json data for a specific level index. 17 | * A raw CCLocalLevels.dat json translation API. 18 | * A value translation API. 19 | * Express web server support. 20 | 21 | ## Express URLs 22 | 23 | * `/api/docs/` The API documentation (unfinished). 24 | * `/api/save/level/` The level selection page from a level save. 25 | * `/api/save/level/raw/` The raw translation of a level save. 26 | * `/api/save/level/json/` The full translation of a level save. 27 | * `/api/save/level/{index}/` The selective full translation of a level in the level save. 28 | * `/api/endpoint/` The endpoint API index page. 29 | * `/api/endpoint/{type}/{values}/` The endpoint request API. 30 | * `/api/valueNames/{navigation}/` The list for every GD format value name navigated by the URL. 31 | * `/api/docs/` The base of the new documentations. 32 | 33 | ## How to install the API 34 | 35 | 1. Install the project as a zip. 36 | 2. Unzip it on the location you want the project to be hosted from. 37 | 3. Install [NodeJS](https://nodejs.org/en/). 38 | 4. Open a Powershell terminal in the folder where the project is located. 39 | 5. Run the command `npm i; npm run start`. 40 | 6. Check if it sends startup info back. If so it works. 41 | 42 | ## How to configure the API 43 | The values refer to the values in [The config file](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/config.json), 44 | The word `all` refers to everything in that object. 45 | 46 | * settings 47 | * logging 48 | * errorLogging: If the application should log errors or not. 49 | * analytics 50 | * allowChartInteraction: If the user is allowed to disable chart lines. 51 | * showBrowserVisits: If the user is allowed to see the browser info from visitors. 52 | * endpoints 53 | * all: If the specific endpoint should be enabled and tested. 54 | * expressWebServer: If the express server should be initialized. 55 | * values 56 | * webServer 57 | * port: On what port the server should run. 58 | * analytics 59 | * refreshRateMs: What the analytics endpoint refresh rate should be. 60 | * chartRoundOn: On what number the chart should round. 61 | * maxErrorsPer100Visits: How many errors 100 visits on average is allowed to have. 62 | * endpoints 63 | * targetServer: What server should be targeted for the endpoint request API. 64 | 65 | ## How to contribute to the API 66 | 67 | Follow all the steps in [The contribution information](https://github.com/SMJSGaming/GD-NodeJS-API/blob/master/CONTRIBUTING.md). 68 | 69 | ## Credits 70 |
71 | 72 | * Miko 73 | * Decoding gzip. 74 | * Helping with general NodeJS features. 75 | * 101arrowz 76 | * Cleaning up some parts of the code. 77 | * cos8o 78 | * General geometry dash info. 79 |
80 | 81 | ## Upcoming updates 82 |
83 | 84 | * CI/CD. 85 | * Gherkin tests. 86 | * Style check. 87 | * Auto generated API docs. 88 | * Discord support. 89 | * More value translations. 90 | * Translation support for all save files. 91 | * Documentations for all encryptions/encodings Geometry Dash uses. 92 | * GD method documentations (cut candidate). 93 |
-------------------------------------------------------------------------------- /WebAppStart.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name WebAppStart 6 | * @typedef {Object} WebAppStart 7 | */ 8 | module.exports = class WebAppStart { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name http 14 | */ 15 | http = require("http"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name express 21 | */ 22 | express = require("express"); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name bodyParser 28 | */ 29 | bodyParser = require("body-parser"); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name useragent 35 | */ 36 | useragent = require("express-useragent"); 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name GlobalRequestController 42 | */ 43 | GlobalRequestController = new (require("./api/controllers/GlobalRequestController")); 44 | 45 | /** 46 | * @private 47 | * @since 0.3.0 48 | * @version 0.1.0 49 | * @type {Object} 50 | * @name controllers 51 | * @summary An array containing all controller class objects 52 | */ 53 | controllers = { 54 | GdEndpoints: new (require("./api/controllers/GdEndpointController")), 55 | ValueNames: new (require("./api/controllers/ValueNameController")), 56 | Documentations: new (require("./api/controllers/DocumentationController")), 57 | Analytics: new (require("./api/controllers/AnalyticsController")), 58 | Saves: new (require("./api/controllers/LevelSaveController")) 59 | }; 60 | 61 | /** 62 | * @public 63 | * @since 0.3.0 64 | * @version 0.1.0 65 | * @method init 66 | * @summary The web app initializer 67 | * @description The init method where all the main web app config is located and where the web app is started 68 | * @param {Object} globalData An object containing all the values and methods the application needs 69 | */ 70 | init(globalData) { 71 | // Making the app 72 | const app = this.express(); 73 | 74 | // Setting the max data limit (you really don't need more than this) 75 | app.use(this.bodyParser.json({limit: "50mb"})); 76 | app.use(this.bodyParser.urlencoded({limit: "50mb", extended: true})); 77 | app.use(this.bodyParser.urlencoded({extended: true})); 78 | 79 | // Useragent logger 80 | app.use(this.useragent.express()); 81 | 82 | // Setting the static files address 83 | app.use(this.express.static("resources/static")); 84 | 85 | // Doing some default actions on a visit 86 | app.use((req, res, next) => this.GlobalRequestController.controller(globalData, req, next)); 87 | 88 | // Since this is only an API for now I will just auto redirect to the docs 89 | app.all(["/", "/api"], (req, res) => { 90 | res.redirect("/api/docs"); 91 | }); 92 | 93 | // Calling the endpoint visit event listener initializers 94 | Object.keys(this.controllers).forEach((key) => { 95 | if (globalData.config.settings.endpoints[`enable${key}Endpoint`]) { 96 | this.controllers[key].controller(app, globalData); 97 | } 98 | }); 99 | 100 | // Starting the listener 101 | const server = this.http.createServer(app); 102 | server.listen(globalData.config.values.webServer.port || process.env.port); 103 | } 104 | } -------------------------------------------------------------------------------- /api/background/OpenLoop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name OpenLoop 6 | * @typedef {Object} OpenLoop 7 | */ 8 | module.exports = class OpenLoop { 9 | 10 | /** 11 | * @public 12 | * @type {OpenLoop} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see OpenLoop 23 | * @param {Object} [globalData={}] An object containing all the values and methods the application needs 24 | */ 25 | constructor(globalData = {}) { 26 | if (OpenLoop.INSTANCE) { 27 | return OpenLoop.INSTANCE; 28 | } else { 29 | Object.keys(globalData.graphEntries).forEach((key) => { 30 | if (globalData.config.settings.endpoints 31 | [`enable${globalData.graphEntries[key].settingsName}Endpoint`]) { 32 | this[key] = 33 | new (require(`../services/${globalData.graphEntries[key].baseClass}.js`)); 34 | } 35 | }); 36 | 37 | OpenLoop.INSTANCE = this; 38 | } 39 | } 40 | 41 | /** 42 | * @public 43 | * @since 0.3.0 44 | * @version 0.1.0 45 | * @method init 46 | * @summary The initializer method 47 | * @description The loop initializer which will activate every 6 minutes and time test all services which are activated 48 | * @param {Object} globalData An object containing all the values and methods the application needs 49 | */ 50 | init(globalData) { 51 | setInterval(() => { 52 | Object.keys(globalData.graphEntries).forEach((key) => { 53 | if (globalData.config.settings.endpoints 54 | [`enable${globalData.graphEntries[key].settingsName}Endpoint`]) { 55 | OpenLoop.INSTANCE.timeTest(globalData.graphEntries[key], key).then((time) => { 56 | globalData.graphEntries[key].data.shift(); 57 | globalData.graphEntries[key].data[9] = time; 58 | }); 59 | } 60 | }); 61 | }, 360000); 62 | } 63 | 64 | /** 65 | * @public 66 | * @since 0.3.0 67 | * @version 0.1.0 68 | * @method timeTest 69 | * @summary The time test method 70 | * @description The test method which will set a start time, request the service, awaits the response and does a delta calculation on the time afterwards and returns the result 71 | * @param {Object} testPart The test info from the @see globalData 72 | * @param {String} key The service to test 73 | * @returns {Promise} The response time 74 | */ 75 | timeTest(testPart, key) { 76 | return new Promise((resolve) => { 77 | let time = Date.now(); 78 | 79 | if (testPart.async) { 80 | OpenLoop.INSTANCE[key].service( 81 | testPart.testData[0], 82 | testPart.testData[1], 83 | testPart.testData[2] 84 | ).then(() => { 85 | resolve(Date.now() - time); 86 | }); 87 | } else { 88 | OpenLoop.INSTANCE[key].service( 89 | testPart.testData[0], 90 | testPart.testData[1], 91 | testPart.testData[2] 92 | ); 93 | resolve(Date.now() - time); 94 | } 95 | }); 96 | } 97 | } -------------------------------------------------------------------------------- /api/controllers/AnalyticsController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name AnalyticsController 6 | * @typedef {Object} AnalyticsController 7 | */ 8 | module.exports = class AnalyticsController { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name UrlToParams 14 | */ 15 | UrlToParams = new (require("../converters/UrlToParams")); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlToGlobalUrlArray 21 | */ 22 | UrlToGlobalUrlArray = new (require("../converters/UrlToGlobalUrlArray")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name AnalyticsDataService 28 | */ 29 | AnalyticsDataService = new (require("../services/analytics/AnalyticsDataService")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name AnalyticsIndexService 35 | */ 36 | AnalyticsIndexService = new (require("../services/analytics/AnalyticsIndexService")) 37 | 38 | /** 39 | * @public 40 | * @since 0.3.0 41 | * @version 0.1.0 42 | * @method controller 43 | * @summary endpoint listener initializer 44 | * @description endpoints: 45 | * /analytics, 46 | * /analytics/*, 47 | * /analytics/data/* 48 | * @param {Object} app The express app with all the necessary methods to use the controllers 49 | * @param {Object} globalData An object containing all the values and methods the application needs 50 | */ 51 | controller(app, globalData) { 52 | const classObject = this; 53 | 54 | app.all(this.UrlToGlobalUrlArray.converter("/analytics"), (req, res) => { 55 | const params = classObject.UrlToParams.converter(req.params); 56 | 57 | if (params[0] == "data") { 58 | this.AnalyticsDataService.service(globalData).then((response) => { 59 | res.status(response[0]).type("json").send(response[1]); 60 | }); 61 | } else { 62 | this.AnalyticsIndexService.service(globalData).then((response) => { 63 | res.status(response[0]).type("html").send(response[1]); 64 | }); 65 | } 66 | }); 67 | } 68 | } -------------------------------------------------------------------------------- /api/controllers/DocumentationController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name DocumentationController 6 | * @typedef {Object} DocumentationController 7 | */ 8 | module.exports = class DocumentationController { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name UrlToGlobalUrlArray 14 | */ 15 | UrlToGlobalUrlArray = new (require("../converters/UrlToGlobalUrlArray")); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlToParams 21 | */ 22 | UrlToParams = new (require("../converters/UrlToParams")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name ParamsToRoute 28 | */ 29 | ParamsToRoute = new (require("../converters/documentation/ParamsToRoute")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name DocumentationIndexService 35 | */ 36 | DocumentationIndexService = new (require("../services/documentation/DocumentationIndexService")); 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name DocumentationDataService 42 | */ 43 | DocumentationDataService = new (require("../services/documentation/DocumentationDataService")) 44 | 45 | /** 46 | * @public 47 | * @since 0.3.0 48 | * @version 0.1.0 49 | * @method controller 50 | * @summary endpoint listener initializer 51 | * @description endpoints: 52 | * /api/docs, 53 | * /api/docs/{navigation}, 54 | * /api/docs/res/{navigation} 55 | * @param {Object} app The express app with all the necessary methods to use the controllers 56 | * @param {Object} globalData An object containing all the values and methods the application needs 57 | */ 58 | controller(app, globalData) { 59 | const classObject = this; 60 | 61 | app.all(this.UrlToGlobalUrlArray.converter("/api/docs"), (req, res) => { 62 | const params = classObject.UrlToParams.converter(req.params); 63 | let route = classObject.ParamsToRoute.converter(params); 64 | 65 | if (params[0] == "res") { 66 | params.shift(); 67 | route = route.replace("/res", ""); 68 | if (params[0]) { 69 | classObject.DocumentationDataService.service(route).then((response) => { 70 | res.status(response[0]).type("html").send(response[1]); 71 | }); 72 | } 73 | } else { 74 | classObject.DocumentationIndexService.service(params).then((response) => { 75 | res.status(response[0]).type("html").send(response[1]); 76 | }).catch((error) => { 77 | globalData.errorFunc(error, "docs", globalData); 78 | res.status(500).type("html").send("

INTERNAL SERVER ERROR 500

"); 79 | }); 80 | } 81 | }); 82 | } 83 | } -------------------------------------------------------------------------------- /api/controllers/GdEndpointController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name GdEndpointsController 6 | * @typedef {Object} GdEndpointsController 7 | */ 8 | module.exports= class GdEndpointsController { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name UrlToGlobalUrlArray 14 | */ 15 | UrlToGlobalUrlArray = new (require("../converters/UrlToGlobalUrlArray")); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlToParams 21 | */ 22 | UrlToParams = new (require("../converters/UrlToParams")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name GdEndpointIndexService 28 | */ 29 | GdEndpointIndexService = new (require("../services/gdEndpoint/GdEndpointIndexService")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name GdEndpointDataService 35 | */ 36 | GdEndpointDataService = new (require("../services/gdEndpoint/GdEndpointDataService")); 37 | 38 | /** 39 | * @public 40 | * @since 0.3.0 41 | * @version 0.1.0 42 | * @method controller 43 | * @summary endpoint listener initializer 44 | * @description endpoints: 45 | * /api/endpoint, 46 | * /api/endpoint/{type}/{values} 47 | * @param {Object} app The express app with all the necessary methods to use the controllers 48 | * @param {Object} globalData An object containing all the values and methods the application needs 49 | */ 50 | controller(app, globalData) { 51 | const classObject = this; 52 | 53 | app.all(this.UrlToGlobalUrlArray.converter("/api/endpoint"), (req, res) => { 54 | const params = classObject.UrlToParams.converter(req.params); 55 | if (params[0]) { 56 | classObject.GdEndpointDataService.service(params, globalData).then((response) => { 57 | res.status(response[0]).type("json").send(JSON.stringify(response[1], null, 2)); 58 | }); 59 | } else { 60 | classObject.GdEndpointIndexService.service(globalData).then((response) => { 61 | res.status(response[0]).type("html").send(response[1]); 62 | }); 63 | } 64 | }); 65 | } 66 | } -------------------------------------------------------------------------------- /api/controllers/GlobalRequestController.js: -------------------------------------------------------------------------------- 1 | module.exports = class GlobalRequestController { 2 | 3 | /** 4 | * @private 5 | * @since 0.3.0 6 | * @version 0.1.0 7 | * @type {String[]} 8 | * @name ignoreLinks 9 | * @summary The links which should not be logged as a visit 10 | */ 11 | ignoreLinks = [ 12 | "/analytics/data" 13 | ]; 14 | 15 | /** 16 | * @public 17 | * @since 0.3.0 18 | * @version 0.1.0 19 | * @method controller 20 | * @summary The controller method 21 | * @description The base method which makes sure that the url is valid and will go to the next listener 22 | * @param {Object} globalData An object containing all the values and methods the application needs 23 | * @param {Object} req The request holding all info needed to inject into @see globalData 24 | * @param {Function} next The next function which makes sure it will get to the actual request after it's done 25 | */ 26 | controller(globalData, req, next) { 27 | if (this.ignoreLinks.indexOf(req.url) == -1) { 28 | this.addVisits(globalData, req); 29 | } 30 | next(); 31 | } 32 | 33 | /** 34 | * @private 35 | * @since 0.3.0 36 | * @version 0.1.0 37 | * @method addVisits 38 | * @summary The visit process method 39 | * @description The method which will inject all data which should be logged into @see globalData 40 | * @param {Object} globalData An object containing all the values and methods the application needs 41 | * @param {Object} req The request holding all info needed to inject into @see globalData 42 | */ 43 | addVisits(globalData, req) { 44 | globalData.visits++; 45 | if (!globalData.browser[req.useragent.browser]) 46 | globalData.browser[req.useragent.browser] = {visits: 0, versions: {}}; 47 | 48 | globalData.browser[req.useragent.browser].versions[req.useragent.version] = 49 | globalData.browser[req.useragent.browser].versions[req.useragent.version] + 1 || 1; 50 | 51 | globalData.browser[req.useragent.browser].visits++; 52 | } 53 | } -------------------------------------------------------------------------------- /api/controllers/LevelSaveController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name LevelsSaveController 6 | * @typedef {Object} LevelsSaveController 7 | */ 8 | module.exports = class LevelsSaveController { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name path 14 | */ 15 | path = require("path"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlToParams 21 | */ 22 | UrlToParams = new (require("../converters/UrlToParams")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name UrlToGlobalUrlArray 28 | */ 29 | UrlToGlobalUrlArray = new (require("../converters/UrlToGlobalUrlArray")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name LevelsIndexService 35 | */ 36 | LevelsIndexService = new (require("../services/save/LevelSaveIndexService")); 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name SelectiveLevelSaveService 42 | */ 43 | SelectiveLevelSaveService = new (require("../services/save/SelectiveLevelSaveService")); 44 | 45 | /** 46 | * @private 47 | * @type {Object} 48 | * @name EncodedSaveToAcceptedJson 49 | */ 50 | EncodedSaveToAcceptedJson = new (require("../converters/save/EncodedSaveToAcceptedJson")); 51 | 52 | /** 53 | * @public 54 | * @since 0.3.0 55 | * @version 0.1.0 56 | * @method controller 57 | * @summary endpoint listener initializer 58 | * @description endpoints: 59 | * /api/save/level, 60 | * /api/save/level/*, 61 | * /api/save/level/{levelArrayIndex}, 62 | * /api/save/level/json, 63 | * /api/save/level/raw 64 | * @param {Object} app The express app with all the necessary methods to use the controllers 65 | * @param {Object} globalData An object containing all the values and methods the application needs 66 | */ 67 | controller(app, globalData) { 68 | const classObject = this; 69 | const url = this.UrlToGlobalUrlArray.converter("/api/save/level"); 70 | 71 | app.post(url, (req, res) => { 72 | const params = classObject.UrlToParams.converter(req.params); 73 | let data; 74 | let tempResponse; 75 | 76 | if (req.body.data) { 77 | if (Number.isInteger(parseInt(params[0]))) { 78 | tempResponse = this.SelectiveLevelSaveService.service(globalData, req.body.data, params[0]); 79 | res.status(tempResponse[0]).type("json").send(JSON.stringify(tempResponse[1], null, 2)); 80 | } else { 81 | try { 82 | data = this.EncodedSaveToAcceptedJson.converter(req.body.data); 83 | } catch(error) { 84 | globalData.errorFunc(error, "level-save", globalData); 85 | return res.status(400).type("json").send(JSON.stringify({ 86 | "ERROR": "There was an error while decrypting your data." 87 | }, null, 2)); 88 | } 89 | if (params[0] == "raw") { 90 | try { 91 | res.status(200).type("json").send(JSON.stringify(data, null, 2)); 92 | } catch(error) { 93 | globalData.errorFunc(error, "level-save-raw", globalData); 94 | res.status(400).type("json").send(JSON.stringify({ 95 | "ERROR": "There was an error while parsing your data." 96 | }, null, 2)); 97 | } 98 | } else if (params[0] == "json") { 99 | data.forEach((level, index) => { 100 | tempResponse = this.SelectiveLevelSaveService.service(globalData, JSON.stringify(level)); 101 | data[index] = tempResponse[1]; 102 | }); 103 | res.status(200).type("json").send(JSON.stringify(data, null, 2)); 104 | } else { 105 | this.LevelsIndexService.service(globalData, data).then((response) => { 106 | res.status(response[0]).type("html").send(response[1]); 107 | }); 108 | } 109 | } 110 | } else { 111 | res.redirect(req.url); 112 | } 113 | }); 114 | 115 | app.get(url, (req, res) => { 116 | res.status(200).sendFile( 117 | classObject.path.join(__dirname, "../../resources/pages/levelSave/dragBase.html")); 118 | }); 119 | } 120 | } -------------------------------------------------------------------------------- /api/controllers/ValueNameController.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name ValueNameController 6 | * @typedef {Object} ValueNameController 7 | */ 8 | module.exports = class ValueNameController { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name UrlToGlobalUrlArray 14 | */ 15 | UrlToGlobalUrlArray = new (require("../converters/UrlToGlobalUrlArray")); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlToParams 21 | */ 22 | UrlToParams = new (require("../converters/UrlToParams")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name ValueNamesDataService 28 | */ 29 | ValueNamesDataService = new (require("../services/valueName/ValueNameDataService")); 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method controller 36 | * @summary endpoint listener initializer 37 | * @description endpoints: 38 | * /analytics, 39 | * /api/valueNames/{jsonNavigation}, 40 | * @param {Object} app The express app with all the necessary methods to use the controllers 41 | */ 42 | controller(app) { 43 | const classObject = this; 44 | 45 | app.all(this.UrlToGlobalUrlArray.converter("/api/valueNames"), function(req, res) { 46 | const response = classObject.ValueNamesDataService.service(classObject.UrlToParams.converter(req.params)); 47 | res.status(response[0]).type("json").send(JSON.stringify(response[1], null, 2)); 48 | }); 49 | } 50 | } -------------------------------------------------------------------------------- /api/converters/LowerCaseWordToTitleWord.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name LowerCaseWordToTitleWord 6 | * @typedef {Object} LowerCaseWordToTitleWord 7 | */ 8 | module.exports = class LowerCaseWordToTitleWord { 9 | 10 | /** 11 | * @private 12 | * @type {LowerCaseWordToTitleWord} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see LowerCaseWordToTitleWord 23 | */ 24 | constructor() { 25 | if (!LowerCaseWordToTitleWord.INSTANCE) { 26 | LowerCaseWordToTitleWord.INSTANCE = this; 27 | } 28 | return LowerCaseWordToTitleWord.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {String} word 37 | * @returns {String} 38 | */ 39 | converter(word) { 40 | return word.replace(/^\w/, c => c.toUpperCase()).replace(".md", ""); 41 | } 42 | } -------------------------------------------------------------------------------- /api/converters/UrlNavigationToResponseEntity.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name UrlNavigationToResponseEntity 6 | * @typedef {Object} UrlNavigationToResponseEntity 7 | */ 8 | module.exports = class UrlNavigationToResponseEntity { 9 | 10 | /** 11 | * @private 12 | * @type {UrlNavigationToResponseEntity} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see UrlToResponseEntity 23 | */ 24 | constructor() { 25 | if (!UrlNavigationToResponseEntity.INSTANCE) { 26 | UrlNavigationToResponseEntity.INSTANCE = this; 27 | } 28 | return UrlNavigationToResponseEntity.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {String[]} params 37 | * @param {Object} output 38 | * @returns {[Number, Object]} 39 | */ 40 | converter(params, output) { 41 | let objectCheck = {}; 42 | if (params) { 43 | params.forEach((param) => { 44 | objectCheck = UrlNavigationToResponseEntity.INSTANCE.caseInsensitiveKeyFind(param, output); 45 | if (objectCheck) { 46 | output = objectCheck; 47 | } else { 48 | if (param) { 49 | return [404, {ERROR: "Non existing route"}]; 50 | } 51 | } 52 | }); 53 | } 54 | return [200, output]; 55 | } 56 | 57 | /** 58 | * @private 59 | * @since 0.3.0 60 | * @version 0.1.0 61 | * @method converter 62 | * @param {String} key 63 | * @param {Object} object 64 | * @returns {Object} 65 | */ 66 | caseInsensitiveKeyFind(key, object) { 67 | return object[Object.keys(object).find(keySearch => keySearch.toLowerCase() == key.toLowerCase())]; 68 | } 69 | } -------------------------------------------------------------------------------- /api/converters/UrlToGlobalUrlArray.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name UrlToGlobalUrlArray 6 | * @typedef {Object} UrlToGlobalUrlArray 7 | */ 8 | module.exports = class UrlToGlobalUrlArray { 9 | 10 | /** 11 | * @private 12 | * @type {UrlToGlobalUrlArray} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see UrlToGlobalUrlArray 23 | */ 24 | constructor() { 25 | if (!UrlToGlobalUrlArray.INSTANCE) { 26 | UrlToGlobalUrlArray.INSTANCE = this; 27 | } 28 | return UrlToGlobalUrlArray.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {String} url 37 | * @returns {String[]} 38 | */ 39 | converter(url) { 40 | return [url, `${url}/*`]; 41 | } 42 | } -------------------------------------------------------------------------------- /api/converters/UrlToParams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name UrlToParams 6 | * @typedef {Object} UrlToParams 7 | */ 8 | module.exports = class UrlToParams { 9 | 10 | /** 11 | * @private 12 | * @type {UrlToParams} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see UrlToParams 23 | */ 24 | constructor() { 25 | if (!UrlToParams.INSTANCE) { 26 | UrlToParams.INSTANCE = this; 27 | } 28 | return UrlToParams.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {String} url 37 | * @returns {String[]} 38 | */ 39 | converter(url) { 40 | return (url[0] || "").split("/"); 41 | } 42 | } -------------------------------------------------------------------------------- /api/converters/documentation/MdToHtml.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name MdToHtml 6 | * @typedef {Object} MdToHtml 7 | */ 8 | module.exports = class MdToHtml { 9 | 10 | /** 11 | * @public 12 | * @type {MdToHtml} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see MdToHtml 23 | */ 24 | constructor() { 25 | if (!MdToHtml.INSTANCE) { 26 | MdToHtml.INSTANCE = this; 27 | } 28 | return MdToHtml.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name showdown 35 | */ 36 | showdown = new (require("showdown")).Converter(); 37 | 38 | /** 39 | * @public 40 | * @since 0.3.0 41 | * @version 0.1.0 42 | * @method converter 43 | * @param {String} md 44 | * @returns {String} 45 | */ 46 | converter(md) { 47 | return MdToHtml.INSTANCE.showdown.makeHtml(md); 48 | } 49 | } -------------------------------------------------------------------------------- /api/converters/documentation/ParamsToRoute.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name ParamsToRoute 6 | * @typedef {Object} ParamsToRoute 7 | */ 8 | module.exports = class ParamsToRoute { 9 | 10 | /** 11 | * @public 12 | * @type {ParamsToRoute} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see ParamsToRoute 23 | */ 24 | constructor() { 25 | if (!ParamsToRoute.INSTANCE) { 26 | ParamsToRoute.INSTANCE = this; 27 | } 28 | return ParamsToRoute.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {String[]} params 37 | * @returns {String} 38 | */ 39 | converter(params) { 40 | let fileToRead = "resources/docs"; 41 | if (params[0]) { 42 | for (let i in params) { 43 | fileToRead += `/${params[i]}`; 44 | } 45 | } else { 46 | fileToRead += "/general/welcome.md"; 47 | } 48 | return fileToRead; 49 | } 50 | } -------------------------------------------------------------------------------- /api/converters/gdEndpoint/BigResponseToJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name BigResponseToJson 6 | * @typedef {Object} BigResponseToJson 7 | */ 8 | module.exports = class BigResponseToJson { 9 | 10 | /** 11 | * @public 12 | * @type {BigResponseToJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see BigResponseToJson 23 | */ 24 | constructor() { 25 | if (!BigResponseToJson.INSTANCE) { 26 | BigResponseToJson.INSTANCE = this; 27 | } 28 | return BigResponseToJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name valueForName 35 | */ 36 | valueForName = require("../../values/valueForName.json").endpoints; 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name SmallGdResponseToJson 42 | */ 43 | SmallGdResponseToJson = new (require("./SmallGdResponseToJson")); 44 | 45 | /** 46 | * @public 47 | * @since 0.3.0 48 | * @version 0.1.0 49 | * @method converter 50 | * @param {String} data 51 | * @param {Object} type 52 | * @param {Object[]} output 53 | */ 54 | converter(data, type, output) { 55 | let tempData; 56 | let dar = []; 57 | let misc = "misc"; 58 | if (type.type.indexOf(4) == -1) { 59 | // type 1-3 60 | dar = data.split("#"); 61 | tempData = dar[0].split("|"); 62 | } else { 63 | tempData = data.split("|"); 64 | } 65 | if (!tempData[tempData.length-1]) { 66 | tempData.splice(tempData.length-1, 1); 67 | } 68 | if (type.type.indexOf(3) != -1) { 69 | // type 3 70 | misc = "miscEncrypted"; 71 | } 72 | for (let i = 0; i < tempData.length; i++) { 73 | output[i] = {}; 74 | BigResponseToJson.INSTANCE.SmallGdResponseToJson.converter(tempData[i], type, output[i]); 75 | } 76 | if (type.type.indexOf(4) == -1) { 77 | // type 1-3 78 | if (misc == "miscEncrypted") { 79 | output.push({}); 80 | tempData = dar[1].split(":"); 81 | for (let i in tempData) { 82 | output[output.length-1][BigResponseToJson.INSTANCE.valueForName[misc][i]] = tempData[i]; 83 | } 84 | } else { 85 | for (let i = 1; i < dar.length; i++) { 86 | if (dar[i]) { 87 | output.push({}); 88 | tempData = dar[i].split(":"); 89 | for (let j in tempData) { 90 | output[output.length-1][BigResponseToJson.INSTANCE.valueForName[misc][j]] = tempData[j]; 91 | } 92 | misc = "miscEncrypted"; 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /api/converters/gdEndpoint/SmallGdResponseToJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name SmallGdResponseToJson 6 | * @typedef {Object} SmallGdResponseToJson 7 | */ 8 | module.exports = class SmallGdResponseToJson { 9 | 10 | /** 11 | * @public 12 | * @type {SmallGdResponseToJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see SmallGdResponseToJson 23 | */ 24 | constructor() { 25 | if (!SmallGdResponseToJson.INSTANCE) { 26 | SmallGdResponseToJson.INSTANCE = this; 27 | } 28 | return SmallGdResponseToJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name valueForName 35 | */ 36 | valueForName = require("../../values/valueForName.json").endpoints; 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name LowerCaseWordToTitleWord 42 | */ 43 | LowerCaseWordToTitleWord = new (require("../../converters/LowerCaseWordToTitleWord")); 44 | 45 | /** 46 | * @public 47 | * @since 0.3.0 48 | * @version 0.1.0 49 | * @method converter 50 | * @param {String} data 51 | * @param {Object} type 52 | * @param {Object} output 53 | * @returns {Object} 54 | */ 55 | converter(data, type, output) { 56 | let dar = []; 57 | const valueForName = SmallGdResponseToJson.INSTANCE.valueForName; 58 | 59 | if (type.type.indexOf(2) != -1) { 60 | // type 2 61 | return SmallGdResponseToJson.INSTANCE.type2(valueForName, data, type, output, dar); 62 | } else if (type.type.indexOf(6) != -1) { 63 | // type 6 64 | dar = SmallGdResponseToJson.INSTANCE.type6(valueForName, data, type, output, dar); 65 | } else { 66 | // type 1 67 | dar = SmallGdResponseToJson.INSTANCE.type1(valueForName, data, type, output, dar); 68 | } 69 | if (type.type.indexOf(5) != -1) { 70 | // type 5 71 | return SmallGdResponseToJson.INSTANCE.type5(valueForName, output, dar); 72 | } 73 | } 74 | 75 | /** 76 | * @private 77 | * @since 0.3.0 78 | * @version 0.1.0 79 | * @method type1 80 | * @param {Object} valueForName 81 | * @param {String} data 82 | * @param {Object} type 83 | * @param {Object} output 84 | * @param {String[]} dar 85 | */ 86 | type1(valueForName, data, type, output, dar) { 87 | dar = data.split(type.splitChar); 88 | for (let i = 0; i < dar.length; i++) { 89 | output[valueForName[type.jsonData[0]][dar[i]] || valueForName["user"][dar[i]]] = dar[++i]; 90 | } 91 | return dar; 92 | } 93 | 94 | /** 95 | * @private 96 | * @since 0.3.0 97 | * @version 0.1.0 98 | * @method type2 99 | * @param {Object} valueForName 100 | * @param {String} data 101 | * @param {Object} type 102 | * @param {Object} output 103 | * @param {String[]} dar 104 | */ 105 | type2(valueForName, data, type, output, dar) { 106 | let temp = []; 107 | const title = SmallGdResponseToJson.INSTANCE.LowerCaseWordToTitleWord.converter; 108 | 109 | dar = data.split(":"); 110 | for (let i = 0; i < dar.length; i++) { 111 | temp[i] = dar[i].split(type.splitChar); 112 | output[title(type.jsonData[i])] = {}; 113 | for (let j = 0; j < temp[i].length; j++) { 114 | output[title(type.jsonData[i])][valueForName[type.jsonData[i]][temp[i][j]] || "user"[temp[i][j]]] = temp[i][++j]; 115 | } 116 | } 117 | } 118 | 119 | /** 120 | * @private 121 | * @since 0.3.0 122 | * @version 0.1.0 123 | * @method type5 124 | * @param {Object} valueForName 125 | * @param {Object} output 126 | * @param {String[]} dar 127 | */ 128 | type5(valueForName, output, dar) { 129 | dar = dar[dar.length-1].split("#"); 130 | output[Object.keys(output)[Object.keys(output).length-1]] = dar[0]; 131 | dar.splice(0, 1); 132 | output["Misc"] = {}; 133 | for (let i = 0; i < dar.length; i++) { 134 | output["Misc"][valueForName["miscEncrypted"][i]] = dar[i]; 135 | } 136 | } 137 | 138 | /** 139 | * @private 140 | * @since 0.3.0 141 | * @version 0.1.0 142 | * @method type6 143 | * @param {Object} valueForName 144 | * @param {String} data 145 | * @param {Object} type 146 | * @param {Object} output 147 | * @param {String[]} dar 148 | */ 149 | type6(valueForName, data, type, output, dar) { 150 | dar = data.split(type.splitChar); 151 | for (let i = 0; i < dar.length; i++) { 152 | output[valueForName[type.jsonData[0]][i] || valueForName["user"][i]] = dar[i]; 153 | } 154 | return dar; 155 | } 156 | } -------------------------------------------------------------------------------- /api/converters/level/LevelDataToJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name LevelDataToJson 6 | * @typedef {Object} LevelDataToJson 7 | */ 8 | module.exports = class LevelDataToJson { 9 | 10 | /** 11 | * @public 12 | * @type {LevelDataToJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see LevelDataToJson 23 | */ 24 | constructor() { 25 | if (!LevelDataToJson.INSTANCE) { 26 | LevelDataToJson.INSTANCE = this; 27 | } 28 | return LevelDataToJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {Object} json 37 | * @param {String} data 38 | */ 39 | converter(json, data) { 40 | let tempArray = [{}]; 41 | let tempJson = {}; 42 | 43 | // Splitting color and object data 44 | tempArray = data.split("|,"); 45 | // Splitting global variables and values 46 | tempArray[0] = tempArray[0].split(","); 47 | // Splitting the colors 48 | tempArray[0][1] = tempArray[0][1].split("|"); 49 | tempJson[tempArray[0][0]] = {}; 50 | 51 | LevelDataToJson.INSTANCE.color(tempJson[tempArray[0][0]], tempArray[0]); 52 | LevelDataToJson.INSTANCE.data(tempJson, tempArray[1]); 53 | 54 | json.k4 = tempJson; 55 | } 56 | 57 | /** 58 | * @private 59 | * @since 0.3.0 60 | * @version 0.1.0 61 | * @method color 62 | * @param {Object} tempJson 63 | * @param {Object} tempArray 64 | */ 65 | color(tempJson, tempArray) { 66 | let tempValue; 67 | if (tempArray[1][0]) { 68 | tempJson.Colors = {}; 69 | for (let i in tempArray[1]) { 70 | tempValue = tempArray[1][i].split("_"); 71 | tempJson.Colors[`Color${tempValue[15]}`] = {}; 72 | for (let j = 0; j < tempValue.length; j += 2) { 73 | tempJson.Colors[`Color${tempValue[15]}`][tempValue[j]] = tempValue[j + 1]; 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * @private 81 | * @since 0.3.0 82 | * @version 0.1.0 83 | * @method data 84 | * @param {Object} tempJson 85 | * @param {Object} tempArray 86 | */ 87 | data(tempJson, tempArray) { 88 | if (tempArray) { 89 | tempArray = tempArray.substring(0, tempArray.length - 1).split(";"); 90 | tempArray[0] = tempArray[0].split(","); 91 | LevelDataToJson.INSTANCE.jsonPush(tempJson, tempArray[0], 0, true); 92 | LevelDataToJson.INSTANCE.kA14(tempJson); 93 | LevelDataToJson.INSTANCE.objects(tempJson, tempArray); 94 | } 95 | } 96 | 97 | /** 98 | * @private 99 | * @since 0.3.0 100 | * @version 0.1.0 101 | * @method kA14 102 | * @param {Object} tempJson 103 | */ 104 | kA14(tempJson) { 105 | tempJson.kA14 = tempJson.kA14.substring(0, tempJson.kA14.length - 1).split("~"); 106 | } 107 | 108 | /** 109 | * @private 110 | * @since 0.3.0 111 | * @version 0.1.0 112 | * @method objects 113 | * @param {Object} tempJson 114 | * @param {Object[]} tempArray 115 | */ 116 | objects(tempJson, tempArray) { 117 | tempJson.Objects = []; 118 | tempArray.shift(); 119 | LevelDataToJson.INSTANCE.jsonPush(tempJson.Objects, tempArray, 0, false); 120 | } 121 | 122 | /** 123 | * @private 124 | * @since 0.3.0 125 | * @version 0.1.0 126 | * @method jsonPush 127 | * @param {Object} json 128 | * @param {Object[]} toBePushed 129 | * @param {Number} offset 130 | * @param {Boolean} simple 131 | * @param {String} [split=","] 132 | */ 133 | jsonPush(json, toBePushed, offset, simple, split = ",") { 134 | if (simple) { 135 | for (let i = offset; i < toBePushed.length; i += 2) { 136 | json[toBePushed[i]] = toBePushed[i + 1]; 137 | } 138 | } else { 139 | for (let i = offset; i < toBePushed.length; i++) { 140 | toBePushed[i] = toBePushed[i].split(split); 141 | json[i] = {}; 142 | for (let j = 0; j < toBePushed[i].length; j += 2) { 143 | json[i][toBePushed[i][j]] = toBePushed[i][j + 1]; 144 | } 145 | } 146 | } 147 | } 148 | } -------------------------------------------------------------------------------- /api/converters/save/EncodedLevelSaveParametersToJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name EncodedLevelSaveParametersToJson 6 | * @typedef {Object} EncodedLevelSaveParametersToJson 7 | */ 8 | module.exports = class EncodedLevelSaveParametersToJson { 9 | 10 | /** 11 | * @public 12 | * @type {EncodedLevelSaveParametersToJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see EncodedLevelSaveParametersToJson 23 | */ 24 | constructor() { 25 | if (!EncodedLevelSaveParametersToJson.INSTANCE) { 26 | EncodedLevelSaveParametersToJson.INSTANCE = this; 27 | } 28 | return EncodedLevelSaveParametersToJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name MainCrypto 35 | */ 36 | MainCrypto = new (require("../../crypto/MainCrypto")); 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name LevelDataToJson 42 | */ 43 | LevelDataToJson = new (require("../level/LevelDataToJson")); 44 | 45 | /** 46 | * @public 47 | * @since 0.3.0 48 | * @version 0.1.0 49 | * @method converter 50 | * @param {Object} level 51 | */ 52 | converter(level) { 53 | EncodedLevelSaveParametersToJson.INSTANCE.k4(level); 54 | EncodedLevelSaveParametersToJson.INSTANCE.k34(level); 55 | EncodedLevelSaveParametersToJson.INSTANCE.k67(level); 56 | } 57 | 58 | /** 59 | * @private 60 | * @since 0.3.0 61 | * @version 0.1.0 62 | * @method k4 63 | * @param {Object} level 64 | */ 65 | k4(level) { 66 | if (level.k4) { 67 | if (level.k4.startsWith("H4sIAAAAAAAA")) { 68 | try { 69 | EncodedLevelSaveParametersToJson.INSTANCE.LevelDataToJson.converter( 70 | level, EncodedLevelSaveParametersToJson.INSTANCE.MainCrypto.gzUnzip( 71 | EncodedLevelSaveParametersToJson.INSTANCE.MainCrypto.base64(level.k4)).toString()); 72 | } catch(error) { 73 | level.k4 = "Invalid level string"; 74 | throw error; 75 | } 76 | } else { 77 | EncodedLevelSaveParametersToJson.INSTANCE.LevelDataToJson.converter(level, level.k4); 78 | } 79 | } 80 | } 81 | 82 | /** 83 | * @private 84 | * @since 0.3.0 85 | * @version 0.1.0 86 | * @method k34 87 | * @param {Object} level 88 | */ 89 | k34(level) { 90 | let tempArray = []; 91 | if (level.k34) { 92 | level.k34 = EncodedLevelSaveParametersToJson.INSTANCE.MainCrypto.gzUnzip( 93 | EncodedLevelSaveParametersToJson.INSTANCE.MainCrypto.base64(level.k34)).toString(); 94 | if (level.k34) { 95 | tempArray = level.k34.substring(0, level.k34.length - 1).split(";"); 96 | level.k34 = {}; 97 | for (let i in tempArray) { 98 | level.k34[i] = tempArray[i]; 99 | } 100 | } 101 | } 102 | } 103 | 104 | /** 105 | * @private 106 | * @since 0.3.0 107 | * @version 0.1.0 108 | * @method k67 109 | * @param {Object} level 110 | */ 111 | k67(level) { 112 | if (level.k67) { 113 | level.k67 = level.k67.split("_"); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /api/converters/save/EncodedSaveToAcceptedJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name EncodedSaveToAcceptedJson 6 | * @typedef {Object} EncodedSaveToAcceptedJson 7 | */ 8 | module.exports = class EncodedSaveToAcceptedJson { 9 | 10 | /** 11 | * @public 12 | * @type {EncodedSaveToAcceptedJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see EncodedSaveToAcceptedJson 23 | */ 24 | constructor() { 25 | if (!EncodedSaveToAcceptedJson.INSTANCE) { 26 | EncodedSaveToAcceptedJson.INSTANCE = this; 27 | } 28 | return EncodedSaveToAcceptedJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name LevelArraysXmlToJson 35 | */ 36 | LevelArraysXmlToJson = new (require("./LevelArraysXmlToJson")); 37 | 38 | /** 39 | * @private 40 | * @type {Object} 41 | * @name EncodedSaveToXmlString 42 | */ 43 | EncodedSaveToXmlString = new (require("./EncodedSaveToXmlString")); 44 | 45 | /** 46 | * @private 47 | * @type {Object} 48 | * @name LevelSaveToLevelArray 49 | */ 50 | LevelSaveToLevelArray = new (require("./LevelSaveToLevelArray")); 51 | 52 | /** 53 | * @public 54 | * @since 0.3.0 55 | * @version 0.1.0 56 | * @method converter 57 | * @param {String} data 58 | * @returns {Object[]} 59 | */ 60 | converter(data) { 61 | return EncodedSaveToAcceptedJson.INSTANCE.LevelArraysXmlToJson.converter( 62 | EncodedSaveToAcceptedJson.INSTANCE.LevelSaveToLevelArray.converter( 63 | EncodedSaveToAcceptedJson.INSTANCE.EncodedSaveToXmlString.converter(data))); 64 | } 65 | } -------------------------------------------------------------------------------- /api/converters/save/EncodedSaveToXmlString.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name EncodedSaveToXml 6 | * @typedef {Object} EncodedSaveToXml 7 | */ 8 | module.exports = class EncodedSaveToXml { 9 | 10 | /** 11 | * @public 12 | * @type {EncodedSaveToXml} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see EncodedSaveToXml 23 | */ 24 | constructor() { 25 | if (!EncodedSaveToXml.INSTANCE) { 26 | EncodedSaveToXml.INSTANCE = this; 27 | } 28 | return EncodedSaveToXml.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name MainCrypto 35 | */ 36 | MainCrypto = new (require("../../crypto/MainCrypto")); 37 | 38 | /** 39 | * @public 40 | * @since 0.3.0 41 | * @version 0.1.0 42 | * @method converter 43 | * @param {String} save 44 | * @returns {String} 45 | */ 46 | converter(save) { 47 | const crypto = EncodedSaveToXml.INSTANCE.MainCrypto; 48 | return crypto.gzUnzip( 49 | crypto.base64( 50 | crypto.xor( 51 | crypto.base64(save).toString("utf8"), crypto.keys.saveFileXOR))).toString(); 52 | } 53 | } -------------------------------------------------------------------------------- /api/converters/save/LevelArraysXmlToJson.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name LevelArraysXmlToJson 6 | * @typedef {Object} LevelArraysXmlToJson 7 | */ 8 | module.exports = class LevelArraysXmlToJson { 9 | 10 | /** 11 | * @public 12 | * @type {LevelArraysXmlToJson} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see LevelArraysXmlToJson 23 | */ 24 | constructor() { 25 | if (!LevelArraysXmlToJson.INSTANCE) { 26 | LevelArraysXmlToJson.INSTANCE = this; 27 | } 28 | return LevelArraysXmlToJson.INSTANCE; 29 | } 30 | 31 | /** 32 | * @public 33 | * @since 0.3.0 34 | * @version 0.1.0 35 | * @method converter 36 | * @param {Object[]} xmlArray 37 | * @returns {Object} 38 | */ 39 | converter(xmlArray) { 40 | let outData = []; 41 | let inSubDir = undefined; 42 | let tempObjectKey = ""; 43 | for (let i in xmlArray) { 44 | xmlArray[i] = xmlArray[i].split("><"); 45 | outData[i] = {}; 46 | for (let j = 1; j < xmlArray[i].length; j += 2) { 47 | if (xmlArray[i][j] != "/d>" && xmlArray[i][j] != "/d") { 48 | tempObjectKey = xmlArray[i][j].split(">")[1].split("")[1].split("")[1].split("LLM_02")[0] 44 | .split("_isArrk_0")[1] 45 | .split("k_"); 46 | let levels = [dirs[0]]; 47 | 48 | for (let i = 1; i < dirs.length; i++) { 49 | dirs[i] = dirs[i].split(i + ""); 50 | dirs[i].shift(); 51 | levels.push(dirs[i].join(i + "")); 52 | } 53 | 54 | return levels; 55 | } 56 | } -------------------------------------------------------------------------------- /api/crypto/MainCrypto.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name MainCrypto 6 | * @typedef {Object} MainCrypto 7 | */ 8 | module.exports = class MainCrypto { 9 | 10 | /** 11 | * @private 12 | * @type {MainCrypto} 13 | * @name INSTANCE 14 | */ 15 | static INSTANCE; 16 | 17 | /** 18 | * @public 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @method constructor 22 | * @summary The class constructor @see MainCrypto 23 | */ 24 | constructor() { 25 | if (!MainCrypto.INSTANCE) { 26 | MainCrypto.INSTANCE = this; 27 | } 28 | return MainCrypto.INSTANCE; 29 | } 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name pako 35 | */ 36 | pako = require("pako"); 37 | 38 | /** 39 | * @public 40 | * @since 0.1.0 41 | * @version 0.1.0 42 | * @type {Object} 43 | * @name keys 44 | * @summary The encoding keys for different encodings 45 | */ 46 | keys = { 47 | saveFileXOR: "11" 48 | } 49 | 50 | /** 51 | * @public 52 | * @since 0.3.0 53 | * @version 0.1.0 54 | * @method xor 55 | * @summary A simple xor decoder 56 | * @description A xor decoder which does a simple xor calculation on all bytes 57 | * @param {String} xorData The xor data to decode 58 | * @param {Number} key The decoding key @see keys 59 | * @returns {String} The decoded string 60 | */ 61 | xor(xorData, key) { 62 | return xorData.split("").map(str => String.fromCharCode(key ^ str.charCodeAt(0))).join(""); 63 | } 64 | 65 | /** 66 | * @public 67 | * @since 0.3.0 68 | * @version 0.1.0 69 | * @method base64 70 | * @summary A simple base64 decoder 71 | * @description A base64 decoder which makes it first url unsafe and then generates a base64 buffer 72 | * @param {String} base64String The base64 data to decode 73 | * @returns {Buffer} The decoded buffer 74 | */ 75 | base64(base64String) { 76 | return Buffer.from(base64String.replace(/-/g, "+").replace(/_/g, "/"), "base64"); 77 | } 78 | 79 | /** 80 | * @public 81 | * @since 0.3.0 82 | * @version 0.1.0 83 | * @method gzUnzip 84 | * @summary A simple gzUnzip decoder 85 | * @description A gzUnzip decoder which uses @see pako and then decodes the buffer to utf-8 86 | * @param {String} gzipString The gzip data to decode 87 | * @returns {String} The decoded string 88 | */ 89 | gzUnzip(gzipString) { 90 | return new TextDecoder("utf-8").decode(MainCrypto.INSTANCE.pako.inflate(gzipString)); 91 | } 92 | } -------------------------------------------------------------------------------- /api/loggers/ErrorLogs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name ErrorLogs 6 | * @typedef {Object} ErrorLogs 7 | */ 8 | module.exports = class ErrorLogs { 9 | 10 | /** 11 | * @public 12 | * @since 0.3.0 13 | * @version 0.1.0 14 | * @method init 15 | * @summary The main method which will report an error 16 | * @description The init method where all the main web app config is located and where the web app is started 17 | * @param {String} error The error message 18 | * @param {String} section The API section where the error occurred 19 | * @param {Object} globalData An object containing all the values and methods the application needs 20 | */ 21 | init(error, section, globalData) { 22 | if (globalData.config.settings.logging.errorLogging) { 23 | console.error( 24 | "\n\nError case #%s\nTime of error: %s\nSection: %s\n\x1b[31m%s\x1b[0m", 25 | globalData.errorCase++, 26 | Date.now(), 27 | section, 28 | error 29 | ); 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /api/loggers/StartupLogs.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name StartupLogs 6 | * @typedef {Object} StartupLogs 7 | */ 8 | module.exports = class StartupLogs { 9 | 10 | /** 11 | * @private 12 | * @async 13 | * @type {Function} 14 | * @name exec 15 | */ 16 | exec = require("child_process").exec; 17 | 18 | /** 19 | * @private 20 | * @since 0.2.0 21 | * @version 0.1.0 22 | * @type {Object} 23 | * @name dependencies 24 | * @summary The dependencies used by the program 25 | */ 26 | dependencies = require("../../package.json").dependencies; 27 | 28 | /** 29 | * @async 30 | * @public 31 | * @since 0.3.0 32 | * @version 0.1.0 33 | * @method init 34 | * @summary The base analytics constructor service 35 | * @description The constructing method converting the navigation HTML blocks to the full navigation 36 | * @param {Object} globalData An object containing all the values and methods the application needs 37 | */ 38 | async init(globalData) { 39 | let command = ""; 40 | 41 | console.log(`\x1b[34mApp started at ${globalData.startDate}\x1b[0m`); 42 | if (globalData.config.settings.expressWebServer) { 43 | console.log(`\x1b[32mserver address: http://localhost:${globalData.config.values.webServer.port}\x1b[0m`); 44 | } 45 | console.log(`\x1b[33mModules loaded:\n${JSON.stringify(this.dependencies, null, 2)}\x1b[0m`); 46 | 47 | for (let i in Object.keys(this.dependencies)) { 48 | command += await this.getUpdates(Object.keys(this.dependencies)[i]); 49 | } 50 | if (command) { 51 | console.log(`\x1b[31mRecommended command to run:\n${command}\x1b[0m`); 52 | } 53 | } 54 | 55 | /** 56 | * @async 57 | * @private 58 | * @since 0.3.0 59 | * @version 0.1.0 60 | * @method getUpdates 61 | * @summary The method which checks for updates 62 | * @description The command method which checks for all dependencies if there's an update ready 63 | * @param {String} key The dependency 64 | */ 65 | getUpdates(key) { 66 | return new Promise((resolve) => { 67 | this.exec(`npm outdated ${key}`, (error) => { 68 | if (error) 69 | resolve(`npm update ${key}; `); 70 | resolve(""); 71 | }); 72 | }); 73 | } 74 | } -------------------------------------------------------------------------------- /api/services/analytics/AnalyticsDataService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name AnalyticsDataService 6 | * @typedef {Object} AnalyticsDataService 7 | */ 8 | module.exports = class AnalyticsDataService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name v8 14 | */ 15 | v8 = require("v8"); 16 | 17 | /** 18 | * @private 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @type {Number} 22 | * @name totalMemory 23 | * @summary The total heap memory limit 24 | */ 25 | totalMemory = this.v8.getHeapStatistics().heap_size_limit; 26 | 27 | /** 28 | * @async 29 | * @private 30 | * @type {Function} 31 | * @name timeTest 32 | */ 33 | timeTest = require("../../background/OpenLoop").INSTANCE.timeTest; 34 | 35 | /** 36 | * @async 37 | * @public 38 | * @since 0.3.0 39 | * @version 0.1.0 40 | * @method service 41 | * @summary The base analytics data service 42 | * @description The constructing method converting the navigation HTML blocks to the full navigation 43 | * @param {Object} globalData An object containing all the values and methods the application needs 44 | * @returns {Promise<[Number, Object]>} The analytics data and response status 45 | */ 46 | service(globalData) { 47 | return new Promise(async (resolve) => { 48 | const memoryUsage = process.memoryUsage().heapUsed; 49 | let errorProgress = this.calculateHealth(globalData); 50 | let keys = Object.keys(globalData.graphEntries); 51 | let outData = Array(2); 52 | let eColor = "#418E51"; 53 | let tempI = 0; 54 | 55 | if (errorProgress < 0) { 56 | eColor = "#8E4141"; 57 | if (errorProgress <= -100) { 58 | errorProgress = 100; 59 | } else { 60 | errorProgress = Math.abs(errorProgress); 61 | } 62 | } 63 | 64 | for (let i in keys) { 65 | if (globalData.config.settings.endpoints 66 | [`enable${globalData.graphEntries[keys[i]].settingsName}Endpoint`]) { 67 | try { 68 | outData[tempI] = await this.timeTest(globalData.graphEntries[keys[i]], keys[i]); 69 | } catch(error) { 70 | outData[tempI] = 0; 71 | globalData.errorFunc(error, "analytics-base", globalData); 72 | } 73 | tempI++; 74 | } 75 | } 76 | 77 | resolve([200, { 78 | memoryText: `

Memory usage: ${Math.round(memoryUsage / 1024 / 1024 * 100) / 100}Mb

`, 79 | memoryProgress: `
80 | 81 |

${Math.round(memoryUsage / (this.totalMemory / 100) * 100) / 100}%

82 |
`, 83 | errorProgress: `
84 | 85 |

${this.calculateHealth(globalData)}%

86 |
`, 87 | extra: `

Visits: ${globalData.visits}

88 |

Uptime: ${Math.round((Date.now() - globalData.startDate) / 86400000 * 100) / 100} days

89 |

Total errors: ${globalData.errorCase - 1}

`, 90 | browser: this.generateBrowserInfo(globalData), 91 | chartData: this.generateChartData(globalData, outData, keys) 92 | }]); 93 | }); 94 | } 95 | 96 | /** 97 | * @private 98 | * @since 0.3.0 99 | * @version 0.1.0 100 | * @method calculateHealth 101 | * @summary The API health calculation method 102 | * @description A method calculating the health based on the max errors per 100 visits @see config 103 | * @param {Object} globalData An object containing all the values and methods the application needs 104 | * @returns {Number} The current health 105 | */ 106 | calculateHealth(globalData) { 107 | const maxErrors = globalData.visits || 1 / 100 * globalData.config.values.analytics.maxErrorsPer100Visits; 108 | 109 | return Math.round(((maxErrors - (globalData.errorCase - 1)) / (maxErrors / 100)) * 10) / 10; 110 | } 111 | 112 | /** 113 | * @private 114 | * @since 0.3.0 115 | * @version 0.1.0 116 | * @method generateBrowserInfo 117 | * @summary The user browser navigation constructor 118 | * @description This method generates the navigation for all the browsers and browser versions used to visit this API 119 | * @param {Object} globalData An object containing all the values and methods the application needs 120 | * @returns {String} The navigation HTML block 121 | */ 122 | generateBrowserInfo(globalData) { 123 | let out = ""; 124 | if (globalData.config.settings.analytics.showBrowserVisits) { 125 | const data = globalData.browser; 126 | const keys = Object.keys(data); 127 | let innerKeys; 128 | 129 | for (let i in keys) { 130 | out += ` 131 | ${keys[i]}: 132 |

${data[keys[i]].visits} visits

133 |
134 | `; 135 | innerKeys = Object.keys(data[keys[i]].versions); 136 | for (let j in innerKeys) { 137 | out += `v${innerKeys[j]}

${data[keys[i]].versions[innerKeys[j]]} visits

`; 138 | } 139 | out += "
"; 140 | } 141 | } else { 142 | out = "denied"; 143 | } 144 | 145 | return out; 146 | } 147 | 148 | /** 149 | * @private 150 | * @since 0.3.0 151 | * @version 0.1.0 152 | * @method generateChartData 153 | * @summary The chartData generator 154 | * @description This method generates the service response time data in the chart format which can be used by the frontend 155 | * @param {Object} globalData An object containing all the values and methods the application needs 156 | * @param {Number[]} now The current response times 157 | * @param {String[]} keys the graph entry keys which need to be given 158 | * @returns {Object} The chart data object 159 | */ 160 | generateChartData(globalData, now, keys) { 161 | let output = []; 162 | let tempI = 0; 163 | 164 | for (let i in keys) { 165 | if (globalData.config.settings.endpoints 166 | [`enable${globalData.graphEntries[keys[i]].settingsName}Endpoint`]) { 167 | output[tempI] = { 168 | label: keys[i], 169 | data: [], 170 | borderColor: globalData.graphEntries[keys[i]].color 171 | }; 172 | for (let j in globalData.graphEntries[keys[i]].data) { 173 | output[tempI].data[j] = globalData.graphEntries[keys[i]].data[j]; 174 | } 175 | output[tempI].data.push(now[tempI]); 176 | tempI++; 177 | } 178 | } 179 | 180 | return output; 181 | } 182 | } -------------------------------------------------------------------------------- /api/services/analytics/AnalyticsIndexService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name AnalyticsIndexService 6 | * @typedef {Object} AnalyticsIndexService 7 | */ 8 | module.exports = class AnalyticsIndexService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name fs 14 | */ 15 | fs = require("fs"); 16 | 17 | /** 18 | * @private 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @type {[Number, String]} 22 | * @name output 23 | * @summary The saved output after the first use 24 | */ 25 | output; 26 | 27 | /** 28 | * @async 29 | * @public 30 | * @since 0.3.0 31 | * @version 0.1.0 32 | * @method service 33 | * @summary The base analytics constructor service 34 | * @description The constructing method converting the navigation HTML blocks to the full navigation 35 | * @param {Object} globalData An object containing all the values and methods the application needs 36 | * @returns {Promise<[Number, String]>} The analytics page and response status 37 | */ 38 | service(globalData) { 39 | return new Promise((resolve) => { 40 | if (!this.output) { 41 | this.fs.readFile("resources/pages/analytics/base.html", "utf8", (error, content) => { 42 | if (error) { 43 | globalData.errorFunc(error, "analytics", globalData); 44 | resolve([500, "

INTERNAL SERVER ERROR 500

"]); 45 | } 46 | this.output = [200, 47 | content.replace(`"%REFRESH%"`, globalData.config.values.analytics.refreshRateMs || 2000) 48 | .replace(`"%ROUND%"`, globalData.config.values.analytics.chartRoundOn) 49 | .replace(`"%CLICK%"`, ( 50 | (globalData.config.settings.analytics.allowChartInteraction) ? 51 | "" : ",onClick: (e) => e.stopPropagation()"))]; 52 | resolve(this.output); 53 | }); 54 | } else { 55 | resolve(this.output); 56 | } 57 | }); 58 | } 59 | } -------------------------------------------------------------------------------- /api/services/documentation/DocumentationDataService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name DocumentationDataService 6 | * @typedef {Object} DocumentationDataService 7 | */ 8 | module.exports = class DocumentationDataService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name fs 14 | */ 15 | fs = require("fs"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name MdToHtml 21 | */ 22 | MdToHtml = new (require("../../converters/documentation/MdToHtml")); 23 | 24 | /** 25 | * @async 26 | * @public 27 | * @since 0.3.0 28 | * @version 0.1.0 29 | * @method service 30 | * @summary The base service to generate the documentation page 31 | * @description The main service layer which using the navigation requests a markdown page and converts it to HTML 32 | * @param {String[]} route The url navigation string 33 | * @returns {Promise<[Number, String]>} The converted markdown page and the response status 34 | */ 35 | service(route) { 36 | return new Promise((resolve) => { 37 | this.fs.readFile(route, "utf8", (error, data) => { 38 | if (error) { 39 | resolve([404, "

ERROR 404 NOT FOUND

This file wasn't found

"]); 40 | } else { 41 | resolve([200, this.MdToHtml.converter(data)]); 42 | } 43 | }); 44 | }); 45 | } 46 | } -------------------------------------------------------------------------------- /api/services/documentation/DocumentationIndexService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name DocumentationIndexService 6 | * @typedef {Object} DocumentationIndexService 7 | */ 8 | module.exports = class DocumentationIndexService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name fs 14 | */ 15 | fs = require("fs"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name LowerCaseWordToTitleWord 21 | */ 22 | LowerCaseWordToTitleWord = new (require("../../converters/LowerCaseWordToTitleWord")); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name UrlToRoute 28 | */ 29 | UrlToRoute = new (require("../../converters/documentation/ParamsToRoute")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name DocumentationDataService 35 | */ 36 | DocumentationDataService = new (require("./DocumentationDataService")); 37 | 38 | /** 39 | * @private 40 | * @since 0.2.0 41 | * @version 0.1.0 42 | * @type {String} 43 | * @name onclick 44 | * @summary The onclick string 45 | */ 46 | onclick = "onclick=\"openPage('%URL%', this)\""; 47 | 48 | /** 49 | * @private 50 | * @since 0.2.0 51 | * @version 0.1.0 52 | * @type {String} 53 | * @name title 54 | * @summary The title element string 55 | */ 56 | title = `

57 | %CONTENT% 58 |

`; 59 | 60 | /** 61 | * @private 62 | * @since 0.2.0 63 | * @version 0.1.0 64 | * @type {String} 65 | * @name topic 66 | * @summary The topic element string 67 | */ 68 | topic = ` 69 |

%CONTENT%

70 |
`; 71 | 72 | /** 73 | * @async 74 | * @public 75 | * @since 0.3.0 76 | * @version 0.1.0 77 | * @method service 78 | * @summary The base service to generate the main body 79 | * @description The main service layer which generates the docs body and sets the navigation highlight to right place 80 | * @param {String[]} params The url navigation string 81 | * @returns {Promise<[Number, String]>} The HTML page and the response status 82 | */ 83 | service(params) { 84 | return new Promise((resolve) => { 85 | let ArrayFile = []; 86 | let fileToRead = this.UrlToRoute.converter(params); 87 | 88 | this.fs.readFile("resources/pages/docs/base.html", "utf8", (error, content) => { 89 | if (error) { 90 | resolve([500, "

INTERNAL SERVER ERROR 500

"]); 91 | } else { 92 | ArrayFile = fileToRead.split("resources/docs/")[1].split("/"); 93 | content = content.replace("%URL%", ArrayFile.join("/")); 94 | resolve([200, content.replace("%NAV%", this.constructNav(ArrayFile))]); 95 | } 96 | }); 97 | }); 98 | } 99 | 100 | /** 101 | * @private 102 | * @since 0.3.0 103 | * @version 0.1.0 104 | * @method constructNav 105 | * @summary The navigation constructing 106 | * @description The constructing method converting the navigation HTML blocks to the full navigation 107 | * @param {String[]} fileToRead The url navigation array 108 | * @returns {String} The navigation HTML block 109 | */ 110 | constructNav(fileToRead) { 111 | let out = ""; 112 | let files = []; 113 | const dir = "resources/docs/"; 114 | const folders = this.filesSorter(this.fs.readdirSync(dir), dir); 115 | 116 | for (let i = 0; i < folders.length; i++) { 117 | out += this.navContentMaker([folders[i], "index.md"], fileToRead); 118 | files = this.filesSorter(this.fs.readdirSync(dir + folders[i]), dir + folders[i]); 119 | 120 | for (let j = 0; j < files.length; j++) { 121 | if (files[j] != "index.md") { 122 | out += this.navContentMaker([folders[i], files[j]], fileToRead); 123 | } 124 | } 125 | } 126 | 127 | return out; 128 | } 129 | 130 | /** 131 | * @private 132 | * @since 0.3.0 133 | * @version 0.1.0 134 | * @method navContentMaker 135 | * @summary The navigation link generator 136 | * @description This will generate a navigation link and set the highlight if needed 137 | * @param {String[]} names The current file to generate a link for 138 | * @param {String[]} fileToRead The url navigation array 139 | * @returns {String} The link HTML block 140 | */ 141 | navContentMaker(names, fileToRead) { 142 | let out = ""; 143 | 144 | if (names[1] == "index.md") { 145 | out = this.title.replace("%CONTENT%", this.LowerCaseWordToTitleWord.converter(names[0])); 146 | } else { 147 | out = this.topic.replace("%CONTENT%", this.LowerCaseWordToTitleWord.converter(names[1])); 148 | } 149 | if (JSON.stringify(names) == JSON.stringify(fileToRead)) { 150 | out = out.replace("%HIGH%", "highlight"); 151 | } else { 152 | out = out.replace("%HIGH%", ""); 153 | } 154 | 155 | return out.replace("%URL%", `${names[0]}/${names[1]}`); 156 | } 157 | 158 | /** 159 | * @private 160 | * @since 0.3.0 161 | * @version 0.1.0 162 | * @method filesSorter 163 | * @summary A simple file sorter 164 | * @description The file sorter sorting all files based on the edit date allowing you to easily order it 165 | * @param {String[]} files The files array to sort 166 | * @param {String} dir The directory where the files are located in 167 | * @returns {String[]} The ordered files 168 | */ 169 | filesSorter(files, dir) { 170 | files.sort((a, b) => 171 | this.fs.statSync(dir + a).mtime.getTime() - this.fs.statSync(dir + b).mtime.getTime()); 172 | 173 | return files; 174 | } 175 | } -------------------------------------------------------------------------------- /api/services/gdEndpoint/GdEndpointDataService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name GdEndpointDataService 6 | * @typedef {Object} GdEndpointDataService 7 | */ 8 | module.exports = class GdEndpointDataService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name http 14 | */ 15 | http = require("http"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name endpointProperties 21 | */ 22 | endpointProperties = require("../../values/endpointProperties.json"); 23 | 24 | /** 25 | * @private 26 | * @type {Object} 27 | * @name SmallGdResponseToJson 28 | */ 29 | SmallGdResponseToJson = new (require("../../converters/gdEndpoint/SmallGdResponseToJson")); 30 | 31 | /** 32 | * @private 33 | * @type {Object} 34 | * @name BigResponseToJson 35 | */ 36 | BigResponseToJson = new (require("../../converters/gdEndpoint/BigResponseToJson")); 37 | 38 | /** 39 | * @async 40 | * @public 41 | * @since 0.3.0 42 | * @version 0.1.0 43 | * @method service 44 | * @summary The endpoint data service 45 | * @description The main service method processing a GD request and calls the response parsers 46 | * Type info: 47 | * 0: Default split 48 | * 1: Multi split 49 | * 2: Multi data sections 50 | * 3: Alt misc 51 | * 4: Without # data 52 | * 5: Default split with alt misc data 53 | * 6: Split based on index 54 | * @param {String[]} params The params array containing all data which needs to go into the post data 55 | * @param {Object} globalData An object containing all the values and methods the application needs 56 | * @returns {Promise<[Number, Object]>} The gd response object and the response status 57 | */ 58 | service(params, globalData) { 59 | return new Promise((resolve) => { 60 | let output = {}; 61 | // Setting valid property or gauntlets since daily requires no params 62 | const type = this.endpointProperties[params[0].toLowerCase()] || 63 | this.endpointProperties.daily; 64 | 65 | // Checking if too many leaderboards entries are requested and converting a number param to a valid type 66 | if (params[0] == "leaderboard") { 67 | if (parseInt(params[1])) { 68 | if (parseInt(params[1]) > 10000) { 69 | resolve([400, {"ERROR":"Too much to request"}]); 70 | } else { 71 | if (parseInt(params[2]) == 2) { 72 | params[2] = "creators"; 73 | } else if (params[2] != "creators") { 74 | params[2] = "top"; 75 | } 76 | } 77 | } else { 78 | resolve([400, {"ERROR":"Invalid request"}]); 79 | } 80 | } 81 | 82 | this.postRequest(this.setValue(type.post, params), type.url, globalData).then((data) => { 83 | if (parseInt(data) >= 0 && data != "") { 84 | if (type.type.indexOf(0) != -1 || type.type.indexOf(5) != -1 || type.type.indexOf(6) != -1) { 85 | // type 0, 5, 6 86 | this.SmallGdResponseToJson.converter(data, type, output); 87 | } else { 88 | // type 1-4 89 | output = []; 90 | this.BigResponseToJson.converter(data, type, output); 91 | } 92 | resolve([200, output]); 93 | } else { 94 | resolve([404, {"ERROR":"Not found"}]); 95 | } 96 | }).catch((error) => { 97 | globalData.errorFunc(error, "gdEndpoint-request", globalData); 98 | resolve([400, {"ERROR":"Bad request"}]); 99 | }); 100 | }); 101 | } 102 | 103 | /** 104 | * @private 105 | * @since 0.3.0 106 | * @version 0.1.0 107 | * @method setValue 108 | * @summary A simple query template formatter 109 | * @description A query constructor which will replace all value{Number} with the right value 110 | * @param {String} post The post query template 111 | * @param {String[]} params The params which should be mixed into the post template 112 | * @returns {String} The properly formatted post query 113 | */ 114 | setValue(post, params) { 115 | let dar = post.split("=value"); 116 | let output = dar.shift(); 117 | dar.forEach((list) => output += `=${params[list.charAt(0)] || 0}${list.substring(1)}`); 118 | return output; 119 | } 120 | 121 | /** 122 | * @async 123 | * @private 124 | * @since 0.3.0 125 | * @version 0.1.0 126 | * @summary The post request method to extract the GD data 127 | * @description A request method sending a post request with the data to the GD servers and returning the data 128 | * @param {String} params The post query 129 | * @param {String} endpoint The endpoint which should be requested 130 | * @returns {Promise} The gd endpoint response (or error) 131 | */ 132 | postRequest(params, endpoint, globalData) { 133 | return new Promise((resolve, reject) => { 134 | const options = { 135 | host: globalData.config.values.endpoints.targetServer, 136 | port: 80, 137 | path: `/database/${endpoint}`, 138 | method: "POST", 139 | headers: { 140 | 'Content-Type': 'application/x-www-form-urlencoded', 141 | 'Content-Length': Buffer.byteLength(params) 142 | } 143 | }; 144 | const req = this.http.request(options, (res) => { 145 | res.setEncoding("utf8"); 146 | res.on("data", (chunk) => { 147 | resolve(chunk); 148 | }); 149 | res.on("error", (error) => { 150 | reject(error); 151 | }); 152 | }); 153 | 154 | req.write(params); 155 | req.end(); 156 | }); 157 | } 158 | } -------------------------------------------------------------------------------- /api/services/gdEndpoint/GdEndpointIndexService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name GdEndpointIndexService 6 | * @typedef {Object} GdEndpointIndexService 7 | */ 8 | module.exports = class GdEndpointIndexService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name fs 14 | */ 15 | fs = require("fs"); 16 | 17 | /** 18 | * @private 19 | * @since 0.3.0 20 | * @version 0.1.0 21 | * @type {[Number, String]} 22 | * @name output 23 | * @summary The saved output after the first use 24 | */ 25 | output; 26 | 27 | /** 28 | * @private 29 | * @type {Object} 30 | * @name endpointProperties 31 | */ 32 | endpointProperties = require("../../values/endpointProperties.json"); 33 | 34 | /** 35 | * @async 36 | * @public 37 | * @since 0.3.0 38 | * @version 0.1.0 39 | * @method service 40 | * @summary The base service to generate the main body 41 | * @description The main service layer which generates and saves the main GD endpoint body 42 | * @param {Object} globalData An object containing all the values and methods the application needs 43 | * @returns {Promise<[Number, String]>} The HTML page and the response status 44 | */ 45 | service(globalData) { 46 | let linkInfo = ""; 47 | return new Promise((resolve) => { 48 | if (this.output) { 49 | resolve(this.output); 50 | } else { 51 | this.fs.readFile("resources/pages/endpoints/base.html", "utf8", 52 | (error, contents) => { 53 | if (error) { 54 | globalData.errorFunc(error, "endpoints", globalData); 55 | resolve([500, "There was an error while fetching this file."]); 56 | } else { 57 | Object.keys(this.endpointProperties).forEach((key) => { 58 | linkInfo += `
59 |

${key[0].toUpperCase()+key.slice(1)}

60 |

61 | URL: 62 | 63 | ${this.endpointProperties[key].link} 64 | 65 |

66 |
`; 67 | }); 68 | 69 | // Once generated this will be used instead of being fully generated again 70 | this.output = [200, contents.replace("%body%", linkInfo)]; 71 | 72 | resolve(this.output); 73 | } 74 | }); 75 | } 76 | }); 77 | } 78 | } -------------------------------------------------------------------------------- /api/services/save/LevelSaveIndexService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name LevelsIndexService 6 | * @typedef {Object} LevelsIndexService 7 | */ 8 | module.exports = class LevelsIndexService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name fs 14 | */ 15 | fs = require("fs"); 16 | 17 | /** 18 | * @private 19 | * @type {String[]} 20 | * @name songs 21 | */ 22 | songs = require("../../values/songs.json"); 23 | 24 | /** 25 | * @private 26 | * @since 0.1.0 27 | * @version 0.1.0 28 | * @type {String} 29 | * @name baseHTML 30 | * @summary The base HTML block 31 | */ 32 | baseHTML = `
33 |

_k2_

34 |
35 | 36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | Song ID: _k45_ 44 | Time spent: _k80_ 45 | Objects: _k48_ 46 |
`; 47 | 48 | /** 49 | * @async 50 | * @public 51 | * @since 0.3.0 52 | * @version 0.1.0 53 | * @method service 54 | * @summary The base service to generate the main body 55 | * @description The index service which parses the save data and makes a menu which links through select buttons the selective service 56 | * @param {object} globalData An object containing all the values and methods the application needs 57 | * @param {Object[]} body The levels array to process 58 | * @returns {Promise<[Number, String]>} The HTML page and the response status 59 | */ 60 | service(globalData, body) { 61 | return new Promise((resolve) => { 62 | this.fs.readFile("resources/pages/levelSave/base.html", "utf8", (error, contents) => { 63 | if (error) { 64 | globalData.errorFunc(error, "level-save-index", globalData); 65 | resolve([500, "There was an error while fetching this file."]); 66 | } 67 | try { 68 | resolve([200, contents.replace("%BODY%", this.pageConstructor(body))]); 69 | } catch (error) { 70 | globalData.errorFunc(error, "level-save-index", globalData); 71 | resolve([400, "There was an error with parsing your data."]) 72 | } 73 | }); 74 | }); 75 | } 76 | 77 | /** 78 | * @private 79 | * @since 0.3.0 80 | * @version 0.1.0 81 | * @method pageConstructor 82 | * @summary The body generator 83 | * @description The main page body generator method which generates the selection boxes 84 | * @param {Array.} levels the levels array to process 85 | * @returns {String} the body to draw on the base 86 | */ 87 | pageConstructor(levels) { 88 | let out = ""; 89 | let tempTime = "Seconds"; 90 | let tempHTML = []; 91 | let dataCheck; 92 | 93 | for (let i in Object.keys(levels)) { 94 | // Splits up all the placeholder values 95 | tempHTML = this.baseHTML.split("_"); 96 | for (let j = 1; j < tempHTML.length; j += 2) { 97 | dataCheck = levels[i][tempHTML[j]]; 98 | 99 | // View Button inserting all needed data 100 | if (tempHTML[j] == "all") { 101 | dataCheck = JSON.stringify(levels[i]); 102 | } else { 103 | // Default values 104 | if (!dataCheck) { 105 | switch (tempHTML[j]) { 106 | case "k48": 107 | dataCheck = "Not found"; 108 | break; 109 | case "k2": 110 | dataCheck = "Eeeeh, well then"; 111 | break; 112 | case "k45": 113 | dataCheck = this.songs[levels[i].k8] || "Stereo Madness"; 114 | break; 115 | default: 116 | dataCheck = 0; 117 | break; 118 | } 119 | } 120 | } 121 | 122 | // Time format 123 | if (tempHTML[j] == "k80") { 124 | if (dataCheck >= 60) { 125 | tempTime = "Minutes"; 126 | dataCheck /= 60; 127 | if (dataCheck >= 60) { 128 | tempTime = "Hours"; 129 | dataCheck /= 60; 130 | } 131 | } 132 | 133 | // Because floor refused to work 134 | dataCheck = dataCheck.toString().split("."); 135 | if (dataCheck[1]) { 136 | dataCheck = `${dataCheck[0]}.${dataCheck[1].substring(0, 2)}`; 137 | } else { 138 | dataCheck = dataCheck[0]; 139 | } 140 | dataCheck += ` ${tempTime}`; 141 | } 142 | 143 | // Orders the progress bars based on which has the least amount of process and formats them 144 | if (tempHTML[j] == "k19" || tempHTML[j] == "k20") { 145 | if (dataCheck == "100") 146 | dataCheck = "5px + 100"; 147 | tempHTML[j] = dataCheck; 148 | j += 2; 149 | if (levels[i]["k19"] < levels[i]["k20"]) { 150 | if (tempHTML[j] == "indexk19") { 151 | tempHTML[j] = "1"; 152 | } else { 153 | tempHTML[j] = "2"; 154 | } 155 | } else { 156 | if (tempHTML[j] == "indexk19") { 157 | tempHTML[j] = "2"; 158 | } else { 159 | tempHTML[j] = "1"; 160 | } 161 | } 162 | } else { 163 | tempHTML[j] = dataCheck; 164 | } 165 | } 166 | out += tempHTML.join(""); 167 | } 168 | return out; 169 | } 170 | } -------------------------------------------------------------------------------- /api/services/save/SelectiveLevelSaveService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name SelectiveLevelSaveService 6 | * @typedef {Object} SelectiveLevelSaveService 7 | */ 8 | module.exports = class SelectiveLevelSaveService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name EncodedSaveToAcceptedJson 14 | */ 15 | EncodedSaveToAcceptedJson = new (require("../../converters/save/EncodedSaveToAcceptedJson")); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name EncodedLevelSaveParametersToJson 21 | */ 22 | EncodedLevelSaveParametersToJson = new ( 23 | require("../../converters/save/EncodedLevelSaveParametersToJson")); 24 | 25 | /** 26 | * @public 27 | * @since 0.3.0 28 | * @version 0.1.0 29 | * @method service 30 | * @summary The selective level data parser service 31 | * @description The data service converting encoded levels to an array and then call the right index or parses an already decoded level object 32 | * @param {Object} globalData An object containing all the values and methods the application needs 33 | * @param {String} data The level object or save data 34 | * @param {Number} index The level index it needs to request 35 | * @returns {[Number, Object]} The level object and the response status 36 | */ 37 | service(globalData, data, index) { 38 | let outputData; 39 | 40 | try { 41 | if (data.charAt(0) == "{") { 42 | outputData = JSON.parse(data); 43 | } else { 44 | outputData = this.EncodedSaveToAcceptedJson.converter(data)[index]; 45 | } 46 | this.EncodedLevelSaveParametersToJson.converter(outputData); 47 | return [200, outputData]; 48 | } catch(error) { 49 | globalData.errorFunc(error, "level-save-select", globalData); 50 | return [400, {"ERROR": "There was an error while decoding your data."}]; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /api/services/valueName/ValueNameDataService.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @class 3 | * @public 4 | * @author SMJS 5 | * @name ValueNameDataService 6 | * @typedef {Object} ValueNameDataService 7 | */ 8 | module.exports = class ValueNameDataService { 9 | 10 | /** 11 | * @private 12 | * @type {Object} 13 | * @name valueForName 14 | */ 15 | valueForName = require("../../values/valueForName.json"); 16 | 17 | /** 18 | * @private 19 | * @type {Object} 20 | * @name UrlNavigationToResponseEntity 21 | */ 22 | UrlNavigationToResponseEntity = new (require("../../converters/UrlNavigationToResponseEntity")); 23 | 24 | /** 25 | * @public 26 | * @since 0.3.0 27 | * @version 0.1.0 28 | * @method service 29 | * @summary The navigation service method 30 | * @description The service layer in between the data layer and the client layer to convert the url to the right value translations directory 31 | * @param {String[]} params The url params array as object navigation path 32 | * @returns {[Number, Object]} The navigated value translation object and response status 33 | */ 34 | service(params) { 35 | return this.UrlNavigationToResponseEntity.converter( 36 | params, this.valueForName); 37 | } 38 | } -------------------------------------------------------------------------------- /api/values/endpointProperties.json: -------------------------------------------------------------------------------- 1 | { 2 | "user": { 3 | "link": "user/:accountID", 4 | "post": "targetAccountID=value1&secret=Wmfd2893gb7", 5 | "url": "getGJUserInfo20.php", 6 | "type": [0], 7 | "splitChar": ":", 8 | "jsonData": ["user"], 9 | "page": "0" 10 | }, 11 | "userlist": { 12 | "link": "userlist/:searchstring/:page?", 13 | "post": "str=value1&page=value2&secret=Wmfd2893gb7", 14 | "url": "getGJUsers20.php", 15 | "type": [1], 16 | "splitChar": ":", 17 | "jsonData": ["user"], 18 | "page": "value2" 19 | }, 20 | "comments": { 21 | "link": "comments/:levelID/:mode?/:page?", 22 | "post": "levelID=value1&mode=value2&page=value3&secret=Wmfd2893gb7", 23 | "url": "getGJComments21.php", 24 | "type": [1, 2], 25 | "splitChar": "~", 26 | "jsonData": ["comment", "user"], 27 | "page": "value3" 28 | }, 29 | "accountcomments": { 30 | "link": "accountComments/:accountID/:page?", 31 | "post": "accountID=value1&page=value2&secret=Wmfd2893gb7", 32 | "url": "getGJAccountComments20.php", 33 | "type": [1], 34 | "splitChar": "~", 35 | "jsonData": ["comment"], 36 | "page": "value2" 37 | }, 38 | "commenthistory": { 39 | "link": "commentHistory/:userID/:mode?/:page?", 40 | "post": "userID=value1&mode=value2&page=value3&secret=Wmfd2893gb7", 41 | "url": "getGJCommentHistory.php", 42 | "type": [1, 2], 43 | "splitChar": "~", 44 | "jsonData": ["comment", "user"], 45 | "page": "value3" 46 | }, 47 | "maps": { 48 | "link": "maps/:page?", 49 | "post": "page=value1&secret=Wmfd2893gb7", 50 | "url": "getGJMapPacks21.php", 51 | "type": [1], 52 | "splitChar": ":", 53 | "jsonData": ["pack"], 54 | "page": "value1" 55 | }, 56 | "gauntlets": { 57 | "link": "gauntlets", 58 | "post": "special=1&secret=Wmfd2893gb7", 59 | "url": "getGJGauntlets21.php", 60 | "type": [1, 3], 61 | "splitChar": ":", 62 | "jsonData": ["pack"], 63 | "page": "0" 64 | }, 65 | "song": { 66 | "link": "song/:songID", 67 | "post": "songID=value1&secret=Wmfd2893gb7", 68 | "url": "getGJSongInfo.php", 69 | "type": [0], 70 | "splitChar": "~|~", 71 | "jsonData": ["song"], 72 | "page": "0" 73 | }, 74 | "leaderboard": { 75 | "link": "leaderboard/:count/:type?", 76 | "post": "count=value1&type=value2&secret=Wmfd2893gb7", 77 | "url": "getGJScores20.php", 78 | "type": [1, 4], 79 | "splitChar": ":", 80 | "jsonData": ["score"], 81 | "page": "0" 82 | }, 83 | "level": { 84 | "link": "level/:levelID", 85 | "post": "levelID=value1&secret=Wmfd2893gb7", 86 | "url": "downloadGJLevel22.php", 87 | "type": [5], 88 | "splitChar": ":", 89 | "jsonData": ["level"], 90 | "page": "0" 91 | }, 92 | "daily": { 93 | "link": "daily", 94 | "post": "weekly=0&secret=Wmfd2893gb7", 95 | "url": "getGJDailyLevel.php", 96 | "type": [6], 97 | "splitChar": "|", 98 | "jsonData": ["daily"], 99 | "page": "0" 100 | }, 101 | "weekly": { 102 | "link": "weekly", 103 | "post": "weekly=1&secret=Wmfd2893gb7", 104 | "url": "getGJDailyLevel.php", 105 | "type": [6], 106 | "splitChar": "|", 107 | "jsonData": ["daily"], 108 | "page": "0" 109 | } 110 | } -------------------------------------------------------------------------------- /api/values/songs.json: -------------------------------------------------------------------------------- 1 | [ 2 | "Stay Inside Me", 3 | "Stereo Madness", 4 | "Back On Track", 5 | "Polargeist", 6 | "Dry Out", 7 | "Base After Base", 8 | "Cant Let Go", 9 | "Jumper", 10 | "Time Machine", 11 | "Cycles", 12 | "xStep", 13 | "Clutterfunk", 14 | "Theory of Everything", 15 | "Electroman Adventures", 16 | "Clubstep", 17 | "Electrodynamix", 18 | "Hexagon Force", 19 | "Blast Processing", 20 | "Theory of Everything 2", 21 | "Geometrical Dominator", 22 | "Deadlocked", 23 | "Fingerdash", 24 | "Explorers", 25 | "Firebird", 26 | "The Seven Seas", 27 | "Viking Arena", 28 | "Airborne Robots", 29 | "Secret", 30 | "Payload", 31 | "Beast Mode", 32 | "Machina", 33 | "Years", 34 | "Frontlines", 35 | "Space Pirates", 36 | "Striker", 37 | "Embers", 38 | "Round 1", 39 | "Monster Dance Off", 40 | "Press Start", 41 | "Nock Em", 42 | "Power Trip" 43 | ] -------------------------------------------------------------------------------- /api/values/valueForName.json: -------------------------------------------------------------------------------- 1 | { 2 | "endpoints": { 3 | "user": { 4 | "1": "Username", 5 | "2": "UserID", 6 | "3": "Stars", 7 | "4": "Demons", 8 | "6": "?", 9 | "8": "CP", 10 | "9": "ShowcaseIcon", 11 | "10": "Color1", 12 | "11": "Color2", 13 | "13": "SecretCoins", 14 | "14": "ShowcaseIconType", 15 | "15": "Special", 16 | "16": "AccountID", 17 | "17": "UserCoins", 18 | "18": "AllowMessages", 19 | "19": "AllowFriendRequests", 20 | "20": "YouTube", 21 | "21": "Cube", 22 | "22": "Ship", 23 | "23": "Ball", 24 | "24": "Ufo", 25 | "25": "Wave", 26 | "26": "Robot", 27 | "28": "Glow", 28 | "29": "?", 29 | "30": "Rank", 30 | "31": "FriendState", 31 | "43": "Spider", 32 | "44": "Twitter", 33 | "45": "Twitch", 34 | "46": "Diamonds", 35 | "48": "DeathEffect", 36 | "49": "ModBadge", 37 | "50": "ShowCommentHistory" 38 | }, 39 | "comment": { 40 | "1": "LevelID", 41 | "2": "Comment", 42 | "3": "UserID", 43 | "4": "Likes", 44 | "5": "?", 45 | "6": "CommentID", 46 | "7": "IsSpam", 47 | "9": "UploadDate", 48 | "10": "Percent", 49 | "11": "ModBadge", 50 | "12": "Color" 51 | }, 52 | "pack": { 53 | "1": "ID", 54 | "2": "Name", 55 | "3": "Levels", 56 | "4": "Stars", 57 | "5": "Coins", 58 | "6": "Difficulty", 59 | "7": "NameColor", 60 | "8": "BarColor" 61 | }, 62 | "song": { 63 | "1": "SongID", 64 | "2": "Name", 65 | "3": "?", 66 | "4": "Author", 67 | "5": "Size", 68 | "6": "?", 69 | "7": "?", 70 | "10": "FileLink" 71 | }, 72 | "score": { 73 | "1": "Username", 74 | "2": "UserID", 75 | "3": "Stars", 76 | "4": "Demons", 77 | "6": "Spot", 78 | "7": "AccountID", 79 | "8": "CP", 80 | "9": "ShowcaseIcon", 81 | "10": "Color 1", 82 | "11": "Color 2", 83 | "13": "Coins", 84 | "14": "ShowcaseIconType", 85 | "15": "Special", 86 | "16": "AccountID", 87 | "17": "UserCoins", 88 | "46": "Diamonds" 89 | }, 90 | "level": { 91 | "1": "LevelID", 92 | "2": "LevelName", 93 | "3": "Description", 94 | "4": "LevelData", 95 | "5": "Version", 96 | "6": "UserID", 97 | "8": "?", 98 | "9": "Difficulty", 99 | "10": "Downloads", 100 | "11": "?", 101 | "12": "?", 102 | "13": "GameVersion", 103 | "14": "Likes", 104 | "15": "Length", 105 | "17": "DemonFace", 106 | "18": "IsRated", 107 | "19": "IsFeatured", 108 | "25": "IsAuto", 109 | "27": "LevelPassword", 110 | "28": "UploadDate", 111 | "29": "UpdateDate", 112 | "30": "IsOriginal", 113 | "31": "?", 114 | "35": "SongID", 115 | "36": "ExtraLevelData", 116 | "37": "Coins", 117 | "38": "HasVerifiedCoins", 118 | "39": "RequestedStars", 119 | "40": "HasLowDetailMode", 120 | "41": "DailyID", 121 | "42": "IsEpic", 122 | "43": "IsDemon", 123 | "45": "Objects", 124 | "46": "?", 125 | "47": "?", 126 | "48": "?" 127 | }, 128 | "daily": { 129 | "0": "ID", 130 | "1": "TimeLeft" 131 | }, 132 | "misc": { 133 | "0": "Amount", 134 | "1": "Offset", 135 | "2": "AmountPerPage" 136 | }, 137 | "miscEncrypted": { 138 | "0": "HashData1", 139 | "1": "HashData2" 140 | } 141 | }, 142 | "localLevels": { 143 | "LLM_01": { 144 | "baseData": { 145 | "k1": "ID", 146 | "k2": "LevelName", 147 | "k3": "Description", 148 | "k4": "LevelString", 149 | "k5": "CreatorName", 150 | "k8": "OfficialSongID", 151 | "k11": "Downloads", 152 | "k14": "VerifyStatus", 153 | "k15": "UploadStatus", 154 | "k16": "Version", 155 | "k18": "Attempts", 156 | "k19": "NormalModePercentage", 157 | "k20": "PracticeModePercentage", 158 | "k21": "?", 159 | "k22": "Likes", 160 | "k23": "Length", 161 | "k26": "Stars", 162 | "k33": "?", 163 | "k34": "LevelMetaData", 164 | "k41": "Password", 165 | "k45": "CustomSongID", 166 | "k46": "Revision", 167 | "k48": "ObjectCount", 168 | "k50": "BinaryVersion", 169 | "k61": "FirstCoinAcquired", 170 | "k62": "SecondCoinAcquired", 171 | "k63": "ThirdCoinAcquired", 172 | "k66": "RequestedStars", 173 | "k67": "LevelProperties", 174 | "k79": "Unlisted", 175 | "k80": "SecondsSpent", 176 | "k81": "?", 177 | "k84": "Folder" 178 | }, 179 | "editorData": { 180 | "kI1": "EditorXPosition", 181 | "kI2": "EditorYPosition", 182 | "kI3": "Zoom", 183 | "kI4": "BuildTabPage", 184 | "kI5": "BuildTab", 185 | "kI6": "BuildTabPageDictionary", 186 | "kI7": "EditorLayer" 187 | } 188 | } 189 | } 190 | } -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "logging": { 4 | "errorLogging": true 5 | }, 6 | "analytics": { 7 | "allowChartInteraction": true, 8 | "showBrowserVisits": true 9 | }, 10 | "endpoints": { 11 | "enableGdEndpointsEndpoint": true, 12 | "enableSavesEndpoint": true, 13 | "enableDocumentationsEndpoint": true, 14 | "enableValueNamesEndpoint": true, 15 | "enableAnalyticsEndpoint": true 16 | }, 17 | "expressWebServer": true 18 | }, 19 | "values": { 20 | "webServer": { 21 | "port": 80 22 | }, 23 | "analytics": { 24 | "refreshRateMs": 10000, 25 | "chartRoundOn": 50, 26 | "maxErrorsPer100Visits": 10 27 | }, 28 | "endpoints": { 29 | "targetServer": "boomlings.com" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "checkJs": true, 6 | "resolveJsonModule": true, 7 | }, 8 | "exclude": [ 9 | "node_modules", 10 | "resources" 11 | ], 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gd-nodejs-api", 3 | "version": "0.3.1", 4 | "description": "A NodeJS Geometry Dash API meant to purpose as one of the easiest to extend, update and use APIs in the game", 5 | "scripts": { 6 | "start": "node start.js run", 7 | "test": "node start.js test" 8 | }, 9 | "author": "SMJS", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.19.0", 13 | "express": "^4.17.1", 14 | "express-useragent": "^1.0.13", 15 | "pako": "^1.0.11", 16 | "showdown": "^1.9.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /resources/docs/general/welcome.md: -------------------------------------------------------------------------------- 1 | # Welcome! 2 | These docs will be added in a later update -------------------------------------------------------------------------------- /resources/pages/analytics/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 15 | 16 | 17 | 18 |
19 |
20 | Memory 21 |
22 |
23 |
24 | Health 25 |
26 | Server health calculated by using a max amount of errors per 100 visitors. 27 |

28 | 29 |
30 |
31 | Extra 32 | 33 |
34 |
35 |

Visits per browser

36 | 37 |
38 |
39 | Response time in milliseconds 40 |
41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /resources/pages/docs/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | 16 |
17 |
18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /resources/pages/endpoints/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 14 | 15 |
16 |

Types

17 |

18 | The word after a /: indicates what value should go there and a ? indicates that the value is optional 19 |

20 |
21 | %body% 22 |
23 | 24 | -------------------------------------------------------------------------------- /resources/pages/levelSave/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %BODY% 7 | 8 | -------------------------------------------------------------------------------- /resources/pages/levelSave/dragBase.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

File required

10 |

To preform this action the API needs your CCLocalLevels.dat file. Things to note:

11 |
    12 |
  • The max file size allowed is 50Mb
  • 13 |
  • Files bigger than 4Mb can take a while to decrypt
  • 14 |
15 |
16 |
17 |
18 | 19 | Upload your CCLocalLevels.dat file here 20 |
21 | 22 |
23 |

24 | 25 | -------------------------------------------------------------------------------- /resources/static/css/analytics.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | color: #DCDDDE; 7 | background-color: #23232B; 8 | font-family: Gill Sans, sans-serif; 9 | } 10 | 11 | p { 12 | margin: 4px 0 4px 0; 13 | } 14 | 15 | b { 16 | font-size: 20px; 17 | } 18 | 19 | #body { 20 | width: 100%; 21 | height: 100%; 22 | top: 0; 23 | left: 0; 24 | text-align: center; 25 | position: absolute; 26 | } 27 | 28 | #browser { 29 | width: calc(100% - 10px); 30 | padding: 30px; 31 | text-align: left; 32 | } 33 | 34 | #browserOut .tr { 35 | cursor: pointer; 36 | } 37 | 38 | #browserOut .tr, #browserOut .ntr { 39 | display: inline-block; 40 | margin: 0 10px 0 10px; 41 | } 42 | 43 | #browserOut .browserVersions { 44 | display: none; 45 | } 46 | 47 | #chartContainer { 48 | width: 100%; 49 | height: 40vh; 50 | left: 0; 51 | bottom: 5px; 52 | padding: 10px; 53 | position: absolute; 54 | } 55 | 56 | #chartTitle { 57 | left: 20px; 58 | bottom: 40vh; 59 | position: absolute; 60 | } 61 | 62 | .info { 63 | width: 32%; 64 | padding: 20px; 65 | text-align: left; 66 | display: inline-block; 67 | vertical-align: top; 68 | } 69 | 70 | .info p, .info small { 71 | word-wrap: break-word; 72 | } 73 | 74 | .progress { 75 | width: 100%; 76 | height: 30px; 77 | background-color: #40404D; 78 | position: relative; 79 | border-radius: 5px; 80 | overflow: hidden; 81 | } 82 | 83 | .progress span { 84 | height: 30px; 85 | top: 0; 86 | left: 0; 87 | background-color: #418E51; 88 | position: absolute; 89 | border-radius: 5px; 90 | } 91 | 92 | .progress p { 93 | width: 100%; 94 | margin: 0; 95 | text-align: center; 96 | line-height: 30px; 97 | position: absolute; 98 | } 99 | 100 | @media only screen and (max-height: 350px) { 101 | #chartContainer { 102 | display: none; 103 | } 104 | #chartTitle { 105 | display: none; 106 | } 107 | } -------------------------------------------------------------------------------- /resources/static/css/docs.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: Gill Sans, sans-serif; 4 | } 5 | 6 | body { 7 | color: #2e3338; 8 | margin: 0; 9 | } 10 | 11 | .top { 12 | width: 100%; 13 | height: 60px; 14 | background-color: #4f5660; 15 | } 16 | 17 | .table { 18 | width: 100%; 19 | min-height: calc(100vh - 60px); 20 | margin: 0 auto 0 auto; 21 | display: table; 22 | } 23 | 24 | .row { 25 | display: table-row; 26 | } 27 | 28 | .nav { 29 | width: 175px; 30 | display: table-cell; 31 | vertical-align: top; 32 | min-height: calc(100% - 90px); 33 | } 34 | 35 | .line { 36 | width: 1px; 37 | background-color: #e3e5e8; 38 | display: table-cell; 39 | } 40 | 41 | .section { 42 | width: 100%; 43 | border-left: 1px solid transparent; 44 | transition: 0.5s; 45 | cursor: pointer; 46 | } 47 | 48 | .title { 49 | margin: 20px 5px 0 0; 50 | padding-left: 5px; 51 | } 52 | 53 | .topic { 54 | display: inline-block; 55 | } 56 | 57 | .title:hover, .topic:hover, .highlight { 58 | color: #4f5660; 59 | border-left: 1px solid #4f5660; 60 | background-color: #f2f3f5; 61 | } 62 | 63 | .title:hover, .title.highlight { 64 | color: #2e3338 65 | } 66 | 67 | .topic p { 68 | margin: 0; 69 | padding: 5px 10px 0 15px; 70 | } 71 | 72 | .leftgra, .rightgra { 73 | width: 10px; 74 | display: table-cell; 75 | background: transparent; 76 | } 77 | 78 | #body { 79 | width: calc(100% - 195px); 80 | padding: 20px; 81 | display: table-cell; 82 | } 83 | 84 | #body a { 85 | color: #0000EE; 86 | text-decoration: none; 87 | } 88 | 89 | #body code { 90 | color: #B65B01; 91 | } 92 | 93 | #body pre code { 94 | color: #2e3338; 95 | } 96 | 97 | #body center { 98 | margin-top: 80px; 99 | } 100 | 101 | @media screen and (min-width: 1200px) { 102 | .leftgra, .rightgra { 103 | width: calc((100vw - 1200px) / 2); 104 | } 105 | 106 | .leftgra { 107 | background: linear-gradient(to left, white, #CDCDCD); 108 | } 109 | 110 | .rightgra { 111 | background: linear-gradient(to right, white, #CDCDCD); 112 | } 113 | } -------------------------------------------------------------------------------- /resources/static/css/endpoints.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | background-color: #201C20; 8 | font-family: Gill Sans, sans-serif; 9 | color: #dcddde; 10 | } 11 | 12 | a { 13 | text-decoration: none; 14 | color: #dcddde; 15 | } 16 | 17 | span { 18 | background-color: #201C20; 19 | padding: 3px; 20 | border-radius: 5px; 21 | box-sizing: inherit; 22 | } 23 | 24 | .geometricbg { 25 | width: 100%; 26 | height: 150px; 27 | background: url("https://smjs.eu/gd/findings/incl/bg.jpg"); 28 | background-size: cover; 29 | background-repeat: no-repeat; 30 | background-position: center; 31 | filter: brightness(40%); 32 | } 33 | 34 | .nav { 35 | position: fixed; 36 | top: 0px; 37 | width: 100%; 38 | height: 150px; 39 | background: linear-gradient(to bottom, rgba(210, 137, 95, 0.5), rgba(210, 137, 95, 0)); 40 | transition: 0.5s; 41 | z-index: 10; 42 | } 43 | 44 | .nav .logo { 45 | display: inline-block; 46 | vertical-align: middle; 47 | width: 65px; 48 | height: 65px; 49 | margin-top: 10px; 50 | margin-left: 15px; 51 | border-radius: 50%; 52 | background: url("https://smjs.eu/gd/findings/incl/SMJS.png"); 53 | background-size: cover; 54 | background-repeat: no-repeat; 55 | background-position: center; 56 | transition: 0.5s; 57 | } 58 | 59 | .nav a, .nav p { 60 | color: #dcddde; 61 | font-weight: bold; 62 | margin-left: 15px; 63 | display: inline-block; 64 | vertical-align: middle; 65 | user-select: none; 66 | cursor: pointer; 67 | text-decoration: none; 68 | } 69 | 70 | .navalt { 71 | background: rgb(165, 100, 90); 72 | height: 60px; 73 | } 74 | 75 | .nav .logoalt { 76 | height: 40px; 77 | width: 40px; 78 | } 79 | 80 | .content { 81 | width: calc(100% - 100px); 82 | margin-left: 50px; 83 | margin-top: 30px; 84 | height: auto; 85 | padding: 20px; 86 | background-color: #403B3E; 87 | border-radius: 3px; 88 | margin-bottom: 50px; 89 | } 90 | 91 | .content h2 { 92 | margin-left: 5px; 93 | margin-bottom: 0px; 94 | } 95 | 96 | .content .block p { 97 | margin-top: 5px; 98 | margin-left: 20px; 99 | } 100 | 101 | .block { 102 | background-color: #201C20; 103 | margin-top: 10px; 104 | margin-bottom: 10px; 105 | padding: 10px; 106 | } -------------------------------------------------------------------------------- /resources/static/css/levels.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 15px; 7 | background-color: #202225; 8 | color: #dcddde; 9 | font-family: Gill Sans, sans-serif; 10 | } 11 | 12 | .sideLevelSelect { 13 | position: relative; 14 | width: 380px; 15 | height: 110px; 16 | background-color: #36393f; 17 | padding: 10px; 18 | border-radius: 5px; 19 | margin-bottom: 15px; 20 | 21 | } 22 | 23 | .sideLevelSelect h3 { 24 | margin: 0; 25 | } 26 | 27 | .sideLevelSelect .view { 28 | padding: 10px; 29 | padding-left: 40px; 30 | padding-right: 40px; 31 | text-align: center; 32 | position: absolute; 33 | top: 10px; 34 | border: 2px solid #dcddde; 35 | color: #dcddde; 36 | border-radius: 5px; 37 | background-color: #202225; 38 | } 39 | 40 | .sideLevelSelect .data { 41 | position: absolute; 42 | bottom: 50px; 43 | } 44 | 45 | .sideLevelSelect .data2 { 46 | position: absolute; 47 | bottom: 25px; 48 | } 49 | 50 | .sideLevelSelect .right { 51 | right: 10px; 52 | } 53 | 54 | .sideLevelSelect .progress { 55 | position: absolute; 56 | bottom: 0; 57 | left: 0; 58 | width: 100%; 59 | border-bottom-left-radius: 5px; 60 | border-bottom-right-radius: 5px; 61 | height: 10px; 62 | background-color: #4f545c; 63 | overflow: hidden; 64 | } 65 | 66 | .sideLevelSelect .progress .wrapper { 67 | position: relative; 68 | width: 100%; 69 | height: 10px; 70 | } 71 | 72 | .sideLevelSelect .progress .wrapper .inner { 73 | position: absolute; 74 | top: 0; 75 | left: -5px; 76 | background-color: #418E51; 77 | height: 10px; 78 | margin-left: 0; 79 | margin-right: auto; 80 | border-radius: 5px; 81 | margin: 0; 82 | } 83 | 84 | .sideLevelSelect .progress .wrapper .practice { 85 | position: absolute; 86 | top: 0; 87 | background-color: #4DA4C4; 88 | height: 10px; 89 | margin-left: 0; 90 | margin-right: auto; 91 | border-radius: 5px; 92 | margin: 0; 93 | } -------------------------------------------------------------------------------- /resources/static/css/levelsDrag.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | color: #dcddde; 7 | background-color: #202225; 8 | font-family: Gill Sans, sans-serif; 9 | margin: 15px; 10 | } 11 | 12 | #mainHead { 13 | width: 800px; 14 | margin-left: auto; 15 | margin-right: auto; 16 | } 17 | 18 | #dropbox { 19 | width: 800px; 20 | height: 375px; 21 | max-width: 1000px; 22 | max-height: 500px; 23 | background-color: #2f3136; 24 | padding: 15px; 25 | margin-top: 50px; 26 | margin-left: auto; 27 | margin-right: auto; 28 | margin-bottom: 20px; 29 | border-radius: 5px; 30 | border: 2px solid #dcddde; 31 | position: relative; 32 | cursor: pointer; 33 | user-select: none; 34 | transition: 0.5s; 35 | } 36 | 37 | #dropbox .innerline { 38 | width: 100%; 39 | height: 100%; 40 | text-align: center; 41 | border-radius: 5px; 42 | border: 2px dashed #dcddde; 43 | position: relative; 44 | } 45 | 46 | #dropbox .innerline .item { 47 | left: 0; 48 | right: 0; 49 | position: absolute; 50 | transition: 0.5s; 51 | } 52 | 53 | #dropbox .innerline .picture { 54 | width: 130px; 55 | height: 130px; 56 | margin-left: auto; 57 | margin-right: auto; 58 | top: 80px; 59 | } 60 | 61 | #dropbox .innerline .text { 62 | top: 240px; 63 | } 64 | 65 | #dropbox #file_upload { 66 | width: 100%; 67 | height: 100%; 68 | top: 0; 69 | left: 0; 70 | right: 0; 71 | bottom: 0; 72 | position: absolute; 73 | opacity: 0; 74 | } 75 | 76 | #error { 77 | margin-left: 10%; 78 | } 79 | 80 | #tempform { 81 | opacity: 0; 82 | } -------------------------------------------------------------------------------- /resources/static/images/upload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SMJSGaming/GD-NodeJS-API/678616bd5952726407da1e34b78620ae0545733d/resources/static/images/upload.png -------------------------------------------------------------------------------- /resources/static/scripts/analytics.js: -------------------------------------------------------------------------------- 1 | const xmlHttp = new XMLHttpRequest(); 2 | const ctx = document.getElementById("chart").getContext("2d"); 3 | const options = { 4 | type: "line", 5 | data: { 6 | labels: [ 7 | "60 Minutes ago", 8 | "54 Minutes ago", 9 | "48 Minutes ago", 10 | "42 Minutes ago", 11 | "36 Minutes ago", 12 | "30 Minutes ago", 13 | "24 Minutes ago", 14 | "18 Minutes ago", 15 | "12 Minutes ago", 16 | "6 Minutes ago", 17 | "Now" 18 | ], 19 | datasets: [] 20 | }, 21 | options: { 22 | responsive: true, 23 | maintainAspectRatio: false, 24 | animation: false, 25 | scales: { 26 | yAxes: [{ 27 | ticks: { 28 | max: 1, 29 | beginAtZero: true 30 | } 31 | }] 32 | }, 33 | legend: legendOptions 34 | } 35 | } 36 | const chart = new Chart(ctx, options); 37 | let inVersion = "none"; 38 | 39 | function getMax(data) { 40 | let max = 0; 41 | for (let i in data) { 42 | if (Math.ceil(Math.max.apply(Math, data[i].data) / timeTestRoundOn) * timeTestRoundOn > max) 43 | max = Math.ceil(Math.max.apply(Math, data[i].data) / timeTestRoundOn) * timeTestRoundOn; 44 | } 45 | return max; 46 | } 47 | 48 | function version(id) { 49 | document.getElementById("browserTitle").innerHTML = "Visits per version from " + id; 50 | document.getElementById("browserTitle").style.cursor = "pointer"; 51 | Array.from(document.getElementsByClassName("tr")).forEach((list) => list.style.display = "none"); 52 | document.getElementById(id).style.display = "inline-block"; 53 | inVersion = id; 54 | } 55 | 56 | function browser() { 57 | document.getElementById("browserTitle").innerHTML = "Visits per browser"; 58 | document.getElementById("browserTitle").style.cursor = "inherit"; 59 | document.getElementById(inVersion).style.display = "none"; 60 | Array.from(document.getElementsByClassName("tr")).forEach((list) => list.style.display = "inline-block"); 61 | inVersion = "none"; 62 | } 63 | 64 | function chartgen(data) { 65 | chart.ctx = document.getElementById("chart").getContext("2d"); 66 | chart.data.datasets = data; 67 | chart.options.scales.yAxes[0].ticks.max = getMax(data); 68 | chart.update(); 69 | } 70 | 71 | function request() { 72 | let data; 73 | xmlHttp.open("GET", "/analytics/data"); 74 | xmlHttp.onload = () => { 75 | data = JSON.parse(xmlHttp.response); 76 | document.getElementById("memoryOut").innerHTML = data.memoryText + data.memoryProgress; 77 | document.getElementById("healthOut").innerHTML = data.errorProgress; 78 | document.getElementById("extraOut").innerHTML = data.extra; 79 | if (data.browser == "denied") { 80 | document.getElementById("browser").remove(); 81 | } else { 82 | document.getElementById("browserOut").innerHTML = data.browser; 83 | } 84 | if (inVersion != "none") 85 | version(inVersion); 86 | chartgen(data.chartData); 87 | }; 88 | xmlHttp.send(null); 89 | } 90 | 91 | setInterval(() => request(), interval); 92 | request(); -------------------------------------------------------------------------------- /resources/static/scripts/docs.js: -------------------------------------------------------------------------------- 1 | const xmlHttp = new XMLHttpRequest(); 2 | function inithighlight() { 3 | document.querySelectorAll("pre code").forEach((block) => { 4 | hljs.highlightBlock(block); 5 | }); 6 | } 7 | 8 | function openPage(url, thisObject) { 9 | xmlHttp.open("GET", `/api/docs/res/${url}`); 10 | xmlHttp.onload = () => { 11 | if (thisObject) { 12 | document.querySelectorAll(".highlight")[0].classList.remove("highlight"); 13 | thisObject.classList.add("highlight"); 14 | } 15 | document.getElementById("body").innerHTML = xmlHttp.response; 16 | inithighlight(); 17 | }; 18 | xmlHttp.send(null); 19 | } -------------------------------------------------------------------------------- /resources/static/scripts/endpoint.js: -------------------------------------------------------------------------------- 1 | function navscroll() { 2 | let height = $(document).scrollTop(); 3 | if (height != 0) { 4 | $(".nav").addClass("navalt"); 5 | $(".logo").addClass("logoalt"); 6 | } else { 7 | $(".nav").removeClass("navalt"); 8 | $(".logo").removeClass("logoalt"); 9 | } 10 | } -------------------------------------------------------------------------------- /resources/static/scripts/handleData.js: -------------------------------------------------------------------------------- 1 | const reader = new FileReader(); 2 | 3 | function errorCSS(message) { 4 | document.getElementById("error").innerHTML = `⚠ ${message}`; 5 | document.getElementById("dropbox").style.boxShadow = "0 0 20px rgba(253, 43, 43, 0.7)"; 6 | setTimeout(() => { 7 | document.getElementById("dropbox").style.boxShadow = "0 0 0 rgba(253, 43, 43, 0.7)"; 8 | }, 1000); 9 | } 10 | 11 | function fileDropped(event, input = false) { 12 | let file; 13 | let saveFile; 14 | let redirectForm; 15 | event.preventDefault(); 16 | if (input) { 17 | file = event.target.files[0]; 18 | } else { 19 | file = event.dataTransfer.files[0]; 20 | } 21 | if (file) { 22 | if (file.name == "CCLocalLevels.dat") { 23 | reader.readAsText(file, "UTF-8"); 24 | reader.onload = (evt) => { 25 | saveFile = btoa(evt.target.result).replace(/\+/g, "-").replace(/\//g, "_"); 26 | redirectForm = $(`
`); 27 | $("body").append(redirectForm); 28 | redirectForm.submit(); 29 | } 30 | reader.onerror = (evt) => { 31 | errorCSS("Error reading the file!"); 32 | } 33 | } else { 34 | errorCSS("Only CCLocalLevels.dat is allowed!"); 35 | } 36 | } else { 37 | errorCSS("No files were detected!"); 38 | } 39 | } 40 | 41 | document.getElementById("file_upload").value = ""; 42 | document.getElementById("file_upload").onchange = (event) => { 43 | fileDropped(event, true); 44 | }; -------------------------------------------------------------------------------- /resources/static/synthighlights/styles/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Original highlight.js style (c) Ivan Sagalaev 4 | 5 | */ 6 | 7 | .hljs { 8 | display: block; 9 | overflow-x: auto; 10 | padding: 0.5em; 11 | background: #F0F0F0; 12 | } 13 | 14 | 15 | /* Base color: saturation 0; */ 16 | 17 | .hljs, 18 | .hljs-subst { 19 | color: #444; 20 | } 21 | 22 | .hljs-comment { 23 | color: #888888; 24 | } 25 | 26 | .hljs-keyword, 27 | .hljs-attribute, 28 | .hljs-selector-tag, 29 | .hljs-meta-keyword, 30 | .hljs-doctag, 31 | .hljs-name { 32 | font-weight: bold; 33 | } 34 | 35 | 36 | /* User color: hue: 0 */ 37 | 38 | .hljs-type, 39 | .hljs-string, 40 | .hljs-number, 41 | .hljs-selector-id, 42 | .hljs-selector-class, 43 | .hljs-quote, 44 | .hljs-template-tag, 45 | .hljs-deletion { 46 | color: #880000; 47 | } 48 | 49 | .hljs-title, 50 | .hljs-section { 51 | color: #880000; 52 | font-weight: bold; 53 | } 54 | 55 | .hljs-regexp, 56 | .hljs-symbol, 57 | .hljs-variable, 58 | .hljs-template-variable, 59 | .hljs-link, 60 | .hljs-selector-attr, 61 | .hljs-selector-pseudo { 62 | color: #BC6060; 63 | } 64 | 65 | 66 | /* Language color: hue: 90; */ 67 | 68 | .hljs-literal { 69 | color: #78A960; 70 | } 71 | 72 | .hljs-built_in, 73 | .hljs-bullet, 74 | .hljs-code, 75 | .hljs-addition { 76 | color: #397300; 77 | } 78 | 79 | 80 | /* Meta color: hue: 200 */ 81 | 82 | .hljs-meta { 83 | color: #1f7199; 84 | } 85 | 86 | .hljs-meta-string { 87 | color: #4d99bf; 88 | } 89 | 90 | 91 | /* Misc effects */ 92 | 93 | .hljs-emphasis { 94 | font-style: italic; 95 | } 96 | 97 | .hljs-strong { 98 | font-weight: bold; 99 | } 100 | -------------------------------------------------------------------------------- /start.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @private 3 | * @type {Object} 4 | * @name fs 5 | */ 6 | const fs = require("fs"); 7 | 8 | /** 9 | * @private 10 | * @type {Object} 11 | * @name configData 12 | */ 13 | const configData = require("./config.json"); 14 | 15 | /** 16 | * @private 17 | * @type {Object} 18 | * @name StartupLogs 19 | */ 20 | const StartupLogs = new (require("./api/loggers/StartupLogs")); 21 | 22 | /** 23 | * @private 24 | * @type {Object} 25 | * @name WebAppStart 26 | */ 27 | const WebAppStart = require("./WebAppStart"); 28 | 29 | /** 30 | * @public 31 | * @type {Object} 32 | * @name globalData 33 | */ 34 | const globalData = { 35 | startDate: Date.now(), 36 | visits: 0, 37 | browser: {}, 38 | errorCase: 1, 39 | errorFunc: new (require("./api/loggers/ErrorLogs"))().init, 40 | config: configData, 41 | graphEntries: { 42 | endpoints: { 43 | baseClass: "gdEndpoint/GdEndpointDataService", 44 | settingsName: "GdEndpoints", 45 | async: true, 46 | testData: [ 47 | ["daily"], 48 | { 49 | errorCase: 0, 50 | errorFunc: new (require("./api/loggers/ErrorLogs"))().init, 51 | config: configData 52 | } 53 | ], 54 | data: Array(10).fill(0), 55 | color: "#A51D38" 56 | }, 57 | saves: { 58 | baseClass: "save/SelectiveLevelSaveService", 59 | settingsName: "Saves", 60 | async: false, 61 | testData: [ 62 | { 63 | errorCase: 0, 64 | errorFunc: new (require("./api/loggers/ErrorLogs"))().init, 65 | config: configData 66 | }, 67 | "", 68 | 0 69 | ], 70 | data: Array(10).fill(0), 71 | color: "#1D3FA5" 72 | }, 73 | docs: { 74 | baseClass: "documentation/DocumentationIndexService", 75 | settingsName: "Documentations", 76 | async: true, 77 | testData: [[]], 78 | data: Array(10).fill(0), 79 | color: "#27A51D" 80 | }, 81 | valueNames: { 82 | baseClass: "valueName/ValueNameDataService", 83 | settingsName: "ValueNames", 84 | async: false, 85 | testData: [""], 86 | data: Array(10).fill(0), 87 | color: "#951DA5" 88 | } 89 | } 90 | } 91 | 92 | /** 93 | * @private 94 | * @type {Object} 95 | * @name OpenLoop 96 | */ 97 | const OpenLoop = new (require("./api/background/OpenLoop"))(globalData); 98 | 99 | /** 100 | * @public 101 | * @type {String[]} 102 | * @name args 103 | */ 104 | const args = process.argv 105 | 106 | /** 107 | * @public 108 | * @since 0.3.0 109 | * @version 0.1.0 110 | * @function init 111 | * @summary The main runner function 112 | * @description The run function which will start the API and all its dependencies 113 | */ 114 | function run() { 115 | // Setting test data 116 | globalData.graphEntries.saves.testData[1] = 117 | Buffer.from(fs.readFileSync("testData/CCLocalLevels.dat", "utf8")).toString("base64"); 118 | // Thx for the light save Alten ;) 119 | 120 | // Get all the startup logs 121 | StartupLogs.init(globalData); 122 | 123 | // Starting loop 124 | OpenLoop.init(globalData); 125 | 126 | // Giving the launch data and start listening 127 | if (configData.settings.expressWebServer) { 128 | (new WebAppStart).init(globalData); 129 | } 130 | } 131 | 132 | /** 133 | * @public 134 | * @since 0.3.0 135 | * @version 0.1.0 136 | * @function test 137 | * @summary !in development! 138 | * @description !in development! 139 | */ 140 | function test() { 141 | 142 | } 143 | 144 | if (!args[2] || args[2] == "run") { 145 | run(); 146 | } else if (args[2] == "test") { 147 | test(); 148 | } -------------------------------------------------------------------------------- /testData/CCLocalLevels.dat: -------------------------------------------------------------------------------- 1 | C?xBJJJJJJJJH&r39S@yryB9&bdy2}]`i}dfqyyO|ZHYJ3XHGlc=HZ=;jfcbm[|I2~a\|EXQ?q<&iFa~nmfT2:q=m8TT[m3Cqo>sM{TT3~Tb[GHx&H[8<}?~>=n\2es8{ye2mTySm<&;MOMmL}HZYBMzYTEi?3?TlhH>3Nh82zd8;RA^o&M}SqgyYxISioY?C^iTMCirm9Xmb}R8Q@}a3\ToR~dFh]Tn;TIdobLL&@;YLd9gClAH_8MFD|}MLYohdyhlRB}CgnMz=cgy]22C]n?qrn9M_D=iB]N_x>{h;c|j]DC>o{c\s:}`@JqhOg:Iz|2a}A3R3FdO=F}boXZ=|XSe&~E3Y[{T}Bx[db>9mGRJIGm:;>mm~=QO8n:cHysC`J=zLG@hrBqQD^Sl`Q{{TSglExdOAhg`nq|}[FsY<9|Iyfj3EySAhR&rgdO&y^N&E}E~_dsRBflTeB_iH`&`rxfTBJbiOQI;RJ^M8F`xs:F&^Oc@9CD&yQ2q9[_GqR=B`mDL:S;?lrCl{[hcJHqcI@ld;RmIHjh_S2@iHqJQ[OIzZDy>8J`b3H8XNImD]8B||Sjr]}jY?Cs>Nx&`HHChsF\MryhZiCf=a}gFR[@R&R8gNA^>@aHllcDCZC^29>;ZR`aHj}=28^LBF[&DqG3mob]XOhefllgb~Ti]G`:^}hA3dJ&gYY?b?8?@Y8|`Ff@TR=LO>;x2=m[;ac{H\i^yx;i||JmyJ]MIYsNQJ]DO_i@qR]3^oD?N&A~@]H&>HXRg@Gs;`SNDcbdRAJCZ~mBRIYZrs\`r|TcyJ@ROY?2c[IrxF<]ZaI}nhBlnM8[{xqFmgZQ?HXZR^Ab?Zzmfr>oqIramBa:|O~fgRxoZ?J`|bJIE>l3RfDms]aZqzyZ@:eEQ{_nIIh{o8H_Y|Lj~s`{|yMyj{9_DjjyMs}o&[9;Dq}YgHrXSbhs?;@;gQ:hZ`adXg3_m^QON3[@2hzcRToFxEYAfj:hs3~Y[@fBac~&y?hSeql_YHXA&^lhalY:QINX9Tx?AD^iy9B3^8Obb98=JD\<3:=CMh{g;\F|:Oa^yi[~|XO^r^>r9{fY8[|NL:eXbMny^|q]DMxZXJGD@^ZhgjhMTeBdZZ^f8_[g@T=zayiN??l}l2Oxs;=r3|eG|a^?xcrTe&]Enc&[|J&J[H&m?LO>9C&fEqoaRMxXny_[o{a9;agXg]|3yZzzNeal9B8RYsH;{cELXBR;&=qM[{[X8zhd[a?nsXh2xYOcEO:8R>S3N`\&Qi;INsRc|3YOs`JMIBMGJzI3z:Hhz^G8amL}dA=q3d@nlA>8OSb2onXLSDMy^@mb@gcNC9\TGSa3^:iajr3Sf@CRiToL\fqcf\zLL:Gz>m`dYSDsAcSL9Ly`zG\D{Q~8CF|q^ThnLJjB\b`;m]EQd|I{hqi\QNGQnz=Gl9qGl3JxCICOSxgnIx9JY9\nrS{8o_~8J]}coI=e9j|zMJl8Ycy|>eEDb2RgYLN~ZBg3_zDJfrZAQfq>GBD@rjRN^M|lQ3=@;ENxsDxbx]blS^]IF?jCy\zy^~8bQd:mdy`hz>BRg&~mh^rSJIeL`jY@9|>gGaNqMebTrZ]zjS`\Zlci9AfSsx2xR:ZdBD]?sSB^hf<]{>~I?zZnNFfM8Gih|jYQRFQGrNlIra<9l?EGlI@QmF[d8QocNnDC&O=]Bq>AcRF^{2Z_:DQiA~]E[nmjcg3h@;hzO8d=MS{XcOX`CTJaCx_as@O]Z^^Hfz|}Gh~o`e{g:A;gI;}M~bD\9>8b>{ZEg@22D@gHX[sF{rDN=aQ@DB]JA9>lybD^:|RS:MQs{2_Rz?|CirF^8mJEsQ?FRiSn\e~\XomBsdghg`8\xL{Iz<\r23Qa}|9FAJEYZ^@`y}hZM:oFy@i>;=eE2zBjGYYCo\FmR8`ARL?id|LBG~e|:Dh2gc^O\|EyJ9^Zoq|;Yr=SgyfobgBi3?zDr@HaOh:rEaDrLcScDHc29&qr_EgX^>2RYXTBD@<]g_z{JR``;mlAjMozaHQ|ljagQz~doRN`M8HM?j8GAb\xCO<92fq{XeLIsNCR=GNoRsM:GEBxD&F3if?~8~zbz3FcCHI@`yS\lIiZ[gCQX?ZsL@n~9y[S>C^~bN>c_n~~ZN~R9^OR|Ql{~l2~ZJh2DL\Q^@ebbBEXqA3\fD9X`xy]SyeNqexJhX3qG^_DEC^Xr\DQcA{RE@qCIilR?fRhif:;Db|X2yGgA`8gsLLIO&\dC&A9o>LAdbxXAAd~A~o`de=\2~n2J}]fYZ2y^;}i`OH;9^`}Sy{NqgO\^]JzSHHRnIJ[r[ar_9AXb`cMT_l<:zHsSj@?3}DXh9e]i|E{irjJDehyF@:D|EsBcNjl~gGLL;}Yi>b?3eM:Ba`?z\R~=[Mged{8{Fc=LX\}l`|Fa;?Qb{3`xme=[:H__YRSi]Q`?~bf_GMnf?8^Ae?LO@I2?ex9OGgi`hhFcRe@bEgXQe|=MXS_\zoZT8;;rzm?`coNSg&`b&e{Y`aeHcArmQRLfjZ_N_:_|Is3dSB{h3hncJo`dXEX3|qFd3D{sYE_~J;Me;y^nmdoAA@Sa2SFqj~DL?XMN~BZ3yMG@HxGh9LE{hh{g=9d<3cayhzBM=lMS:JdZnL_{GloX~9O_i_:xa\nI~Gnz2IMCc9`]igBF|ZEDBhCBb=^>ad8^}JLobr_?`o\]]dQL]sFAo9x|{HdZS[nrsj~g9^no=ggZ9i2|dTBIs_hEDZ`cbjMy|H\^}yfEd~`CDFaqS}&Y=>DJeig`J]aCCxn:9MAIC3Fii?&9Djqj:~z}@?qd:QhQSgfHhE=NQ?Jr;iJ3NdQ^q`@&8E~BLIiioiHZ`Ee_Nq`;8GO@>ldSxyobBfFGn?DMoBMMh?eiHDaoE{}GfSJs:yJ:XQ_@Fic``Qr\y2C_T@Y\nH@sc>Dq:\YCjxc>AAaEIg~gIoI9zDf~;rrB}\aHJf3r]QOj<{m9X];Cs>zZo@_ZbeS@ecM>Fr>>y`QfiF:Y~lYy{D:?d&`~ZMjI~|j8`RF;ydTX]?g`OqdDQCR\gCdI]]QayL=r:e=F]ZdgcGo9mgBbMGI_&@zdb[DSnxznF&[SLZEqeYnABgYZQCys9GbR\fqg=xd}QZ{9YX^;FEELYMo{~:HRNI|ezG[I[rR9g[j?3>yzAslYrH;c=?9Sn3A\JRCF_fr8EY{n\~2OjZfGAh}3R9H2iSL^r\L3i;bcfis^>qLQNG>lZxmCJ>Qy~\FN\<8hQog>8AGZ:2gIexJ?ab^\NYmGAi^l3:[e=oF9ZF^Q]Zs8RHXFglXNJOjMHE=cBydLFc;jY9:BYEZxhTaHodhGF|Ag]iBL?]9MlbSi<`8\NbgZsaSLSAz;8Jq>eJj>xjRbQD}^B|~&S8Se?rqcCnjE&XCr[^SR&qIg&^bx89qSX2S\ong]]NL=^|T~&iNFgD<|om|;`Fshz[zLY:|go~gqL>j;SZ]~GY@3Z;JG>rghQB[AA`L{DcOM2E9FSMOjEnzsyzfSySg`~E39m@s8NqReBqHdsgz`~Y=~]XHAOhde?E{fj\B2ro<8iH~XRi{C:&jaMc`ir^9iynaSyi9|D~\J>[i=q:{_crNgSBonGlBSH=aHeh3;@3?anbdCaJ]i]DxY\2_<\[2>xN_irlc`O=;C{i]MSEN?^Icfhj|LfrQB9\AHj`O38fNyZyMa=hOjT^{zgqA]~{qbJqojgSFxezRxfn^E`8g|9@ydnQ_>_3H_{ZZqQM[\xo\XJOGH]c3g^gRyjCJTOaalAyif|Ld2^sbo??9qQNJ\OQc8nMbY:`{gQMNs:8lH_m=}G:^NyGm9e=^F98Zac`jbcf>aXDZlR8{fnC]sBnQ2Bge]B2`9fZyxrH~:Oqe>^CQr}AD&eIE;yExF>Rz_D9ohG&NF3{c{[:LsbMx?D:=|T^N`jdZodM>~N3q`N\&8=E]^]XSamJmaLb_Hyd}JJiBlbZEXT~9:]=:GBcZ&~h:fqRQ2J\AX9{9ci3meO9caD@3CS?xHxE2;Z_@LsdzB>li~B93yQI:\Th`9ra{gjYfsIylqnXmAl:_zHL`\H9blIIgLhJE}Y<[YLeAR=9A>H3&gA&abq[g[;=j`x?QgjoCx?`QJiY&ZXhdRMnsB^_d9_cf|E?LiAg=n|<:Q9XQyDJ;]\&f]ArA^}L:c[|]_gR^O]CxqyRiOFd:adZGn[cNjBj:\~fhAoeR:LS[LimNI\?HSbG~Q?FE~]Erz=8O~n@aEyA\XOoR~ObRY29of?lcAlfX;}[dnnODORSOoiT:dfdE2xY9hCiY{FeNid<~Q|HCCNSQZF|sZ>:bf^GaxhoHX;QD}ejQ8nGFeSRdD9mx=\?E9hG`Cr{?N^ZG9@3QJfOlr8NY2l|3RcEXqETYObm\J;`A?O|iDGGAoX}=9]isRCYN8Z=^|?EEL9T^fMaDAgIgmQgZ9;GCD@_ZOJdB@fo{Xf@N\A]@a[^8XaNY`Y}JM;]eXQJ;AlziZ9Ossga>F^&Ix^>MbFbamZ|~lanomlleqA2]Ei:>]Sifc\`qh~fm@Y`[Iz{yMD?=NjSc;8ZFCgEY:{AALYi]z~Q]oIasm;A;BOhaxBCs{n:JFBEjI2eFRrxf_XeBzCm^qT>BE?8_hBO8}a9jo2{ey_|gF&Tdg[L>NJ|IO=S_fALm@3BLJ[LszxAGign|&g]L}MLjz|c~moHQ~JfF:eYaj|>}iI~:>]h{3DMdb^8{eGXq2y^oxXg[{=yhBQmfIBgJZ~oQrq~IYOCsC[`d=CllSz\sTAlsl[RGbRqJdloy][:^]=n8QE~:rgqsH3\Bbdbqh^Qqjm9HDHXRIi_fBYahlarieJj:dahGOi\:Dz@>|[^D;sOx^lm^AJR2H9E;J}}q:gGoYfh_crNndBx`H@H{aN8Q^9}z3f\xd`GjEqX2nX;L{oa}cQ\DrEM|QCB~YF;MfZ~rF@|{RG|A}<|Ry;G_9hCDXn`mJESr|=_L\Egi]cmCb>s}fbShFBQS3oqb}NiHoIqdghFQ&q:8h{]?miA;>=rmMAHsgm[QL9S{`cDfHc`MFfYH=oaRbOE;AqFG>{FbH]nA3zOG]|3fL|CC^{e>S]JNYc:`?BRbG{z_rRCSyYNIo&bY{CA?LndL~;sRYdcL]xydIREMdS^AE>:dQe2@^o`{=D=2JhMQB&X:`2:}oIgm||YddCL8SogJ9Oyb@gxFM{x2<_|2R{x_aCYEA?>M[Q<qQzN|ogmRRCGMA|AhIy>|B;Ge?[Fa{>\\m]Qyg_~iYjl=X=gFlDq~Tn&sJ^`>bM`>2?z=orAqF_}R8JaoN}3\I&zERFXHZ2XoDT^Q_jenez9qG]`SL=S\_X{Eg{G`Ao=hn8N\BozmzDJ|Sz9zyRbf[;jX~NY[r;qGnHcaOErb]zH~I]zZgm={iJDM~oS=R?D]Cc9i~l8^@CYL?o:fH;gAexEAD_qRXGaM|o{cXDELz_`n9r=Ojbm=lEJh?}>=j3MFH=:R2cGgA9_^@9aaJCDxOHDOGiy2~dSdRge_J3@nqr\<`qi>n:jbSe|BjsnzZQD9=^>8BHz_X;dOj=L:[\n_cc^e~iSYg_eCHBJ9J@Da}McaiAsgca9^Jhdxf3q[CDsArLEf\cl^iF@d{E8MNMBz2J]De^:=becNMIYe[DXodnYjmno=Hdh?qjeaQoG_D]G>9BQRqLFRArlF?j[^=r>A~YjMXcL2ezR\QTxMCB\dnGX\[{o[c;ax=Zr93c\G~>nb=;x?cy:[GB3h{gE}Zix`F98&=8he`bjMgOI^\Qq\nfDseY>@MQ@cqyhd&@\n;Oz?BB;;;z2xm`INHSH2n`;C{X_?~2=ARS:@hT>CexmMN}@OIgygCi38L{Cy8L:}e?=^f:lrbon3x`bhZFRfG;rNZNzX@LNy{N][h8R@JOLO`hZYen8nrm~MQ&AyQhygGNoCG>LD`nbZRYeX9<:Y\]NAfojR<;q=ebH~~mDF8jsnAd~jJ^QZ8>q8_MH^>]XQQ=QR\8\GDC`rMLLgM;&DR;Bbj;Nfq[{{hZC=czjQTN}]DR3z?LF@=HOlJ]OHXlBg|`FRFMMRxI?jsLf;RDAE~AZd~?9mOC\`heLxIj~2z=gi:X2g3QJ_3:M?@_LBz}EjA_mgFLS[d:x[Da3@L|2^2f\TIFSX29AdShbq8l@lR>_?flZ@n^?Q:Z^9C_ob9gEM3\A9QMJ^qhx<:Rel{^^A`2aqm9~INQH~xC^eOFaC&j9R`}:n[sQlGMCm{}GEgT_^^>MI~rXdcoO\xbOAg]aijFXC{8\]~2ioo~Onig\QSJ}]\`MO9Ir{dIxcF93rOJHRqM|f_<]_T_ni:z|f>MO^\h>fZeMfqlxNS@I}fzcFai2YdY{zYRqJbE|R>;h9{iEi]SXGLji]E:=R|cHBAo=QAlAIrj>z&YABX93gR_Rny>Bj&b?DGY@e?\eIyAAs_i;^|fIzRrBg2yIdhH:mY[ooqq}{nSHR|Tl_FXdXo;:BaDRY?QgEl`\i?ZY[llf3RjY|Xyon\;:ol]z:[CXnh~lyFz?Q``bjO]qY}bol~?[_]Xl&3]aS{3;`bF~MJ?_r?]TGn=r[l^~>?yAsSXm{?&Q3@cb;~{cFQYA8Ln@MzHQ^e{M>aNxZ}qg9hrNC>bcHBcLjAHsD3_bxl[F^cqDJDY>HaZmERN~sl|QL^cq{MFDZiahZxjoydY:?SdBs\_N~n99xa{SfZOzfDjXsRXXL8H]O;N?DAjO&oB`Fcqy~:MrjB|cy=rEnyomEy}~LNM`A[`SOCn{a&:G^:Ac\n|zN[|?:J^Q_29|L|>yXqiD9ZMlh}=|3FMNM3~|BLZm[[Bn@A&ZJFb3zIN:{M^[JqCj\C;bY\D>&[O@eOxyaM^n\r8r]y\G@Ys^^}rRBc2bo~GI:jFMrxcoaIis`?B<]j@I:&d=jQXEYX}fX2ls;;Q|goj8HBq]hi~ZYrEbc8Yl@N[M9Rm;rgDH];E|AAl:y_E_]jR~lcxsRLa_{HNz&QBq:F8LxJJJR;nC89oZ}_^a_T\CM9hZEyZX|DerT;d>>G[DGS\R3e9RCDynne=J~J\A\oZ@`~dy`MG:g@gL>fDz=DZB\o9}Y&NHjxhORQ]Sz=\_Xb_lDbZBd={j`ac&x[J`J~Fi}o[`:8JMN\A|OcrM{\Cd=]j99cLf9zbBR9cG=M9jy]f~jd;=B|eN3{h@O[\qGFJi_G]g@LiIq{R`>:^9<@eN@EjNX9RMabr\C`E_bHg^\Jx[Yd9l`@@BFQc~a}I&gQYlf?X?SF33YyBhyazR>=S]iQS[IE2cH]cMIm`yRH:yj<&Ac[SyESa{eZJ>^j2oRrZ>NZ=|y2Aj@ax:dn~xEB28b~JiaF\A?SS2x~G`flfqcdxciX^xZfX{Q]}D`mjEm`Bq{M{mjfsmL9Ooonh=DNJNL}^\~9eShrjeMBjy;mlsAXMQ{syZNYE9jr@FY\3}n2@3Sd?XHbaFC|B>XZhNEMJ?_qr3Gnm2N]Zmc{D{|8L3C`NH{brYjM\;H_DBl=DzdsSibHHN&EXCF9XqABG~N~lXRQRD=}M8=H<&g}I]>hxF_]2FFjEX]ar>9inGR;cCIN?nb&MFnl_xbZi_N{zBRz9|`Hb2OcOxmQ}in&:r3]Q<~Qm:TGcboGLHaTRj@H?AYD99sYf]2AOrMG?_yS?SQ[d;GT&s;3&OQrRHJghiLYdg2x2CAfNn[jyhX^`Mco|dMxB{{fO{?YIlN9n}}M:`qs~aeSOM_Xzx3gBJR\;Asg<IYOjh]ARrjq{qI8~od{^&RNiB3D^jH{xJ38E~bD8hlyiRGjYgHo}x9f8xDoJoxgBi]_bx|QZ=HqgCJ<|[zmz&}@lY8rby&O@d]xZ~Y_:QBmSHDyy?Bj>A^N{S;Ei:QGN\~j2Q\Y>aDm3L:Z_yMy}2CQMH}H:iR8?_Og><2XNl?diy|CH_S{@jSHyHHgX`Mdsb?ysxoQssa9ohgJ?AOGzIoF_Nj{@\>C8RZC;]>:lf>TDEBDHfTLlE9y:Nf^FOiZEcD;9cMIQ|=O~D=QN:aBMoF2>jFMQ^>ZYO3bAf>MgshF8I@qfY[NmG&>`?zFR~=@9Mjx?T<]jYZ}Qx;LGZ]~od?_N|[bO<}QiOxi<\oh^hER>gBFlo<]:[AIyaAz9IC|9^J39HndeSIognaD_?^GN>?m}{fg&RNFo3j}Mrcss2^caEfo?xeR@f=mT>mx3D&8i[ab>[Xm\2Nme|28djAj>dc]ayOMzhq@]lLdnnrNS=lif>8sTRZR|djy3R?fmj>NEXnJYS\_adHBdsoJa~ZIqYcJRrhqQN|jlr^ZLi8EoHbE>Yr~|lRCcQHg`jyH`NG@LmDM3EM=nTa3TY}srbTCSH?edi92c>[xo|s38OGTm_}ssBYGH88&CTC?qNXLsC_<3oqEhO8B?Y&C&ATs9TT}2qbTn\X<3:i}[a:A]<|=cG}I8o?y=<||yF]lna:M]<`ysmy~89AM?D~<}O&i[{scmoe;=;i}@sA382y=~;G}Ma98nFZZ&hD{crfD@s>DORB`9FYiae3GbEE;}O?DlGlJex{OCZoBiy@c\?ghC9?FOH2O>o?COFY8ECmG^SX\bf|lx:Alq?8_`}[G&HlF\3qzN;MhbAl[Czs~SDQHG@F9bMHiH_q>gCY;AcF3l?r?cOHSJZFr^N3RcaxE}Za[Xrcd[LTMzZmn_d?NC9T@?DJYoYjhql{>DMGANa~y9]N:GFhZne<}y}nid~~S?]ey2HOAE|dbOc^s=\CiNZ=H>rZEd&[I3]qfa3C8SSs<@CsIDNMncX&NlMNhmrA3d_nO:TT=}|&bM}G8[Im`@f?Y>E3BlcJmc@9mim3SscT|mr[&|[m&MRLy&B[o?&]aheq}?h^jBm&HmC_L@Y&HFYTsRZqqBnaITq9?=;Y2ZCBsT:QGaCR{9Gns}Df\}I[R}I[s[s];OF`lH3TxGhm&Ex[2@T>2n:8?z8g9|Tr>hhjd]8I=gxy3oB@ln&iS:yfo~XzCmf`CS2s=Tjzoo9=e;^l[=DgT;XlTd=8qYzTfbyTEMy&jG}x?S}Qd}2a{mRzTqsj;hqQo3eX2>E]Troi<`:SqA:TfX]TDgS~oGSh9Sn{;}2h3T2m|CFm~c&8T\;Jjd{HTzQhF3e:rJjC?83_GC>~>`]s3Z&fS@S;CEDY{Zl:_]?Zj~rG^=I\cc`bNYFNyZd8Tcm?MNS2b:OIssjcTE}:l:O&imfJTc>{=TYmomNaBTT\LIrdh2OT_BimR_O?O\<&bI@m|&2o8m2T=C;Nn~2]G[rqGT\9XN2zIy`\EGzJJSZ89lJIyTAe_2&`R~Zhr3@~X`\}i]`B[drx[e:glX@de?oL`\cirDiZ3J3~ehalqC;YmhBEZ∋f[sNFB{&BCfnGm|r9q2Dfj@B_&_b3[sH|]nbZnAgC]&^ZmsAMTced~oM&{3G8g\8XqfYYe|e8XImX@Rf[2CylZa3JTO~f9mG>B&BHS@h3}rF}GC8|NTo|;8|8xaiIH&ETR{>s:T}|mB=;NXxqROxO^H9mNQadqRq:DqH3RDeD]g9&lD[foLB?2mmFGra3>mDSGT83&`&~ZBCZO?f}iJ83Aq]qr8``3F~d>aCE[aG8nIeiHrDQnocm;J}sJMCzc`xEomsaES`xe[}|q&;H3M}bRYe;3XA]T;HrOl_s\Ox;8ELdil]sSTNO\y&~i`Qs`L>diSRB&CS^ms?Yn^Do9D~JR[_SJjNmano];b~gT3_3SlYI>@2r`NTg8OOa9hEeZ_a?mfG}>nO?2s_{`TL3qfq~oRnHSaDX_9qd]_T8ql~H;hTSCe>i&2&fce3zbycc:r3nFdni83i;dYL2@ngqX_O&Ey9<NY2[am;]o`|3Qx]m_|b&J2Yf2_L>99O~SCT8hA@MSZ=A&3>O[{8Ryzy8j;\&sHC|e=MRA}CihOBG&JRe}?Sfq<;_Ao?nThmBTH9`ma?A2I]N&a:&2jmiqDN22J>?if}8fO~_8Jx;ebe;Oecny~~`Zi9<:~R[Hy3RCTFf}>d?8_Z2&^sM:a8}bE:qc9=Y&h=y[2S[OD&MSb[&an>l3[[:Zg2o=bR@m}hN>ORx`<3T33iXml&22;^\L:D2C;E;o~[eihYOx:Y_}rYQ;oh&nJB9a8QnL}|iy>q:YsN}dS]&q{r8T`:n[m}MeiTiA}qiB9Cds8R@aLOmQ}ZzNm:2sbY?rzcI<3[T@CO<|e;SRGmE[`2?mAH&Glm2Q3ZZTE~f;AYl9&rzIfXTdj{}>LQf:C}aDo]qH2n3_hBl28|G?Gl]&N|[Je[XNHZ:?naSq~m&8l9>9n9=s;xf2[cqS}@>A8{IqTY=@^N3;ZmN?Bgn&MD2ibT|Q<9nxbi3@o}Je{Ceg{g>HfGZ{=rS}Z{_;Lm\bT;@oJC?|J_>ec^:i?]Jc2@gdTIZs[RoI_h[h^xa=C&|TOTgD\2>XY[s^:e|yZ_9CI^bghn|{He;Gy{?lI&ls`dF2JI}dFQ@O[ZJi=OLXlq;JL&ls`dF2JI}d^Q@H[Zli=OLXlq;JL&ls`dF2JI}dFQ@O[ZJi=OLXlq;JL&ls`dF2JI}dFQ@O[ZJi=OLXlq;JL&ls`dF2JI}dFQ@O[ZJi=OLXl_;NL3b=3}gS}miiT`iYD;~O|F[TOoM^n&O|[Sq3jh@o3X2ci~mN8c^oq2<[cd}m:CjJTC3g>N&jcyBDsn&;}A}L;@8:d{3jz9~A}HlZTrb=ln3ExH8g[Q>=m<3DICCeS=oC?2iQ\|Gl8\ETgCO&;}EYA[ycay&rf`mo3SSz&H8o8_8:Y;C\jTI:CDZirG:>8Fy&YllZ&{D&Fna[|=I~ymbm|}b[R]|I>g8O&J=>3Em9re=sf|hQ?b&yT;sF}yfNos}<|8`}3d>h[F&sCzSc}2a38_:}GqGr>;\&GnOn8c2~Io2[=:<r8@X`N[eZ9ALi`m8nx_&j@}&HXR3b3^2FZ{TC{M&;TfarbGryZoq}?G&Q8O3Y:c>D>a233zFx>yin<|\d93D?mm?HYo>Mf~mqaAx?nZ&s&o]ye:Geqjq?a;OZ|h8x<8[Y}JiJMrM3FmH?G^T8e;j=An`2\jI?;3Mcd&ChZhZ}c3LneF>`qqnR:C^M2@MrO:gYC3;H_:IRa>h>H=YsCZymOrQno&NzyJ&3eQs:^q=L~&nm2A}Q2Gn]DY_TyfTXqs{9TnEBlTOnEazd}ny39[fF[iFI=Bo8&N}iZ=|}M?OmXTo^YSJ^>?l?]8ORBTCsNqDTCHG}AgOd_Y<8SMC2a}cr@q|l_?XCf?q&e`cT`?SoYj@&Jc^T>eMEsoIel~SE@~x2=ybQ]aBmSnh~Me[N{JT>Zm8_DL88h_el8<@orO}mo\8D=9r\}S~boR8g=@&sDgS=GgsB92FEdnSh>=R=b;I3r&I}l3?r[8?=8iz&ES=9M:`=@QI8gx|}r8gSe~=q][}x{TsGQ}i:?3g^;T|z?&M}JNJsDc{&yHrmY[9F}MTo>i&EQ[oR&~;3<@>@3Im@EC3~Qe2:m&_Abx_Ee^il&>A?Q=z|o>DSi>RgC2dII3i|9D>q&9x>n=`TN~:s?28ni3j{~?szoxiGJ2d3z>_@So&;~_Eqae]3>s[\_xe3MI[2BX}=Q?q^dhZoYmXyR8_jxGsi8BmOIX}X]lJmzm833ZSgm{nby9omy3m|_FInL?{C:[`:iiJJ>grNnCR~a?Zz2NDimOOhS>}^l|\r@s]3Sq\F2mONAnJcg[sqJd^smxZCR~zCJc?{h8r3blTZqd3D?}2O<[oY{Em?F2aO3]A&S^Y8Obd&Eaq}?s;eNd=c>Q]F[;x3}{qMmfxGCEjFCzTx8hT[[qcqh>g?[Z~bao&aB9[G~BqNnf9@q:9s}[=NF@O9n_:SiQC;2Lis[9{[r_q?A8bi&n}Gr\@e8@e:=DCe>Xde~R:c=qI\~asY2[jJ23[ay`9?[[T>@Jm3Rs<8yG|s3A]S>xF_?3J9LG&=?SC[`ysnz}:X[n_Z\83qne[SA{?nE|2gcJ}oiOBrqoN_JMnC@TncCx|{M>oGFH&~E8:j[T8@er|T2]Drj[28?naYT~TsmA&y&s|[2y3@RBT9T9G3~`GXYTT;m=~JDOYTg&;}=xaL|T9}=ze[2yTbTQ8g\J&ba2SnXCIS|]Zj3N[CbE>ZyHC&sTInL[2eT}JlT9}?G|YT~To?MC&<28lZml}OTa<:8iQia~ENLT|d8Ra&3~;~9=>A:n2o\yHYiFxhay9\EbSQl`aF;`hN>o\y~Qs~q~Z]qDaOaO@dFsI&azZbYDHDAAC{>g[N8fQ?S<>Yxig`^>aN=NTE^YLyI]f^_&xN\SRz`beqsF\QJRchSNNicNnz\`HAYrQ{I\xaR}eODojqiHXg8[=^}SF9XD@IGIJ}FEhQ>E;=XZG9j8L8_o&~;=l>Rh^fe{;b>MSqbGOYYS&rmzJS:^oJ@G=;32=[YQshaa]\@^zm^~`E|lfeGF_=;=JcQSJ=FQRao22sFnzH=<`xLD=qhesQ]r[FAOD`HHXQ@`mH>`zNhijY]Q\Og\HiRX_h{9{nJ\;cn@aXqqM^:ifhBA[creQEXfT=~]=e`AcMR8flcm]?AHRnM`XXX>2|qG^O|Cmaebnn^{L~=2\bXqq_?d2j^gI|Qo8CAJ8N[9HrZ>@dr}crqjZrSDdZsSfOy<@ieB@ZN@dGdRyD[I@SxzjFCLTdaqjD@m~]mFj]f{iO:~I9eQxaJF~L39[l`>rQgA||fCaZXn{OTcel;:2qXjeyQoJ<{?SxAs3i]Fi\ZD9O@:X\hIh~|}CX&s^n_N`MfF&EcJnEA>2Ysqb8hd:ao8coq|]{9B}g|mMd^]{sN_s:xAmd=b?[\bqLNg`3obY}BDhcJsq`\Gy9E~Ey{HM[Yj>M>n8S_\::n2F_^}9:9Aqha:8dmH9YsiF_h2r?IseL`C{xg&C``C[jFQa:eizYzSEA&^irIxL`nF&~QzZ_@~QRgy`fs[Asd8N2NgSCD^>XJ<`xh&A{AcS\XFXN&9_@9D`Dm}Ac8;nC8`C2ge~mzo]AAJoFqj|HLZNOMf`Gd}xIlqbRN3I[RdFd\I[JXrSFH\eyfJA&>G>H\afJLYJ8F2fB=?FOXAcsB\CiQgZ[z}N_9_OrY@Z&{bxAmDBJ[Og_CF|C_[A~fY\a2:X@B\n;?O>YZLR}2{dAq=yf`XJ8`BGMHD:{JMrb`FQHgblSGNlJS@@Zq`BiIJDm\]?XZL3cIRdAq=\CZXJ8`BGMHDLGIJDR\I[JZ\@NhF\@Hh|`JnJl~^B|j{&|BEgMFRrNEllSB@J8`BAMOn;yEJDozS\L|SFXHZ2c^dY:shZOeb|lGEGZF\@LT{^rhqm3;^BRFGNJQFyJJ>naoF;dcToZcb]yxDZ~^`sbBT\nIhxrJIF{AODZc`NJ>S`FX@Hhs`@\BIFdsJsBdAqLZc;JH>QlIHQX_LFcOBBMrqBJNr``F>HLZZOgfZJGgAJirNNblCOFllSBXJ8`BAMHDLj_~HrsZ_fBlO?NNr``F>HM|ZOfbA?Mr}H&Y9H>f?JC@FYh[^B=>`NJqR`JH>Rl&o_A_&33HQhqJJAZsJsEZ_`dF2MDCALzs>qYZ_fNl2{3Lrz`]a{BRrNEllSAz9RB`IyB^x^J>2jQmNlE>HHsZ_e9sGBfI[JZ\@@h3]XJ[lZS@zR3gsJsRdAqHZI?HH>Z2MQ<|NMbleFAJC`Gz}`JO>oZbTqNOMbbe]~]sF]lH`r;2H>YoO|AIIb=lCCMsJ\SS\9]dFHsZ2qgZmX{TjnIx~~oFaFOOXl}Lic@ZAxQ\L=|YI|?`&HfO`e^3cQ`>qHZD|TOo]_8iqXfBlO?LM=be[Sx{OXE;Sj@bn\^8bRN3II@zn;dMZfOOd`SGiZBzyxzhNQZ[m\M>RlIH2^2oY|Zm\yT\jcn~:=nEqD|JD\BJ=\Zm~zZYH9[e_XBXZqNTyEJD\QJJ~^`I}BZX@JhyrNAgAFRrMANJ~\RJZf^`saBZrHIhxrJIF{AODZc`NJ>Q`JH>XZL3cIBdI|qBBMrNlE>HHYZacfZZOfAl_|NNbaCOMG8IYRdAqLZc;JH>XZL3cJ?dIqY`;J>8{oBiIhq3JOgfBxC@FohX@JQFXHIh`XmD{f{TjnIFzsFQZC@ybmx]a{A:8si@jXiDbYYr9EeO\B@JY_LFcORBLr>>NC[JZ\@@h|`BmJJ~S^AM|Mg:9EHjOJx^[R3`BSd^T[J9S?JB3M@NFLAzJhgX&gMEAECQ@d>iLqIaLMlocTLbcCOMbleFAJClBGg@F:QBMrHlEQbgblCOMllSB@J8`BGMHDLGIJDR\I[JZ\@NhF\@Hh|`JnJl~^^s>n`BmJJ~S^{s2bIbsZ_fNlO?NMrcLO:C9IIxd{ODZcxNJ>cRN3IIBdi&gQdIqXrr9bsb?lCGX\|FSdIssRRLf>TNrYA3=fje2Q?Mrl@2mLBJrQfJIrcNCXbC2:HLA\c?HLZZOgnZsBdAqLZ{RlNraNONbleFQHCZJGgfJNAgAFRrNFllSGFlJ_@XZq`BQIJD\QJJ~^`I}BZ9Chz^c&&X\BlO?M2H@ebBJ&ImOeX;3H>ScmByMoqFJOgLF~CzJhhrLIQxXJmReB3?Zgd`T[|9^?YHMzOxnz]?{Q[Yrr~^Zyz{>]om@L~X3`Hn9C`j@@h|`BmJJ~^^I}BZ\@Hh3@?GNBx^N>2=qsf|JGgMJirNMble[{JosBOnZlx^B?RxNJ>cRN3IFhTRDjHq>DqyMLn}{b>Z@fqqr?xff~oM>a\ffng3;bom^Z|yLy\B=a_>cNceh=;;fi;IYI;|so2|O2bZ~2^e^fJMznYLX{AJ&I^cB[jm[l\GLGHeRH`F>HL|{9OYLy@eRH`FQHgb_3NbI~|{\JdOnZax@]aNlO;MX9NlO?N2IRxRx@ol@Zq`BScD|mGN8iENCXNSE_=9f{GNQr;anci=Aqr`nBS&XR2XXlsd=D2>?IY:lBm&zjTN~djJYRfMTbg}FdFXDa}nY^^_|LAf::}LBsXAf=D2@mgcDXQ[?^LoN_;GT&[xD=I3sRjMTsBHMT}N|DDlma?HMTeN[XDa}LlB\AYG=~9ZYF&Hlm;_[ZeTSB`JsR@CTD>aO:HL|;O2Aa~Y]hNOTnI|`2FhziRO&fBNM&xh_|\r8mf{>NQ@``BS&XZqNTy[Z[9QJZ}3`I}BZXDlmyrNATQFRrMANZ}&RJZe2`saBZrHcm3rJc[>AODZc`EJTQ`IHTrZL3cJh;I2iQH};s:rx;I2rx^CTs@oCB8d\&bh3oefMT``[c;dFjDa}nRZ\oRHCTz`}&izLlN\Acm?{<&a@ONad<8f>M^3IbQo<&cbF^bn|2Zc;EJT2]8~AL\3Z[T^Q?XsXg~l}&xQSyr\:Ciy{{RSB^`@jnbm|`Ox[|82B|Rx2N2cBJ&IcmgL2Hq;_8l33|y2`s?bgYaZ;ETqzH8zJJT2^:3Lol;IbsBGT][n>Q^Q`EOm3sBxelBXE<}n>h]b`OrCz^DldSTzD22ArebITze[NfD]`ITrFJNTjDAdGQiE<\3HNgXXN[TMJQbT9edCqMldS3@J8`BG[X[:{HMTb`FQHgbdSTNlBS&@Zq`BiOZ[9GJZ}3^I}BZ\DlmF\Hcm|dOnZln=A&g]cSfRdi&yl{Lrrg{Ns2eancA=I2T8|C2BrRx2B3RxEJTClRCTnFYxEJTl=IIi=A3fY}JdD=I&[l?_&\@^E;I3qxNOTnH@RdBjy:^3~G`HXME[Y[RXO9e?i&NZF\&zh|`BmJZ}2dO]ede3AJgbB\&`hF\Dbm|`JnJl}2B|Rx2N2cBJ&Icm?YJsi={qHZc&HITggn[3IhqEOm2myIj`dXC2HE=MedeTI`=sS=Aq:T@qLldi&eIaizJJT2^2&aol;IbsBGT]Enl>RQ`EOm3T?|elBXE<nl3Qb`OrCz^DldSTz=TAArebITzf[R\D]`OT{FhM>G\`~`?2&jeexG[Y[ySzosNJnJl}2^>&LXLBlr|3GT]FRrMGNZ}T^N}AAODZcxEJTRY3NlE>HHq;_8eAZI?HHT:_Sxj?|L8mdsGMNh2Ss2<;Ysjeo8[D:En92ReaNjqSsq&[T8EGhS{_XjFBBxS{{XrEdgx9i~&DqrY{aS~YTngAAB;bCf&aEl>MEEXlE?iS}jqF~=9_iyqg8ys&nZqe&\so8bhLqoaNE~rT{@S9q{g<8{{b3hn:FA}jfz=qZ\8Q\bZI_eqlsO9omeC{9f@y_2bSb3?Jn_m``aN98fd_zTA:|A\cQl{dg@F|Nzs`?S]nAI:&nsq?}|=bH?sbz=}A?\8e8=[gM}9=S}9=aS|TGS8miS2mE3oO2Dz&n&xs>2Zh?\Z:HqI@9QdIfSRsqOhoYg>:Yg>MYg=qOxzqX3>]Ia>:Aa>M@{2AQh>3g`Te]nh]TEQggYo?jMj{yTjzRQJ^{8SjTfRe?fzod:y9echQF~8F~:oSM?mMF>~;b}9[gar&h<RDl2c<=EBr\zMLi\YZ~rYz;fQFMx2Nq3>=oLsE2F?g<<8G3BL@zz=i]&y8f^F\:QrqTd]j=8aqFX?8AAgLTfjn:}T_A[\<]9\|@X9B]h:{y\rza|yQ9@d\OX=BL:r>;{~{hx;L}h{YZj@nbNhyl9x`Cna3QSER=~g]MyA]yAeqr_zD~|\\mI{hnO==omCO`2LGE?|;~ZQLZM=?>iM;r8i@M}@\c2>B=|o::\^3jHy]&aj_8?2dml@Gi~LSZ]2Zl?j_^jBYR\gh&|fEj]yT[~`G:mdHS}Y=:j?>GC||Djdob=eLZ{}yyqsCM&bY>XTCai8?_^j|[gsh^TTQ3&[fi}889n`~sD]In^Cq>?g&&T<{=CB:ah;B^Cq2Ll<_L|YY{BfdjcL^3j{<>gDR9o}{&C]>\Y:=qI:;gALM3L[g^ze2Ln_m;c_n?;x&Z\D]@sFL[xBZxqAagbn2c?EysSHrNe=chx3M~`oE=~jaR?F|IN^e]~|i\&g=C}[Nh3Nb]qmRfB:B|^iI>9d\[S}mhy]n3=X{b==g{anS8~S&}nndFQN>Q=:NimqONy}>ab[D`|fslToZqfzQ`j@{q\LT=_g_TnrX{r}DHe[?Bo&&]]=yDYY\9Cljfj{Q^:XIXyZDD::aad{};Iq}CI>hyqZSJs:^ajZBmEA2dxlXe{8Cf?]]f>O]y_\@|&=@szSq@]O=a8QefbEq^Yry\Fd>qr[zs`>jXfTyRZ8QL@S~iDx;][89Ae}}El;{L@Yh8_~NlZEz;rGRboS=YRc9&bB}Ci@[Zxmcx]aIjD;dET{8L>_BI}>eZrOl~\xM@a&xsadx\&~JrCFL:Sz^e~as&bT]QA~fg:iG;<|2xILrjn{mcRAr&rYzr;sdBn|3y~@;dn{=msZgAjd`{i8?Ey~D{oAHT?Ef_9hf?Hrr:__nGA;RjCl{=AgxCYQf~HmnYy^cJXCqZ^;jO`FGz\&~nZY}AYjByZXch|{H:d=Roo[bcNi_\y}`l{N[cX2C[{YxaerqCE8lbA^Cxm_Qqf@Ash|xZ`CXbOSZ`Bs_^CL;fJcR3fr`GL^\xcR?~9b9Bfs2R}b{}GA]B<Re|XRT8Iojxq~Jy&IEnsDH=EY]hs\dHmoFr~DIgn]xDHzfYchg;LOz|>hhM8OOj?@yXgc|]ZdAyaGL|]]MAyc@ZZn=fL>|]XFGyl@8|]S=EGlzo|]Shoil@l^QSN^sl=~dR8J]\lr~HeqI]j|r~J{aIgmo}~JzfI{hc^\Oz?I`hM]FO@=bx3M]LOD?beJLS9CF?@{ALg|]idDy=fs|]n?Ey`@I|]\NFo>JMbZmq&3jYrfqsgNryjC@e?;C|`:^shIr@\e3Ne`{zi:&yq89jXjz=3Dsa&FFSyx]8Y~:SDgzdf{Dg[cz@3J:oF\>^mbj=jLs&aH[jd?9yDgLFE?gsxoij]]9qho]Ze]mR2_N9=b?bgh9y~@]]9nC{=GEjF]~FziEX?b`[MObzcRO=a?]d}Ldb=HyR9yl@lajxlh~Dzr2D?bzN:yyAfO]eEQqSqyx9Gex[@O9sdZ:&>GA~y]D>N>YdSirzB9yGfmazzGR~Dd3Ez>@gB9y]fiazx}Q~JdCE=<@dR9yhCOazfJ8fY~m23\lSgn2:}`rd=Ah==ld:qdzrySYSaGZ^BiSo|8^^^SN`:^^hfACjyqXy^MDl|Rf<_<_ny2Ax[~z^FiSm`fs~9a3z;FXsS]nyfz]9h?hnyjD@RD~dBcfyEiSQzsB9LjxXCbsnd`CCInGf3de~YycQMch2igj]9fN:gSRd8S]>9{oojsjSsSI:g]CdC^]]RqEB_NoyeLsyfiyzyiY~d{Mz=@AmFr9?MhON\B\\mSrg^qz8]]2\{o]i:j]=L2:gSCzeS]bR|IHaCqyO_>@a=9yzA[yjxhjDxzaoj=r{\:yyA^yj}ZXnxzBX=r}D:yxB;yj~jinxzBX=Xxr;yyB}yj}N_DxzB2j=rf9:yczbyj~R]nxzzoX=@zyLTb:I>m~\X&i{}So]}\:oM9Q]c<]:]m\El=MNO:;&M@9Yy`y9yjzm~xzho==8cIdS\3aDZyI9DBx=Fz2`;;&s^IRs{Ch@DzgR?IxbS}??jaTqTfLYDnxE2l>=|:9oG8I^Zz=j}m?{D}8fqXns]fDSx]Qagh]NA{=LSFsq@{2FoY3}Qaqso@Qq]ZSx=[zBejXQDnz[b>r~::yd@Jej~Q~hz2n>=a>9yyzQej~;S&hzQn>bb{9yy@[ejxNS~nz\Ez>jdA9yl@D]rag;\DS&yFc>3>]CsG^GfNlT]2Dgh2r3>]DqFLdrRz];LbaA9ydLSez~8S~Z{jozbxQ9yrMqezb[R~Ryloz=zlA9ySMqe@{8i;j]qQ|ySIEA9sgS[eF]XDsh:S;<]:eMqg^_~C[]2~h]Y^<]:SMqg^3x8E]D~sh2iR<]s8sqg^Y}CE]fn>hEXR<]=Sqqg\JCD]mn8B=~[aD]>[:\DhjIr:Be~z]~Y?>E8_YZceD3Yn;3S\Yse}b28q32<\IosiIAJ`&^`hln:[]Siir`^8DM~eB|}ls3F^OiCn]oz}o2^Z=:8:y8{m~i9QaMJ~]3L88gSgxCm]mDsoAh>=]qf:8g^;y8o]{nsoEY2<]>CO8g]?3DzHDjchb`|a@MhMqE>]^GM8giGxSn^@n:hA|o>]yyI8giGxSh^Jn:mAs2>]y>]_j:8:Qm~SmSnngo~o>]E<8]iTySQ\2n:o2}>]n]}<:8]\CxSYSfn:mo}E>]O3eb8nCr}[}c8DR~fxMmxn[ECmC|CM:2s|2ib@=ZjRGL>B_`9LT2R_>F;3yby|}{f>d@=S]F[Y~Aj&e_]^oXy_&TY2j[g[jbnT>i&C@2jE^X{lzX9>`oDZ=~Na9OzsqZ?Hd2EGa@CJ9~``zOz8q_?Hd]Eia@^l9~ReLOz:ah?Hd\E2&nEB~Ge2FQs~m2_e\srOXObomcqe};?{[x}EyQcc~I=8qO8Fhz?s2q|_x?|:&~Rgq3Zx^3\Tz8BeF3{^3&2zIOBEiYl~YeoRS[AJA^Jy]ixMax2G`OjGzajdy?ZGNT{r&jxerEqFQbZhfFi8fFgB{OD_`iIz]bRX|;:`a[2sBmMEB&~IqLX&j\Y2`EfD&=jY2^AfFcG&qX}l;sy\NQeB\N2`A}EEHn~G_LXxFq@_&RXGo^hfFRmfhg3:d<:XLRr88@q[xgFYd~rzT`D@?be:DQEs9?=3DCs9?9yi~oF>{Lmys:\bnr\a]o]iexg3Dx{}E=?=xaDQi|C?Hdn^{jEHrmZgFasX^f^}]c[I{jAnArG]Li>F_a@a_@^>{e[N|3{FsRHf]hz`of;YGi@fLh&G93n&j32I|N:oYJ|N8`DE9yzhLD@oEnIym&;&cz{DG^eqshB}F{TF@DhIYA8qJ@GDnh>X>q|ILdmQEn\]fXC;m?8q:&xzyZA2CDm`|?F=jY=|ZEnl|hfI|3lOs:j~\`>AHXf@:]B?qEX`A?;C<_hn}=GsnOHEs=2d[C>M?TNyfgs&YQ[Gy9cr&Y]E_y&brngSEMe3bfd>_Am\E{:ijroXSfxexEsh`iBQ@S_iSs[zozr=Q9&X~}s|]z[2o=Nxsja[lrAo[|eaB[]T2Q{gq^qq?M8jg}Tfne={XQy9`F?_XDl3lR_DN;ad[BLNqcEB=OrIcH>SBDLrOFfFhl^X~{rIcH>eB@CGN`bd]b}d;iz~ho~Sy~N]oy^~Q}xb<>A_AQRaxTAZamq@J&qFc\DqsmrI&~zeo_Sy{39NgoDg3y2E_eHa8:~^A[mh3q2EYeH_:Eb[_BQ\Q;I}N=nf|;XXclrnBF8bHDBFebOE?labOA?lqnBB?lrnBF8bHDND~BF=Z@?lq>JybOGfHDN[DBF=ZF?lq>JqbOMfHD[E]:\NR@8]n8[r|?Gf2yed}}z2=GaZ\}Hdhy[8\[x?>_2:lbcrTeRcN~8EocfYLf]q>nY@q9A>L_Fsr&G@_3jy@xzsRCMm:Gqs\gZC}S|rL=ZDfnz&q|_M>:]b\n=~D^m;EC\E>>d=rnq[e_ae2}]MhF8RYMsjY8<{mO:Fr:=?Da2Q<~DXyXq8snr8>eyJF?xzE}=LdxHaeFzEFxR>>EmGReX{ayLD\rNqFxxZ=AfLx[n>royaGhnoX>mgbLQ^X>8~h<{Gh>8n^=z<h2TDy[LCcE|`&`&y}_]E&ZXcfS93fOOGc9x&iE}y=3`ZnHX8zeb8hrB`@YEHc^3a`}|DHG3Ol~BQmaNeB>GbLS?sArDb?c{&FXFqB~xfAyMx[xf?2rGy:BD}\l=siOqB~x9AYiRxrBaA]{[j]}QH;]hI<_8Z~[oJ?2?OaSx[ED?2;Ga8ZD[nJ?:qZ~F&Aq_~h;Ga[fD:fS`]bXGq]8[]`sSgz>^d&3cd?qbY~Rqa?[NiIdTmFCa3cxCaE||n}9C|&J:OGyTcxlqAaCG>O^ED}9CB=_hFLR}j_FF`ojojy_=y>^[OjSf:\geq::jq>YslqT];]@\=hGcLNsLem2h}A^yNsN=8fF\nfBCXgB8d|mRfOyoDS9AeD>B@{yOMT[^3f:`Q[^QfFgMg2LBjZ9j9d&hiTBx&GyjOjyz3[rSCsE]Y@=zaTBT@;]Nz?x{:BDD`xO>^~fj|qrann{A&m&nasBBjFs_d_Lih]]R=|Z:;>|l<_g;c_SO`bHEm~{X{2goNy}x>;hz&rYICfHSiNzbRbog^rc\[fy&hQm&8sz9~[S:8e3z}yeC>:T@y=rs&2S_]y=J3`hqfeFS\9QqJ:ER]=>rrdF?:8z{~~d]]chd>=^[ebh2~Is9`F~{q8`oEzOs9`[Czh2nAq9?CCjZr=e[nY;9`F~{q8`oEzOs9`[Czh2>CGjZsjem_RgdY|fl:=|Ie;rR=N=A2Y:J9C=Q{z\8iYH=Dri:diL}`EnAbm^89~Sjr]3fG3Xb3SaFahoaia|nh>[GR9>rnxsEGd&>rn`sEs=[~mM?qAh}AR3T{3mhnaqf:~Fs<`3>ajes2s=[Di\?qL8~_qfEzmC8Cd3>iaFin>[DR9>;g;=8=IQXQ:]G9mXi8}A\os8}{[E>Gg3<=SA=G>8Cn&`38`}e3]?=aTmX>mAn~{qnX>mGn&grnb&os8}{[E>Gg3<=SBnDSi~]r2f^dT]=Gn={cFlDoE`>f^m:g}\}}nJTETMglmy\h_F2lyJ__i8I=28~jFr`:RQ[ROI=Sj}H?]NF~g9yB=]BEC{oz3Gc^Zr=Sjxa{^l;ng9y|~MYOG{ozrDgXahG`Zcl_ji?8:9jDgamS>yRnXqh@N[TffdD|\cN=8}fQ:hisix83]hnA=eaA9e^>]J?}[DAbOFJ:r3gLdO>=:`FRHlh8e`dCE<>2H^DiN2mRfh=`8h&mq;G9GsFY~rzQ}[[>=:`9|Cg]C[Q:B}SR:>e^3XAeSxAOE{gemzax|r}:E8>Aa:ei{q<}ob{amB>>BJ[zgl=ZbXcEBYAAf&YEy;cN`~SR}:^@^LRF`:>gDSAhQ&Tefe3Oxo2BfGCe3gse<&njm|Ds8;bRxnmrSLm}>?enqc^ym~]zJ;G&{\zhX>i&>Rj|G2}j;sM9zj@[Rgfe{Azq|hE~]FC]~g[]@9=\bao8X_d_q[Y\2?lE:g}aJiNezF~:yF`B:FC:@fMq}^z9QN=s=`dj<3y>AobN}yrDogCOqC=Dlre]`RdfyZjjB}byy<[LBNx;jn{IdGrrzZhSd_sI?2T_@|2^lLS~ZSGRRT]:o}|>S|ZA_Qm?BxbY|;cmHbDJoeRfOO@c2DIj@ld|aYa_SAJR[]:hy9jeMLNcddbqO:BOlbx}x=D[9h^djLbHCF[`lFHz=&q?33XYQc=;MGdm~{If|s^]:3es2Tf[[:z@^og<`Lr&_q_T{b&`goM3S3|2IF[&qy?zb:3?&iX2}f;yAimmC&&F]SC2;ic}Ym:2B}b;M>&E}ir;Re{ys=omTZlaO3=T&[_8T:c=`b?O@qmOzrnc:y[[=ff@S93T[G92~<3rqMzjmTq=3}}Ts2A2[9<22\SsQTTi&sqT2?&}MeiTmT};ma&3&mCG}<83=R}imT<{rTxm}<&&H}2?}=?h;[S8<<=CS29>~T&D[TT@?&Diczq233yxTm[TgS>23&&&Ge}T82xTGE}T}T3o}mq|E?qnTnRnbNl@DssMsXFdoYm@Z|bHdBYOnmYjI^L{dAr&`hsai@RYgc~Tg:Tg:}Tc;9T9?9T9?8T&?9T&?8Ts=9TR`=~D:c~nRD=ngHoTzc[m>_gl={{C{_]&Mo2Tbx3~Mh&jI3ha^TsdCZ[bbi;sT2l&AIT=O3qTG?o8S3TTy?Tq}`QYy>BA@m9d|NRTxoA\ON<8Nz@s[aN>T:~hTosl:@zDzjzsOjg^fgf{|A_;|>C3&Jnby2De2F{cLNS`|~a~;?TG3&8q?2&eOn8?]8}8T@2|2TaehQ&ya82:??&f};=}@I?MhqaTzqyT\QTT[[Tb?8IfFT<9TDqE3jTrTDa3lN>qbzQ]A>i]2BODFArDF;?ha?>qomZ8qTL}`SQbnayd[BmBQcifxSsT8RRgI@On3mE2hAa|nG`mcoc\AI8:2EYij\?qsO2X>Xa=S8Snbb:oy;L2Z^xeRxMlSncRl:E`C{lAJf|EaGa{l=8}I9:2d3ZEiQ?{RX&|sjCr~Lg]qycJ:GC`]T;GAeSZcO93F[\b;~o?F\e;&9`BONSc_bh;x3E[`&_IZ:[}[@x`_z?br;@rTf[9^{ZolyS9Z~AnjX_9q]:_by_aaxfzn3Ef^sz[_2A@}<~Ac&T?`DHzH3zOg\dgxqebn&N`OoAECO~Zc\CiB`Dc[Os>fDGj?2q@XZ3Alq:fx[2H\O2gnR8QQE3^M:`d|^ngcc^sBIl^9SiE;?ez{:Co@2:^2QADYr_{enXOiEz{OfAaxRn;2>q?glLsCnD~{_o3F9G;3^2J9JfxQl9Z9_LBCXYNXqi{y>eIrh|`N`}E3XYh{;8n&>gXnjc:\>?_C;3d:c:DYa2FM|{eY}S{OhHdrYILhzxeMIBNgzJ~9_qsMHXcDQnNNbx8J9=Cnfoz=So|i^_>]x3DYD&nRFedors>ZbIFJ@N>:{fDH^9rAgI^;OCg]oC`hrbraZIFgG;iM^?HQ^cRh=jL3mGYJHDS>>RmI~z^[BqE:j@@&m9MEhQnC:}bfbYh2|c^rOzHMB{Y:{<3I@BRlNlJg9G|F3eRo^haII9mj|IDJGeiaj&fA8?xXHa^R;IbqT^{2^nZIildS{iN]Q=`SO~XIyYFD>j93<9n|J8QJ8O8;3eXs^gj2emn]N>oyL~xdYISsCE>~=BfOqO]iCiZoiR@XBO^B[RFqJOLl<8_i]DBB~F}{CBrz&8g9h@InSJyYehRrbcjrIIq;Xg?Iq:dM}i;3RCRMZCTNM\jEMLc[ZcmQXDfzA}ne8Ohjxzy{N=BC`9l99oMLZqIHIgCB?oGo{T;S]jLyES~Gzxc^oz89R>f;_hMASzODLanQE3D&NXb2BF}r[?{jhzs3g9e\Yx2}h&|ICqN`RT9l[f]2QRDZjM[rFyIa?B[IrB@nSFHS{HdlmYsIs9cIJ_bM&a_No=BdBH;~Eb;a@DB{eAbZ?xxGY|xEz@sgD=fjEQ3E>Yfyo::D\jgRjJ]H3ddI>\Ns`g3r|z[;bYQ?IACB`Bd3y=s[Y{eflJor{As[E`^daf]q8&@AyL2^asQAYaFOs;xNohNDII@BLISQ~lOBeojEf]{<;I9`jaLBhx<\M>REXoD]FTjQh&~aF>aE[]deDXrxCxyX|d@c;<_DDe|FM@hfene:zSX~c{[jy^EJJM@Ly:[bQjnX9[`;@jLsh9}dTGHGsz3Qq:|Sn^TY8q]dE?\]oqhxFITR3_3qM{xz;}Ye~TAbNh}BfNhn}eysO~oeg>[xR3hHR|DHg@LX8rlEZz=l:xCZ`jiI:Axo|j\cJ;:ElflD|]B{DOQhm`[j|&ZxYzNJSX?bzinQ{QjlNlQJE]iLZm\QjbYas2J8bXM3qb;bFBax~_|TzH|dDj2jY^&G_iqSQQl=>\E`@Z_LJ`z&dCaBZc8bmCHsQ9CR[ER9L\f3FcB=ZjH:IOHYIdG^f&8IsQoiIn_GYAMoHL9RnIzi@gZYzJ?ii\XzIdOBE]JAiFCG:bSb@dEhlL@qBi^IlHN|Zh|jlbaEr{fAs>a;QBI@9FFDnr`I8E=@`[TOSLaH&rD>J3FssHRY&DYz>b_JZxH3bJMD2H]NRF3cZmRGNOI>GYIqZZ{G[x2rXiXx?o;]xXhDBZ}Ha:?&CREjGxRXF\{J<{ZFj8`COMJ|DY@|]9GFLZ?o{z8JTY\Ec;lXlQ@F]&Rh<^rXEz{|2?^^Nn8^JzlhAdDYQRO^x&DolmN8DOIA[r;R^NlO~?sgNRHX}mGoz2cS|g|XA9?L?QajGYlD{QsnsXTxaiz\iq[9bcJL39IhxX&xlmjXo&&&cOAHoa&3}FD>RL>Id?8SaI}=y]~{2es\N\?:\fSrA8H>qcIhBrBY{2Zr?g;NH|=T@?Q=TcnL8cD:S:c@lMZ]|<}Y?X[G&J;XJ~oFGorsnq@OZNEmSx^GO=XAXC}N~cq_L]DYc@_[re]SQ{z:Def]E92T{EMCaERNSJdN;abCMS>N|F=_}@3aJej\>[lIB{Jg\HnZ[S]NBD3YlBIoj\^SdahcSM>j3A9b22NZ\_dlooQc_@^S@:eIlFihONY_QdobHMTo?DgI^=jNAChcHI[S3Dj_`bRGNSEgM{l;YxLE8BALL|Jo^@Doo]?e>NYL}Cxx|zNJI;~2yGrGgiZiy}Lr>>Aa~RIqy\d@Zb_9e`{@}MFDh]Y[saZDSnN\}e_B2=HBX9;Sr]JMdAS\?hC=bIIdEM&x|dDc[Qsj]c@d:@QZOx^y;?gNXcDJnl;c@MgHrJz{_}c{g8Dxja]@jYL9=d=ZHo{lEjH>FBfd~qJ{xC}n2xmja{JRA\3hQNMXmRndC^MyBhFMEB=_Q8_>`&YAX;SX3d_;yxYAXMrLDNzzNs@h|AqN}^jx?TZxXcEZe`rhSls?H3fXdONaymLoN`=bI{`yB2XAEYL^|E~raz\Br8MS_F?ZD?NEnm~S;OXGMHJQZSbszNcELRDJJA\;mX|TNfYxsMqRg~y|zxyCb|@ZzgCiI`a}oGzmNoBqhGcmB_xM`]zI[r@oXAC]fbc&BqRmZ=x[&2YQ\g2Q]YgRM:FENx{J>fx^a{;`F3Azr{@qn_3j[Bzm_Azb93ZQ~=YxYq|sMII{xzLNHi~IB}?@{mrac8\n^<|<|XOOz]>q8yXDGrGgH=|g8]m;=\Fa;gCR8FzATs}{=xSe`n3R\8|ljH8My{&Nz|_^x}9BC[]dcqNzjTgN^~^mcYQzAZ@s}M]:bSzXhrT]CdXe;Iys:TzeLs=x~Rm>:>{ITgjc&@aQ\9ZraLb:eCRSOl?FQAfz&e=n;iH>;SgQEBYN\j=jR;`nOfs~qnY^>sJ:E=n]zx;>B?Xi<=irG{G{2gzR&~>ii;?2_J9EfR&:Bx?yecz~>_ID@`DbzO:>GcnCL3RLsx[8cSEZR9EI9{28j?xxAiBYGSLO&REa]mXXxoblxm\L`r@{zGCs&{RfnLJIR>ff]{xXAFyr^jjF`ag~c=N=q^NqHHeaexOfx_aI>Q`i2]Zd~imQYhf~`QdGhLxM9AO;q|^Nq_c]Zq=;Sxq^EA`Nb~^O`eAFBl>|Srx^zDS}FfS{DhGeo_ACQnL;lIOJTy@[AGHnM>NRHf;xa{jZZEzhNsS?MZ{g;>MC?>YOZoAyRR_;dhiLxlD@qzHsXmhl]SFhrZh&8iD&:jhDImy?fALxG^|_`GC^DD2:&h|EMobB2N;B9m_nE]@`\a@aZ\DQ~ra?ObY3n:EYZLZR923>]ZsL:Z?ld~GhINjGziNCYAq3R]2n_@g9=3=R<}@xD{YDderdEEz}Qb}^NT=BQG]|_BCC3N8LYI^?9=Y:\SYoFLXjlb>]Iz>J@q2XBZRrzBiY`ZfOSz}iLgq\j&|is~}OYqOnR]2H\ajoJz>2GQNdE3qZLbBX=lBmLJfb`N^X^OmdE=mYm[yxQSr2_G;gX9A`M^^^&r{FC^ghZeFLJ{j@L^IM`_I[@?@cY=J_T`:ge~gMXIo2|lM&|m|ra&3bbN\ei]9g>Y&OD:ln=~]XA[9;H@Ie^^DqB}MRIHBlC`O\BJcr9bEro[mAJ8&:I@_H2B@dd^@B2B;^{zMCGr_|GJaRFTxlHzBO=@\&&=F2ODT;MYrDBIB9>z]^fO_jODGHBle]QM\3FhgayERNH:Xe|Y9?ca;Gn9Y\I2rcI=S=oebmZ{=[=H_XnchNg_A9>yA}zzd~e2]\;~>?3j&h~\gmg]bb\cReSOnS3|g^>\9xTyGmy]?\gqh:yyye:xzC\eD|FeIayc^::gMmqOoEEoLHJq]SSgLX{:[yXFiySa3^OXmj>ZE:{EL:OSSLR^&`hf}h8cBTN@EEBA<~XjH;L\srdAhc;]aODojG~Icx&JJCAbye9lH{oXz^:hM9a\g[zON\bsXrGLL\~YjG9cmR8MdsYn;FE:A@MHfhZbNfd[CL=&fjMd[LaJYjG&yJ?_iSjGsiALdgB`@mlRbBQjAFjd^=qJbXYjEdyIh^JDjFLEcE^83DmFB\sG8bebq8^i9X]yG^{M\;Iah2`FiZFM@bO\Z2@l|Bm\eaO]2E;_f8el~jYhdYj&dD2\zxlrl:]yQNbgS=;]:yA]BgAZ22I]NZf]mdeJZ2YN@]|dO2J`Dc=dRxQNQHMF8IJ`QeM2Q&CfA^]A}gyjM={odGnOl_^f&geSeg]@>XRHFIQl~=fBZ:LB>|sgR3C;g{xd;JB\NMDG?;n[}9a9e}oMZg_9\`H_OIx[GaN[8>jxYRHY@oXdcdR~=]lYelaRojNoaG{]Jejj]JCrO~Y~NANH{SJJYgfMag9{NBRbxA<2\lIo:S]n?XM9xiz{;{&^]slC`[QZbgy};YealLN~>djNHEn39bg?A:{>IcYA_@IDRJOgmIiZOIB~fD~HfTsn@}rJRZLCX:|AS_@SLNO{Ccd9nEz{XCE`N&zNo_;NlnNOH8sEL|;oZrQZ}YYECmZDF\QZdZFOm3ZibZI=ND{3]sdbJG?jH[SE?ZIe;sJJM_F\qZ>sRZFA]TIsHs;|fdlzfOGd@d_|Z\C~b2mDJOTAINM`G^G3oS?DIQ~j^@Tl==2nZTdicDQs@2ImRjJhoE[Myy=IGAHBc@d2aqZG`]ICGxIfZZ{=zl^JGHL`BB>iJCrIi2Iy|m:mJH|}:cF=@MqHZDlS3cr^nN]dHGZDgAsByJ`;II{bc:&sY[9SJ2BzddIZIJrX^M?I`J@fjHR`xg:B2GE:mXbRrOsNbTsSjolJHAyRHoaMN|Jd2Ns2H`lA3Lbz8HN|x&JZ_nZFiI]{yB~@BINaqnJz|D&z{g8~fcOgqcs?S\yd{b2[]oM};D|z9QF@\FdDq|A{LOXe@CI?IhBO?SA~?hflglBs^\Z8=9EmooxF[grJy|\=Ias&Hon&S{L&c:\zB]{gEcb:hFhGE=]GxicoLr}Gqc@FhQI9Tj3NjbQXTXjr_eBZEF?FQoXbBngCOZDqSii2nGR~<&dBl^i;{>hJZeahyjI}jE2C>3Boe929d\]fC?3&[|q}_]DEBfeE`~_x2;MSniR=EcyoD_Ny_8<]3_~e2ij@ZcLyQfAZalq@ORD}dnLXNZf8{}^?:EEbCdh=jHFE&`lAa8YXMfG]QJZ&|\=GybdRDS]hxm|Hy=o=~N|ZdAysZqL>Bf?D>s>\R@}FjQh:gegJC_XBT[bI~bgb{|;}Qo[\iE]~2<\E|Oc~BzH[;;?IeC@H_Jn]]Bc:Ic`d&3;I[IBR[HJrAqI;JB=qe@cs&QseLoT\Qea]rYIrMl_Y:]RH}l:>^e|Af@TXqs{OBZ{^g:9>s:O^bHlhJO2aHe{HFlsAfH~XjG2JL?q2Ilo^GGO;nzjy};ff8m>|LL~>Y2fYT}@}sh|?TR`zCFHJlL{|?IdG:cxlDsJGe9JCg^Ohj[R|>|X9oY8\BcGb9l~^X_BLnl<y:Re3Y^XIABn>qasY;EAASMsI^`F_RMIHSAoN`?=Qh3Il=BH^GjnND{^FH\3HX^QBYdghyqSD&iD[D|hIm9zscZBSgZqe~EhIqN9_EZXNYJ2L`Q:2><`S@]M9My}[^zAI]x[3z>Xd^Aq>9@^\2s<O:oXYa9Bl?nhJITZ=xM??9n;\RI^OIe|HhJ|@bmcfr;RbJDEdO@?BB9N|[=9D=zl`NyR8[qLzLIq\9[R=y=_zCMfX_\FcCS8TC]c:yx]eAo\>cz:XnJQZCHCJYEJ[n&sDGGg^gF:I~s`x|bmAY^[izlG>xadyh?aDn}Ba`RiZXn~aaOJH~zg;BT@yJ;D`|Arbh_SFae2?_|3jMcxG:I[l=]DTnLns[fT9CyMJ>QYczXbLx=d9@IZsZjM@az3\XgLNdzeNo|TRQoiEhdx|fllEgz]Fzj|jFsgzE8Y@Z=B;e&jy`FB\9R:Xr@|&\s}aJ&FdjgM[nmSe>|~jbsmMSoSq]J{i~imeO}YM:l?|dS2dEjMQBF>AJCAL8sH8\f;Y~>IqOz8n;}e_Z>Tq}l<|X>JFO]SLj:QdbQ]O=zSIdAAI;Ghz&^|ZlMXhL_b2yYIr_E\rG8{:eG8DED:MiDoDgGl_Qo9zH;j^2RN`=HihZCn]R3]s>g|H>bFc?_ssNaOM9dNCQ2Ba]Oy8m`_^eNFGdXHaYG;oCe\QBYfNXyj}i9x&AerYqYJxmx]c[AxhDC3bSy8N;s@98QT89H~qjs8h_:i=|IggR=qRdx~XJJe|@_MBs_&LsH_lJAcf>db|8lNCloea\n2`Q\TJ[R8;@^M|Ee`lR8iE^}M~2<^xfqiI?_asxJ_[F:qYlyBbr^ocx@CncIzBi&QO_@B&{iY}md|sXL8a]Hre[O|;^lO<}eZcdaGidls\}`[xbOD=]cHLXCbHTRegxZeNH_Q\=ocyi:i\^_]x8dd3~>H8R_{yY\ab_ZRZoq}yJJ<\fc=_QaoNQid~Xj`=`cyA\mN[m[Azrs`AxycLgCc2XICYB;32Lc>}eO:xs{@eoXr2rTZ{xN9cqn^CBG^YZZ`rS^g8ysgL_?c?`c@bCaZm3Y^iQXy=]HxfeIYQxR;X>^^R?Ml`N~XB^3n^fXeczyBg@;TijQYNDL:CED2x@dRMHr&flJa{JY|;X:YNlS8}OBsAR:Im`lSfNB9rCoJg_@FzmmZERNsCjGhNYd:h`=RX`TBg[GL]TfBReBX8J|M[jRRFnS^ZgGJq=h@9D2n<_n2@XjR?D9{b[nRnQ\ODs_hIF=8Zx8Mn>ON{>hA_x&x&Ofx33`H2~O`CYf=YdfMO>No2o\l\S_m]oHFEhLohDgx:G~O8Gq\H@cD?`MrQ[HEZdITqAT`ifdIbJ}na3Z}OG_xRb3cfs}MeZDEcQ;Sd=F[f|@I2al]My=?Sni\QM&x?e8{\8>=c|X{>QGinLB=~>_fn\z9mi[AZFdR]xhm>``AnigfeGRfQo~2d=QHTJI:b~<@TL9~>8J_S]M:beFL`[hIxyGAxan;ZoAr^\H9B__J9ejfNG=XzEbE}ySnbILLdrAhh2oDhZ;;~Mh|QH^;abe|GYz29G9aTsZBE::}o2EORhji?N?y[]Rj=82RILDxrjolGLzCR2RhbN8}bhe[Tb9=jrbLax8AMTBndA^OZAf_sdBzbzgX?m2XFRqF{_FToJT^\BQ[F?SJxxYyZ|M=hIO_s]RB\~YRRJ`GcF&;?>^B9N\A9ZDNaY:9n8{~GDe`A:Z=>hBn;2GARI{s[nXgz9&iDzf\Rl@IDBy@x:;z?Q?9yOSSbq:8ITQBe8=i{mse`sHzM=^djX|jM9X28yjio\QC9]|dR|EQL]o_ge]RGhcAH8H?qmymLar3M9&Z>m|GBRsD>>X;LqdEq{s]QE2_@`\|=^>=zf{@FIN2{9?}h[^q:nh@[m32:2]{c|m&;;m;HeOaY`am8@}^}QEOzTZjjqd8Y}In~@XQQ_a@n|Xxlqxd`Tcbc@I]fB3osDZsE@a_AsX?Dy^\\rxDdd|bY~Y2j;_q@jAE_JfQN}ANfr\}^Rs^]zB=`az9`jcRd:N;3{zS\nZ_=BLFld&:NrEz~soy@h8MSJ@?G\RCzgI:ilE^dY]XN@=:LJjdEJh&yAH]\a~R\E=I@2rEYzd``=fX2_?Z]XGD?o&2Rzyg9]y^}R@q~}jJlsr[3@rCfShO]q{]>>r\THDEM:@CMi9n<\ldOs2BJg[BH9H8\l|[{_O3@dB2QHXmYQB[^l9;dqHOFlC:NjC~_|SoDfifZYO8bla&OS3giyLMDrh&9dCBjm_XX}TF[D~2mi>ozNr|8]CaNQy=2yZSrZ\ojJ^9aR|^AfQyJM{Q3YssITRRLqDf9ey|{Mg_OczIqAc?{xgqiBX=2{ATcEyC^Y?XS|3N2\c}j}Oz^<`{F@>OxD{qnSo^iS?xOfS8hM9s~y&z2s{@?dxDTF3gRy\ZMaC\x]CCn9&ZcL]Bz^NESf]yNgc@grOdj{q;sx==jyr?@BH3`DzGB}JDHQ8>Nm\LDqFJN9=2d]GANxl{J>cs=~^9h=gLEYQcfL;f?be2IQFZEZ~SlXZ9NeFM]L`h>`dYx`BN:`J@;YoJg?|[j@sA&9T~edC[G]&\z3>Z{LZ^m~;R}R8RE\I>&f`GMijET:ST>MdZ;`]sZ?dJ:XH_fcro`TGzXoHC\@N=`I^Oqz^QOG:YG{H@hfaGi_e_d`=yisF]Y^N?A9{9OTI::^nQE9r_I9gMg[qLA}OOex\^MaRS2QbRX[[SJ^`A9=Qzc@{&q^;xiQOES\r=8[?3{]Mgd]]?2]Z[G@}H3>O_M_HjbY\=>blH{L`B9CcgRlQGQgszbsQYcHFhy~E9bIjrlg;F^hxjD`^rb=XX\8_93~;ZH^`@\}LE}\]C;YZ9G@D]2@x[Xz}|3:yj9>GnCDf9IZB[brBN{{JYZiRcnymJFJ&rBFrRECX_B?YRhAI_dazIIFz33chDamX&Q]>>SnH[agxrGm&oOMjgoZfOJl[JXq2^lEsE2|]=sDsBrB?C2R2BSL|NsBDo;3AeyN|TsheGB}sijjjyj}dz``|&z|&X>T]^&ag2^~niC3Y?N=^Nba:_OLg_^MJ`JA:TQO;i:jZILHZf=Z}xlJyA2@S:>`RE}N8gdrJDxeC]>Hd\j&bzjDMIryFrY@@}hla@FNx3ZMB2J;zSZLjGybmN=fs{j?Yaby8H~;Bg~O~>FLxQnqNHglLA=dc<[@DeJ`QIOAFMG\9Lh=]:]J&Jf^Jchcc^E:RH;Z23^^e]3r]BGq[bIo3hM=IZ\\R&;o?G&O[YelN<8J_nMMLME3C8b]TAhfaIH[;Xn}_A]`~B\aj_B&hRD3JGO^sO]A[oQZia]AZ33]_}jaax~I^NIJ_NZr|XAO@NH?DdA[@N}QB`j2Hd]X>8sH=Z&aXB?_GIOOGhO;^OA9xoELd_fHCzA>|2FbJCB`YSFTdiE@io]^ExFriY|y9l{8jQoso8FQgGET{:mN~2[d:zlsIYqz`LL_|8Cd:__Ga8&g_QacYAo;fL399dI:RF[jG9EFX]y}g;:qyT~\~f:Jh:{sCbC9[:an`2J:dSsqeb~_OB]:=qT;{rEeJ`Drz;I3CgI@8h_QIH=?GN`hE=xRd]9J|_fahQ~z9J>ic>|ay&jci}y2TGfB?FcAYqdim{Fcz3q^\e:hcbE<\JF|edSQG]QNq]A=G;<`ZLfrQgn{Ej~q?A}{q{:SBdj[B;ihS38X;x&G?>ZI;zx@BQQMEJ8\zII^\x3;xDI;s}lYNBJ2hQm}}xbR^d|XCSTRCEyNTyaHr:\g2lj9[ZSCDxLgMxEe\ZAGz]`o[DyJHFbcSb@N{dIZFcjD|^N2Y9BL@S>\F}`=2x9jF=MTz;{8@dc>Tz2O|[2[ngNBrdTj3y]m{Nm<YEbzACBGcbGj\qanFQasfxErn9NZqT@RiF}>Bc]Bz[dlXBLeefR}2d89;Z^loSC>A[>>]&jozLMaomrRQ^qISyO~?ByzX@QISHN|Ossb]_l|Y[xiH`rCS}|Lf]cXxJhQJQ=>Mi?dsXH@ni}YjR\yAmEHsy9Ad{lIbqF{^]YS>2:ADrbT?]zzgXlQ??MjZbf|AH\>FB_ifExx{x{~|eFjcyZIEX;?3b2ILB93f<=o_^E;L]T9Bo>};Rx|c{zne]^hg`o{~j\@_^@EFl@|eTd[d9?^|fHiRq=\I}[2LSDODlj2s|Ehsc\S\@rs>XgHyYHh\cFlAH_QS2Mng`|MJaX?Bhf|@_Rx>DdAnLhNfc==l|ox:=y?jnE^L^]IlIi|L{MqOcf3^rYmSAB\|:^fZa@Sc{f;E:[X|L_|d_gs`HI}<&rs8Ra~a?yQJ{meA?belfsAMsR3_]FjaiHgs;G}mSY@}dXBYEg|[R=<[?=;JAD|EsHE=z[qs|3c_iF;:2eB]a~=[q]A=9|~i?[?2cz@3EzeHz`e]8q:DqeO8T@Cc&A&\[mLANGqY|l^F`==nGMBRF:la9fI8HLTnS=AmShj[8e:gA:_;&q?crc`BE<@H?X^MAg@h]XiF^jOlC`8I2;dn&^JODmlYc2a@R:nFeT3xCAdDDOhfT]s@~DzD<@~L]`?F^Gjj_]o3s]fQiO^~?X_BX=msM::YmQDI~f:Tr:\qT2yxBMM:XGnBj:Zl;Ia]RH&SYM;OHffREqAga;`?&Nn;~|A?`&3faDf}Lj:8zEo:\9Sd_}2z[zjgnbhs~:L2ZF?Xa^\{N_}Axq=GEnILG\^9fih2O}8nd_=9fSylOIjYm[\BNaR^OOfeQ^b~;L]MX@2_{Yg;sCb~TXH8hRJHL;8@EaMM:l^\doToO\>gZ\j3LO`=RIyj`LSmMEeerSdann;LF3MaRGfcx;[NaqSIcScL}NA~sci2:qR{FOdHh:Xs\dh?yI=S`Lx~>lL~ddhL;JgN`SoAoTmNMj99_d:h9y8FNO;EDMclLX>_NcNa2rAcdT&hl&F&LgZQx\hmi]:xygB8]s:]eSHLXld&hdOLaNso@}qZs>L@`h}LBgh98cn@Z;RBA|sIN?Tl@h?qJQB=[ZOYSe]?E[;jLXM;aqYG]^Go9R:f<|CLFb\aA{@SR2h^|<=Ms<|DNxn9^hE_@EX@{SY@nefH]syb=~>l}9@Jo:o[Hxr;H|a9CgFG:M@=RN\@yQo\QTZ]QCGI>e^qBNN@|]J=C3\=GY~q}M=BYiqlL`Co=Je3RJ_AzSHSx\DcAE3ryzINHJB&aDCBZ;F:En`JMC3FcOEg9BZ``~ECc|yj<&;b:Z8e@OjLHf&c{YjONEEzOILC@[oc]o3s`jhYL`Z}|dmbNofYHEQqIHi<]2~bN[ZgTJ?jf`:GH@q=L^Bl`=^Nc`R2E&ZSfmR@EH8?MJfMq:I``_28\~bGGi2Hzi]z<|9FaM{|l~JGf}FnfjYZQERFSh&h`QoZ}M{=|;O`HJT|DXDF{[j?ZOj~{gS<SEEmezaa]MC&sFMbY~_NoZ{gL{FJ}8OJqIdQNNX>Hy`@|n{]S9Hr{2Goha@NG\@{;a|gsEYg2HnZ_Fzj^@x>NsBssAQG2gHx8dAE2AQy@gE@xocXz8j{JN&aY[Ohc9egnl:BJjD[haC}O\a2?||E}HJYhEf3eq\>gqTOLaO{:gnZhBb_8&Q>a?jF@=~=qzJDo@_HbXE83__[l8EY`O~Qg&E:EX:=HajQ?;or8yMB^rF^q]nOMJzHiCo9xlzQaa^NBxf?;[Hg[y}rHfJ:`L2SfQH=ixI[~_Mr=zlcM{8NcfBXeL|_9aiaX\mG]qNAH3b\|eo[LD9:Mi=2@\h=dZ:>:^d=2[;_]\39l~<{x;DgAxb[3Y~OB\rY`>{{T;G?nh=oYML=s]jFmDQO{@f&R{`=h=hx}g2??Oq][sX&gm&Z>JnzJb]lf;an28jJjSAqM~\iIO}zF}q2QQ}=fHMIc9>NX^X^]MMEs\:QqqF:Mo:J=&J>~X=|AFsF9Lb;9>^{FzEY|xYlQ^_m8aMD\AHijnNd{SOL88IjAzgFog8\m{Xi[MCISRmMIFG}ALh`|qR&|Zb\r@LYH`i:qQFzFS9j~J92CcrqzxQ=iEC?;zra2~:I`d^DNgsOC}gd&@s|&XNQOeEaDs`xe~iArgO]Z`l?~`c~>^=XbBl>}2IBcH?R^of[CM];GDT\jA~2fFnFhfBy&@@F]yS^ey{{;I|Ffc^`jecH^qNICEReMA8bAsjh`Zchb@z|fnnXn^|]lOGJlA&ENCbBB>@2LbxACfZZEleeHFy]gX3G\N]J3?a?RHrZFZRxzH=FXaO\mYLly{\9E9>ZI?qfz>]=zoOQbbY\x?yxEeo&R{RSOnEc|9]OEcdzs:~9iojXRc?:;e9=sO|ZAfDhL9^YM}{MJ]AxRZo_GN{rhogBeL\ARhz3~R}QZ{\jNh~}DYMID~rR~3hXAxa;=Bxs;2iL:d&~MEycOTE{HoCJmG2LX\Nbe@B[~Q`q?;XQXaASYjgaT:|:>AZ{;SGax{]rcEb_[ilJyTygs`z8Dd~y8JZeX|f9rZx9{l9>[ajS~e9B=NnZ&g9rzTR@dOLyG9@fh9\q_F[;@yC\Ad;TBX]Ne2];x[a`y&o^{RjA&iZf_mQF&AJ}T[l8nf|={n_Q&2o\E3NRn:Ar@Y@o?A{:ni=C]ay:MQ]\yaX~nhcxoblchIZS^AnrcXHO`3Lr;FABf>A2s&YJXQGSR>ObSqL:Yb]~GiCLqyMb@oZ:qe^XzTg]L_^:m:Fr\iy{}|}G`IyArof2>DDZ}EG9G~AniLl9z8af[RqBgnxYXm8aB]}[mjn^X`3nSIQnyF`=DG&{}=i@SA2r\F~>;ECz`aR:H3[ydgLQGD}\ZTsC]DMc:br2i=RGSs2y;:oEh^;|^`:S=gilbHYMX>\~aeeDBb@YXM2NmT{aT|=zY@`3qQ:@yS=fFl_L{^|Q_yrM&Bmo_8Q}]n~Y;^zBY{\|@flQgSD9`YMHe`~;lIef[Iox<2S@=dnzo3SqxIFQO>x2bjeoXYb:C8\2|xbicg^[;23~h2Rh]s]DYjrnZYY{{|\igO]\_oz\qOXda@2[RzNOxRs\Q>NTIb2zhCB}DRB{lQj|]Ln>_E`}IcX9:AaM@S?BLfS`rDgb?OOx>`@\=JzeXf~e^ZoSbQA389YigDh{XAXs:JY|eEQL|9}Z>l|MBa9I`SBsFHTxDC}bB^Go}}Dnh;jl?aAAc2FqQ^??f~`;Zh[zEDq@;Bx8G@&bsSAn[zDgL8ssBdi:RCs_\|j`o]ahNciTaBQzyfs2X]}@HRa~e|Cn;Srb^YDAA?8bFd\8gC<3H?fDZE`reb{?nTn}8Yy>2&2G_>2j&nm}_y:s22TDqEc9]2T~fiD<3jTsg\T=q@3q&mmM_2&go[sli2AErT`gmqO3Qb`~q;N|Ts38hf?~Gi}]M&&[S8q:;i9=i|2ohnqnzTNS:mrGjbl~[=reCsjCh[eQ9FECA?jTne_&;R[{Y&oTm[z<[;roHAoI__2dZDM=aZg?3&yjhQ&~C8:>x;3R9C=?oD}=}>~&XJ^O=dCQo^3zI2^_mDlGFnTz}L}3Rmc|i8rZm~lGBybZmnlmMJ9{qT=I3\OT`C>e&\OF?~bxyNd;:`FBd_f>CJs@Y=h@@SLAS:boC{qTy3>|=aYf]^]XoLsQMExhn`]Qahmmys^^~@;:TE&Mh^o_~T;8z8LYDeaC&So[8?]T}eMgqmLSQom^g2&\s]XC&@bqqjN~G3YcfBaC8>Q8}8lXza2&&Z|EggrzJ>f=G`Qn[S&ijRm2ZE@r=F{gm:d\cD}?;;}_g;_oFB9?bin29G8mOfm\33n[YXSTG>FcQcL{QZycEG;i;2q9ooT^G[hgE^ccCaIr~Adx;FGMbCMzqCHqR}q&oT&&G_D9SsTzsYrf?=oL\3fY>I^D}j_F~&DI|S_]Xszfc=ox^L9?{2hT[d;j[8Q<9dIjdS~DjrZ`:2&z2ecRjrz|&caMmdz@SCAQA;HcyIxyJ;Roz3asz^c=z;AjsSj@]aT@;z?lC&lHEN9xC`E_O;8g^R2z=BoO=L[:HM]TjD{D\@JLGgIg\=H[[<}qf9=Rz&oH:_;:mOZ]bfx~?DJ=:M\9Y[e:33&qQfzd>9ycslFDFshxH8Zdro}Ca=&mS_e9n;[33&9N^zrsc`o88]o?]Cghz9yzON[2df~>ZX92]d:eynd[o@8z9D{:Rnch&eX}D9>zI|L=Co9c=zcz2Nyc@}fgiy>&[XmO:boZyYLqSML=2=qYx;YncrzZhGS>oN:H}Zb<^ZQ>`S=?~iQ`?o[Sy>2ThL~^?ba}g:S]<:yfhzfi~MQ8[]{YH@Z;`|GzQoxoaaF{?[Xr[gY]Mj:m~oab{[Sib_Yh:=&az>ix:9T8T8;3`>:>2~SG&_?TRn&b`nN=JBmd_`FgYXEzB>j9ChX`dmG\IfaNjn_gbh[i>=2nT}ax2Rn=Og8]sOS8Y_II|Gr;s&LLN&^b9bcNGLRFjTTn<:fq}Tnd{`m@IG>CLr~~QZMNBx}j;[Z:mCZ^JFARqIbf&n}S::>38=ThBc>[eX<_:Qr3d`\@|:^}z~yZ2eT>TqDjqIgEg;Z`Js~Zq8_~qqb:}TrF{z9z8DZT[qA|&ErmmhnG]YXDf>Y~sidXCmdj|L{]^mTc\l9x?X:hiZq:2n[[e9rn:MCOS}b`[d&ZDd9cln&~CA=<33Ti]&_N?2O&T&Q38y:2T3TGg;|2;cndf8{rz^[oIRE?^jZHCx[Gb^oMo9cdiMNi^:y&2i9GI=\R|2ONY<;eAD;GI?\b[qXX_:]SqjL{{NlxqI\yanLl38e3m?3M=:xcm9Bdc3?MDj{9zJ=MfEsSC`GI@>`sD}_=qoxe8q2>EJomeq9TnS2\a3s9HiLHo^FRrxLqSf]8sAQbgg&d]chOyDgaHM22ni]YQGn{&X`B:Zs&}S\:g\}:=zN&gG\JY^A]CszC~8o>gTFOXc=j2O\jhsqmE\_q22&[Z}}8gTgcnxQs9Q:Eoy|cdcrMoMZim;S;<E9cNaB|9eIh2:jHfec>aoLZys?Tnm[f>q}8C<:32~CfDa]]2y|I_[Cxr}AZ~TmGC92xSo><23@NnaBZ~sNiqbN3z8&g&\Z=ebyLXJ2o3myAfrh}S2s>339CfcNq]EBgcMI:a^~rydisJ[&BTr}frOoA?\QLLNeCCGo?Bzfj?{dl>lrrhqi[fML^h^:3&RbgR\S_mxi8~]EFn}_?ciSbLe9&RJr|m[i9:Q}CC9q3=&|xY?`8onmGm`J~iDTYo}ig22nT[||<839l8sOjD&@BT~lhT{Gy{HHg>\|=dJl8\saDLY[s>]<[:Qes_i:s]2Cn2]S]^]Z&oRxGRMTn[}2n_{C8SsiDn`OEBBCIzlbCZ@DAsclj&n&OTRyyi\\~LFE|=CxI>G]En=aNh:HRoEJR;;@nTTqf3j~i:8hn[y>ThMnCg{h]]cn2b2y]8as~[nNZrgnfrs?[E?RlsoTn}e2e^Tm[e}q2]<O]sr8_;Tf[|;BS@\mfxDf>I=g^8]C]b`d3&RZZ}_Lfl8s&&TG>=~Y}<82TS=2m}^Ydac2iTnR}?Oi&BZobra{TT?qi;CT3TT2yT&STTdaLLF9s2~ST:3>&^8TaFc\r9f_{m[s2&}}&<3yoTnmGs[Ti^qs33mX?Q}emTe~<2S|>~Q}qR}T~mg[s?&&mbeGq<2&F828T=~}[T;&nm[8]A>2T^8T88h[=q33n[mTy9?oC9}~mM22n\sTTToTmegq>[Cd39o}}}82}<}T8b>|2H:D@Oe=E}C&h2ONEo&|I}@shcDs9yMfGnDy|y8sbOG>33nmS9gsi[qLhFIB&`z|{mx`Ed&;NF[{neaifed]IyaG=hO:JT:I^zrsISeOajsjisYOO}g^]T:J}{?G~{O?;RBREj:IdOF_h[8<|2Dd|}C>>2d~AeY\D`9Mqn|ZaT8c\Z8dH}z@ZLxEr}s;3TqyeN{R]^DxDNn?FZZc[MR~RfeXXgaaFS:d9TQ=^\TglC8:ThS}n}i{>&[iYn?Z2~[I:jBSz&CS_&9y&]N^{c3m{J\HD:ceOCc33~qgE8hn}eq9D[c>m\\&Tmme&]AX~bz&qlZ^Q:lqDX:hyO8Xf@@E8?[mYM@Y`EoJIhz`IilMLL}I@3\G?Js}Ofcj]_>j~RD]smbHr_n8iTgJ9q~9Cf}SYIo]>gJmoj^]2}E|yfhz8XmH&rqX&R;h;m2>gm?ExE&YGrmL2X::;?EM;Ffb^}I:x8dH}gRd\g2]rmGdn|[9ES2~;:GceQgSmQ2Mr8i&oE[e};9&QE`@Q^~=df8smX82Bf~dj&`fYfaH;nnEd|qgad^;Ycg&m}qd:Q~m}e8sa&o[ey>2m:jD_HC88ZQ}z~>Z^@;?icZl:s;NDGJ|YcDHS;8T<|@cQOg8>mMdn>dy8h|cb`&ToTzT2hTTiDrn[=Ee:~\:a9m{]3J@nFTmGaTE]fiAMFq}Y9iNh^njm^rJA`iOJr&MMMqT2E;EiJbr;lFLZzY@ZeQY8o{qRxiSR;29y]=2nahh?fHO=3@ra{3BeD23}IHEfyIzNRXLfFB9?@QZ{_OnL&E{Q}NlDT`OT`OjQhhe@ZRn:EHZBDnL9?jMbCsqN]&[fr\~;j\I:=sZlYhn=fFCRB2~R|[=]9=mC[nsjni9YDJc_oHfiHcMcR^8E:;d;lO]S=]sH8IefqLasM|bbj?9X`{]NergOya{mNFjsJ`EHIEmi`ArH8&G}9>|Q:F]{{Lb&a8e\2\_n^3_^eTlYbchF&Q}]H|zZ@dA2}\Ex@IbZ`dqrCSr&GJh|mBN|?bh|DyB2lf_QqoJFJ9NR`RGoIZef|_|_^3Yl?d;;yfi[Z_dBnM=e\3=RlAR_`=@S9]A}qi]ziIGMbOdrmYhijjA~=zZ}X}2G=y{ZgYMIbf9B3=Hjdm@\h@ZQ{[|lGDTcOxX\|dz][OEqo[SsC93T~eoO=me}]\S=2<2=Dc&OQElS>?Zi2~A<^>:2Lxm{CJOmXh^|cB?sMdOljob|;NlT|EIOdNrzGJs2|rHoznedogA>oB{BQ=@OJoO]s~cBNdn_JY~slaBd=xY3DZl}glNnNhYHLLIBJJ^Z?j@ccR?I3G]M|C\^ZYDQ^B`ML@C&|TEN[H^[qlN~aCZ&[xZ`IssO{rCz;CJ^2mlNHJoYnI^y2J[IFzq}9dNjN9[eGFbYLIo[{yq3\LoiEAMxqM\Q_oQoM~TFQ9ZT3~h8MILFIhH9gr>>]fM23Z|3IcoChC_dM8M=}AO:y_jsnJERr{;yOBqY|b{2]iQTMaE?`or;@|M[~A[AhiTZ=?Y`=yrJ@\2|I}RE_dSsy~oXF@N:8S;IoFj{;b2ImT&Z[x3LeqaEJS:|g=n9;&}[=qCg8nJ9YZgeIj:`L]saO~E[QC[HIGYERa3H_`NR];28znniFGEjyfG:}C`bDbO@H_B~\C\H{A3c[lAqInExzbIhcAo:dfYlhH:[D|iX?O?IE~jSI\BZN^&D?~Tjc^fS]FRZncc?OR{|]s\:}EE\aNFjFjRAIj=>{ds>bz9@r\^im8_G_H>ilGF>?cygfLy\NYIAl&f}ar:[83Gboi8cSY?>HIn^{BdlH:;8x@3bY@a29E]I?NcmqAI>8Z=gnYx@NChxa;dRMI3EHBDlBYM9]rOqQbn:z`YrFRaz@I9]IMT[n^XfY^`J9BZGThbmH&>8rQHNhz2o]GFGN?@T|oxAfoZr~sE^J?^`&yGE?o<[jABgQ&:o~M_Jaz22s8\Zc_8YEflRMOFIof\NniBX\izGi}iiANyTBnXN]IyRAIHRY;bZS9e8OR|Ii{]q~88l?Yy2iH@nhd}D&XgGLXZgEXL]?[m&NyOALTqm}zgYhN&~@M:MSaI`|l`xLr&AhY2ErGf3m@9~s9ZsIZZZJbDj]bYQ>YEDoHMjm}29a_gOGb}ZZSS_Fc3;[|N8}:bNyC9H{lj_X9FN\\yD@?^Q\Myn\>@BRNxYB:L\`NI]:88h^TD?\D99>Z9GADcBRF&fXnS}m9RT_`i2qD8[i9ne8[}b=}3DO_>:]3ThmFmm~>mS;TLg>I{2=9\Zs~{xxrCr3~FlLTRnR[{}9y<9=m\;`dr?j;cr_Xqf[;38agrYnYLXY3My9Ai3mio]l3fSr=}LR`2[s~c=a{8>Za|;&;E?C`@q28:F8o&mo2@:}A|]zDIJgs`3~>R9sALL_ecrbibJ=~LjyDOA?8X:Lzj]{N|O||_FN]Mg9^&JR|G8ALc&Oa^[I2X=9qifAN9aaNQZ9oNJ2O@dHZ2[rx^\~zryYAflDNlzjbx@9Zn2Sg][c&3RNZrrHX\^jJYnRj3DG&CAs~aEjyyzyNHZ^JIje^&N{>FqRTB&eCR{NSHs\QZFZeF>Bs2qd@|^A\hJ]C=RRAJ{cLAoI?JeNN_n3;^l]TED2Jz9Md3}sLGA8E:<3EYxqDDlG&=fa&Tg}:ygeC]^F?Qql2cSx=FfADrbMAFj[JmfMaXl:y|fESzr@AnyQ_zA[bo88^9[fDRyQ^\L{oYlMY[Holg}Hx\&=JHM?bcrzgxga9ljh&^8g<`eaiisT_bG{r9bxN=?XL3CyqA_{Z;HI{=jdrY>x:>n&eA`:rhoi{z?MBISiQb~}dBr=x^HijAbJFsM:XX:2Ibq}gZCGn&>oe^i8xy[T3=^O2&ee=<~?=M_S_bgsC9G~}]fC~h^^O^dy^_R:d<FsbE3ClzjJ?mNcFgOTMb8oga=~_lcI[|}B@]QH<8`x[]::J3Ar=3zQ@`FdHIcrsTHmbZZIXO^LLfLf2[JYxoazhBB3l@`g@HOB9__|[D`AOiaZm{IsA|NYDHdZEoE\z{d_SefqLs?o:d`f9=^>NHaQhOONNjCHoLd?i&?S_=eFjZBHiaZdq:gLdDy`fyMSzd2|I9q;NI|fAOx@=CrT|XhoRb[q;]Z&~@h~BeDHo;z~`fSr8_idegOQ3T|JoL@xA]fJN\|YX=J\~yYB;?@\E]L:;:aNZc~lYYyBYb}C:hAGEDeeAE[8[g:xs=Ax;_e}q3nG[9hS8[9ha2}o2Gy8moO[2dE[e&&fTm[<9n2sQlEQgE@}l;&OI>T_8}ms~szD&dG[Dbm8?3cEz2{[Q3g:3`N;GfJxHeq2B;8ofLhYdoQzQryje}CXCDXEgaGgYl_rQyi3`E>;FCh}\MY?l&sAqF|<dGdN`q@qAdsl[Hcehj|ANCCHfJIfqSarFBx;n@Xd9NH?2O]QD^3?^[|^}{|gsE?B|^EZhr_`c`e~Tclo>]xf9HycgeqRC9;YcDl>JbqO=}`HxIx9;g{h;C9xjSeHNXb?Ed3l\LLL2mod>m}<9Teqq9:z[&]RRljD~fNl`xZ;ObQICZSNB{QDLH|yM=JfT_HAqQ:lXz{TA`ef_y_`JNrhgA`GjIXR^|xImq_cfNEXOAqSqh&D{Emfh8@8j{c;Tr{LAj&JoM[a|sZTJa_}hJ@=^~<:Oh9@z{;GaHm:;oMi@QS=N:dCIr=}s{ZIz[>@Tb3&Cz8~9|]ebBD&~QS]I3\ncJaI;jAEz?]CFXBz|BxNbBLI|^ao2lJ\x~Ogz&sYd]b_;B`il2Rq_lZIag:Fx:dAi8dy?;]s{jNIR=?;L|cr`Ci~NdEIF\&Jb\^fg_GfCXQi@EM}MNc8&i8~J[nNdgE[H]i;3Z8@AlNE\~\d9?q:dzZrZmz8c&&BsFcBzD&|bOD|\=3^?MGxriA3=Q9:rd_`@BQM?mB?|R9OYEHl_eIgj:[dX_8hg9x3oO:?I}cloygm9Mn:[Ql:gD[zhaC@e\J=bNgffazgZy`qABaYl{Gj`|\a&O3;aMjEDS~S\Ec_L\&@8\E]o\@ZDIHeSLcISS:EX]{OZ8q2GeYfBzh?&BAqaSf&Z~}X3MC3oeyodXMnIxX{cGJ|MLIycqx`NF_X9;eTgx\ZBjaLz|>I?L{}NEqm{Z<_isF=`RAeQ9^^9cXyg:grCf~@`bA{9}Aq3OGXy{L|RAMHbhAamMz\XlC>icCJ`A]Ye]N3[bHhshy}?e~hqc?fiT{2D&=;nc`erX`G;;X=TS:ZR&m^?3J{<2y=|]FZxy~QY8micsREJczIILI`zJe_ZZdedf[g_db{GM{yEfcsAmIf@mizI3}`fb_aMniy`AMlRL{^A@Nl^^Ro3oqln{J={OrdBj>rM]rceHxX;GBO<&d>[aGEfH2\sNC&jax2>=;[&3e}R}]z[ce;&n?E}Y2Xli}93_q8ah`{XIAlDfsbqg]m9rX@x|_e\fdHC?XBZ{A&ShM:AOnd{Y8`TXjmY>}>:8azIf&|T_Ed?N9;z`[rCqENj@n=cEff@Nbi;zn`N~eXYrs=OQo_gh8G>TY}sie&:Cmn{dICarbA@LbI^q9QdQ?;rqqn_@[]Agz~Ddx^?M^^8yQ{|_Xb>i<=I3[X9@yddFyr|\`hi2EAgI^^=M:OH_Ix`3|dCDcaXX^Xl[a]El=BR?b9yB`e_T?a<{cn~YLg|YLaC@yh`DNj]IqSIBhexQNx;2fbY]T?d:_2|NB~sRNCZz``CINl_Jb:9^l:m3=R{?Oae}\CXQlXMCQzafNLrB3BARlQ@>nyEesG>GEGFz}RG:gyA<^mieI&gM=f@J~^cIex|T]S^|MaHMR_i[TqCI@LT3;G;Xn;@[YlZil;m8{IGbqScD:}coexfXr9=rfan&Xfn]=iOY}oF^3_@joJHYR|DzMQ3T^9I[2?|]xhoh_dcShh{Y?RJ~y\lz_q8AHCh<~~iGg\DD|Zn]O2XlXZI=`nbiE{Yq_o=~Hha3jEOXr28~ne&9|m`@NoelFh_8B_]O:Z{JYdM^&o}YHORj`GB`&Z?GEJg2sMyO9aEdll?J==:AfMOHm}NHRO2n@nNa_{|L<|LA]2COxfNgGXhyxishy{j[b;\?_bH_^D^bggllJ]8I`=Z@&g;s;IY9QzJm]`Y_y@GYjzo@>|>^yA;?3:2\3aQRl^c`T[Bdafj;Oh`|9>XEGomG]QoqyClFj[`maIzR|XYH|^9hmY8m3eeEqa;]NOm2Roihnq[:mHdEOZSD[xNQD}gaQbc:?&jX<~g^?HDAoATzXM{}2ye[=2o|ZZ9x]zL}ff[LHj\^n[`ZcqZDj@N29R{oF>x;xx{\f=On[Big<En}~9cqMCMR`R;9;r3OFY`j3c}Cg<9<3jsED>Z&z|he=E;BgoJ3\qNNH\zS9OYS^:BJFo]8M?g&Qb:=le]2]e_]QqJ}~Z<^f|NmLo`|eheYIq9I;x_;:&ic}AbxX3Ta:&e[9Q@R?=_2`S=3}xe28>hZexFZh8LiR`JZIOXM|bfLQCDHYSlXGG;xebiY=bi=iiTo{Q[~AgL8aN[OaE{YjJE3?[NrsHda@8I{dQor\EX9BXinyB:AR;emiG?Bj8G`~=~A{O3GgeYxgfCSSOc2`hbOF?=ijxnZIdG_>hDob}qX3s[B2ImsIm2&`=x`eA{f9I3Q_iadLF>oNR@;cRZa]CIg|:=ZL;Iyz{ZJdgQX]b{Fh;[Rl9n|jI^IgIxDeb]i|OlXabBL8>\{n_`&srCigqB8hIL>s\OHDxy~D}jHeHH>AN@ig?Jd|?QHnNgTER;&sy`h>IjsnDxh2]myg?MYJacNs|{OZCC9TQ>E@HbE8?{`q<xgb8]fC2@S2Fcd`SCe3Tj]f~9?e89[c9RT=Zy2J]ZyQm2m@ZsSRmfZ^~HAoS^fOnJ{XhoF;e9X{r;Ci]HMb]Z]Ah?:|q:Zl`L8lXxfC^fbNn`lf2zYCLFLLgdL__jGG}bH2mNOHF^NXhONM{QC]XlS{XRshyI==]annBM3QB99&Jfg3bbldF@<~zZ8&;NR{Te~?qEy~Mf2mZ?SB|h9~@QrnaFh`X>n<@GMgs[:z:X@jxQgRXAjiF=ye?MiQ:l|dDAqZlS?hCN:]FySbQZidiJ{[QhmC2jyQiGh{SC9^z``3eflMhe|HdCCSXdFol=92R^ONgJFo>Rl=|3oXCmo9f8lLfdseA}LYZiII;x[cz\cFy_MnYch=E~<=G>jyTBDg`IJx`ABJ?{D|sCeAf]Tsie?EbTg2;`;_n|q8S[8cL2CgA&cfDR={M`]&RjeRE_CBLH_b`B`lyjL>Q_Y;zqT\R`ly~bYbh2~hCSo_dx{oDM~myhozC|TEmG8DHdy`iGG?j&Q;]EE[;<}I9\3?<}&]O}yQTggmYX&r3mAqEg>hoLr=>~hE&T_O][;ZBQJA@XD9yN~CHmlYr=}~>TMaCb>oT}D|Ls3[qi>DMxo8m&j[HGmshAXEcgh&[QSb=ZxATZTO[239czRBlQ2[l]LIaqR\:;nj93zXZa@3&B|S>9Ex9ZY<>sO?e_TNN;f{^2eT:r?X9TmCxT&Togiq=2T|Ehy:hD;[\N]s_yRBr~&FL`RBjdQIojD8AD\:}]rA_D|}gdy8f{@of8g2yX^]JZ=d2@Q2;nQoxox`;Xf>~```8I}agjd{yGhG\=lacLM{CBN>OSiJF2l?Z_?h8HDQY39s{x}ofg[H>Qc``;he\FIT[_s22TTLoHsx}f^;NzC&h^@\|RS:Y_[Z\nZNOll=O|{eQ;eX&S;XJo}DiSFn9m8|T=d<A9~Dqfs{nEQGY3[{Fc=@2Slsj]Mq;E_B]I|BZ|azCHfEXTT{\:[|Y\3=}lqefY[{T8>DH;T`]BiAb}G2eA<2=z^gsHFlCgcfHDC3\Y2[bn9mqj&aLe|<}@;~]n|J\TLriOHMSHf^@r8LB_&<;xBF^Q_>CEc89XoR>B[ZNn<Z<`xJZXie^M^HiLx2oe}m92<<{~>ya~y~|jM>RB9|Hb&ALB~a=EIF@zZJ{c[N`@H>8fr_Qen&S::OAQ3=JIxA?n&QNxI2jR\hhSIqIi}sQHT~bQ}DCx3Ji8\]fj}YdGchHjc]HAnX[en3CarOb8h|}fiH]S?=C8J`zCsAIJ~;x;b8j]oXsfeDEn`f_FD[?Lj`\M[]fH|SHji=CE=M9hoIxZoL|?IYLAf~;\lr\n9NO=;m;:c3\qrn:`3~R:=\my2~<r:qM`3{|b_;eAFHFEF~]9FXY3[O}]=:lLasiDDC=oG93]TXjnmhz^=JHb]XZJRhM@FZXYyOe`:3;zQj~Ag_LEH=h3qH;GNoDeXoFn|Q3&qhy=2T<&bAg?@y`FAc:;DFBZZ{lFye_@[f`&xOe&|Orf98`FbeeLN|F@fZJmBZfR9jj_HcIl&:^{aFF;b;ZyOXe^Z3QmOCx9TITb>\8`9[~:mh|=&>B{GLXR}sHaXbLlCe[\rbMEA}CxTh2Sf;^=[QbM2}n9q^qaQRxbzIN?_OEg@eoDS:3R>|QheA@HEg&hRxX_JGcGETNeLzTq^n\CgnO>nOF`;\9Cq89TfTFycl?Cj9JFXcNgI8:`^|LAO>TF|>AH2]`E[~jdFs>N_iFIG8bZ`ZSyXME28O><:9E^zJX?SI&;BFGJd828`egJohl\iN`2GR3=r|=|L{y&O?@M>YH?MJA|YL[ZfDo?m:yqcG<>A8;S?2aSoD~G]i@`I@GRgsRMy>&?`L~XJs?9y8AdS8EX_`&cdNQMOReETEjnQ~YmS:A\JeLdB3ABHGOXz=ijzXh]himjhBLjNgDf;IG]^9hAXDdD:NICOm:QJAjfN&D^O]bgO2sb:FFlJf2TheY;x]|H`\yaEzHnE;ONgSo<gB]H|lcblg{XYbcILza[g@9dRRmbf`_`rXL;FA=jFZhHZzffbb}]zc~a9zHIDi][edLLZBZbyxHiX\MI[D`Nf>\;nC89i^H}oJ;^YyBgfBg`ZZ`AT]XJIoY=b~l|DjSI{FMO`L:JqF`9=?SH9XSi{B\b_^D9idOnfOLJ`yC?alMI\~Hq&CgcC9LnYZnE]l]`EB>DMMjAc@_sDE~H8_L[HfEiILTQmeZh8as]&GyRi}qsxn&9f;8h\i8hShaXhTohicaCl^Xr~yONY{gOIHqh`2@lc`x~rgmSo>;E`x99:T}A\=9_qNxJTM|iCO{[|jsi_^~Ih&JoNlTh&C?E^[Jhe{bm?LJbi\i`JrI}8?_JRBXz2j[h`3HcYa[HOS<>DRa_qSb:8rrmOa~]=bQf`m2;h_mda&j_icLA`Oi|q9{e2]9g9YA~f`[XNnD>YQ]_h>?fLDA>lsjLG~i{|a;8qilSxr[2Z\cXd;}~3~e;Qbo[&mO:>2Rh8X~E_^Bs<^asm>`e;A]9`r2giL:^x]9>xG[lqn=MmYA{Mb{D`I\AscR2RLsy`ML;3AA=d:oOS99bb2^eA>MdL^ROx=3x>GeMJG2>C;c``JQ`GNCoR3_HyNNR;ljz^o|rGJAfG3@\IiAL{\D_Cq9}cJZ3e{>\BEX&O`G{^dT|oJl2m\Dz{>FbXJ^Zb|zi8M~}9_@9|nO>h3`QHyH^NA<<[~`;\iRs;_\3]M&Lr~Q3_lE_L=jZhH:Farz9AYHyOM@QLgnoQChYs&Xi_dyzQ&X?i`R2OTDsdxy>{qDAaYia[nRx|LxTC|yTel|TzELSOTyo;z~2`^QG^XiEi{[HjqGS{=gq|]f9;aSR>XAJGY{z^D[GsHGMl@9ned:d[g&Zec?g^lM|n_zRG@B?\`Q{2II>&e~=zMNyn=Bf@}<[?BB]]giq3?J_aFDXF`jOCDMNfdO&_ex_A\:slD\;L`=RhRFyf^IOL?B]Jb|H8afLFxX}\Cy@^MrQ~E:}i~GAfqDL[HgnNe@9Rqc|Hx?QA~HH?g>3hzby;9@2@Z2egTNobM:M[]YCzErQm3BjxRF`Zdr8Yh9qJ`3ojHYy&R3zDJHlaEQcBsiicaNdB]jiG}fQbnRmzO:&&2TTDT[;?}[}?GTfzj_em8GD`~8~TT8T|Co\m2SD:JAJJ66 --------------------------------------------------------------------------------