UNPKG

alclient

Version:

A node client for interacting with Adventure Land - The Code MMORPG. This package extends the functionality of 'alclient' by managing a mongo database.

502 lines 21.4 kB
import axios from "axios"; import fs from "fs"; import url from "url"; import { Database, PlayerModel } from "./database/Database.js"; import { Paladin } from "./Paladin.js"; import { Mage } from "./Mage.js"; import { Merchant } from "./Merchant.js"; import { Observer } from "./Observer.js"; import { PingCompensatedCharacter } from "./PingCompensatedCharacter.js"; import { Priest } from "./Priest.js"; import { Ranger } from "./Ranger.js"; import { Rogue } from "./Rogue.js"; import { Warrior } from "./Warrior.js"; export class Game { static user; static servers = {}; static characters = {}; static G; static version; static server = "https://adventure.land"; static get url() { if (!this.user) return this.server; if (this.user.secure) { return this.server.replace("http:", "https:"); } else { return this.server.replace("https:", "http:"); } } constructor() { // Private to force static methods } static setServer(server) { if (!server.startsWith("http")) { throw new Error("Please specify the server with http(s)://"); } this.server = server; } static async deleteMail(mailID) { if (!this.user) throw new Error("You must login first."); const response = await axios.post(`${this.url}/api/delete_mail`, `method=delete_mail&arguments={"mid":"${mailID}"}`, { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } }); const data = response.data[0]; if (data.message == "Mail deleted.") return true; return false; } static async disconnectCharacter(characterName) { if (!this.user) throw new Error("You must login first."); const response = await axios.post(`${this.url}/api/disconnect_character`, `method=disconnect_character&arguments={"name":"${characterName}"}`, { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } }); const data = response.data[0]; if (data.message == "Sent the disconnect signal to the server" || data.message == "Character is not in game.") return true; return false; } static async getGData(cache = false, optimize = false) { if (this.G) return this.G; if (!this.version) await this.getVersion(); const gFile = `G_${this.version}.json`; try { if (!cache) throw new Error("Skipping Cache..."); // Check if there's cached data this.G = JSON.parse(fs.readFileSync(gFile, "utf8")); return this.G; } catch (e) { // There's no cached data, download it console.debug("Updating 'G' data..."); const response = await axios.get(`${this.url}/data.js`); if (response.status == 200) { // Update G with the latest data const matches = response.data.match(/var\s+G\s*=\s*(\{.+\});/); const rawG = matches[1]; this.G = JSON.parse(rawG); if (optimize) this.G = this.optimizeG(this.G); console.debug("Updated 'G' data!"); if (cache) fs.writeFileSync(gFile, JSON.stringify(this.G)); return this.G; } else { console.error(response); console.error(`Error fetching ${this.url}/data.js`); } } } static async getMail(all = true) { if (!this.user) throw new Error("You must login first."); let response = await axios.post(`${this.url}/api/pull_mail`, "method=pull_mail&arguments={}", { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` }, }); const mail = []; while (response.data.length > 0) { mail.push(...response.data[0].mail); if (all && response.data[0].more) { // Get more mail response = await axios.post(`${this.url}/api/pull_mail`, `method=pull_mail&arguments={"cursor":"${response.data[0].cursor}"}`, { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } }); } else { break; } } return mail; } static async getMerchants() { if (!this.user) throw new Error("You must login first."); //const merchants: PullMerchantsData[] = [] const merchants = []; const data = await axios.post(`${this.url}/api/pull_merchants`, "method=pull_merchants", { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` }, }); for (const datum of data.data) { if (datum.type == "merchants") { for (const char of datum.chars) { merchants.push(char); } } } if (Database.connection) { const informationDate = Date.now() - 120000; /** Assume the information is 2 minutes old */ // Update the database with the merchant's information const promises = []; for (const merchant of merchants) { const server = merchant.server.split(" "); promises.push(PlayerModel.updateOne({ lastSeen: { $lt: informationDate }, name: merchant.name }, { lastSeen: informationDate, map: merchant.map, serverIdentifier: server[1], serverRegion: server[0], // We have to update all of the trade slots individually so we don't overwrite what they have equipped "slots.trade1": merchant.slots.trade1 ?? null, "slots.trade2": merchant.slots.trade2 ?? null, "slots.trade3": merchant.slots.trade3 ?? null, "slots.trade4": merchant.slots.trade4 ?? null, "slots.trade5": merchant.slots.trade5 ?? null, "slots.trade6": merchant.slots.trade6 ?? null, "slots.trade7": merchant.slots.trade7 ?? null, "slots.trade8": merchant.slots.trade8 ?? null, "slots.trade9": merchant.slots.trade9 ?? null, "slots.trade10": merchant.slots.trade10 ?? null, "slots.trade11": merchant.slots.trade11 ?? null, "slots.trade12": merchant.slots.trade12 ?? null, "slots.trade13": merchant.slots.trade13 ?? null, "slots.trade14": merchant.slots.trade14 ?? null, "slots.trade15": merchant.slots.trade15 ?? null, "slots.trade16": merchant.slots.trade16 ?? null, "slots.trade17": merchant.slots.trade17 ?? null, "slots.trade18": merchant.slots.trade18 ?? null, "slots.trade19": merchant.slots.trade19 ?? null, "slots.trade20": merchant.slots.trade20 ?? null, "slots.trade21": merchant.slots.trade21 ?? null, "slots.trade22": merchant.slots.trade22 ?? null, "slots.trade23": merchant.slots.trade23 ?? null, "slots.trade24": merchant.slots.trade24 ?? null, "slots.trade25": merchant.slots.trade25 ?? null, "slots.trade26": merchant.slots.trade26 ?? null, "slots.trade27": merchant.slots.trade27 ?? null, "slots.trade28": merchant.slots.trade28 ?? null, "slots.trade29": merchant.slots.trade29 ?? null, "slots.trade30": merchant.slots.trade30 ?? null, x: merchant.x, y: merchant.y, }) .lean() .exec()); } await Promise.all(promises); } return merchants; } static async getVersion() { const response = await axios.get(`${this.url}/comm`); if (response.status == 200) { // Find the version const matches = response.data.match(/var\s+VERSION\s*=\s*'(\d+)/); this.version = Number.parseInt(matches[1]); return this.version; } else { console.error(response); console.error(`Error fetching ${this.url}/comm`); } } /** * The following function will tell the server that we've read the following mail message * @param mailID The mail message to mark as 'read' */ static async markMailAsRead(mailID) { if (!this.user) throw new Error("You must login first."); const response = await axios.post(`${this.url}/api/read_mail`, `method=read_mail&arguments={"mail": "${mailID}"}`, { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } }); return response.data[0]; } static async login(email, password, mongo, secure = true) { // Connect to Mongo if (!Database.connection && mongo) await Database.connect(mongo); if (!this.user) { // Login and save the auth console.debug("Logging in..."); const params = new url.URLSearchParams(); params.append("method", "signup_or_login"); params.append("arguments", JSON.stringify({ email: email, only_login: true, password: password })); const login = await axios.post(`${this.url}/api/signup_or_login`, params.toString()); let loginResult; for (const datum of login.data) { if (datum["message"]) { loginResult = datum; break; } } if (loginResult && loginResult.message == "Logged In!") { console.debug("Logged in!"); // We successfully logged in // Find the auth cookie and save it for (const cookie of login.headers["set-cookie"]) { const result = /^auth=(.+?);/.exec(cookie); if (result) { // Save our data to the database this.user = { secure: secure, userAuth: result[1].split("-")[1], userID: result[1].split("-")[0], }; break; } } } else if (loginResult && loginResult.message) { // We failed logging in, and we have a reason from the server console.error(loginResult.message); throw new Error(loginResult.message); } else { // We failed logging in, but we don't know what went wrong console.error(login.data); throw new Error("Failed logging in."); } } return this.updateServersAndCharacters(); } static async loginJSONFile(path, secure = true) { let fileData; try { fileData = fs.readFileSync(path, "utf8"); } catch (e) { throw new Error(`Could not locate '${path}'.`); } const data = JSON.parse(fileData); // Set UserID & UserAuth if it exists in the credentials file if (data.userID && data.userAuth) { this.user = { secure: secure, userAuth: data.userAuth, userID: data.userID, }; } try { await this.login(data.email, data.password, data.mongo, secure); } catch (e) { if (data.userID && data.userAuth) { // Delete the userAuth and userID, and try again delete data.userAuth; delete data.userID; fs.writeFileSync(path, JSON.stringify(data, undefined, 4), "utf8"); delete this.user; return this.loginJSONFile(path, secure); } return false; } if (this.user && this.user.userAuth !== data.userAuth) { // Update the credentials file with the new auth data.userAuth = this.user.userAuth; data.userID = this.user.userID; fs.writeFileSync(path, JSON.stringify(data, undefined, 4), "utf8"); } return true; } static async logoutEverywhere() { if (!this.user) throw new Error("You must login first."); const response = await axios.post(`${this.url}/api/logout_everywhere`, "method=logout_everywhere", { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` }, }); delete this.user; return response.data; } /** * This function optimizes G for ALClient. It removes unnecessary things, and optimizes other things to improve processing time. * * @static * @param {GData} g * @memberof Game */ static optimizeG(g) { // Delete GUI-only stuff to reduce file size and subsequent JSON parsing time delete g.animations; delete g.docs; delete g.images; delete g.imagesets; delete g.sprites; delete g.positions; delete g.tilesets; // Optimize items to reduce file size and subsequent JSON parsing time for (const itemName in g.items) { const gItem = g.items[itemName]; delete gItem.cx; delete gItem.explanation; delete gItem.trex; delete gItem.skin; delete gItem.skin_a; delete gItem.skin_c; delete gItem.skin_r; delete gItem.xcx; } // Optimize min and max values to improve pathfinding generation for (const mapName in g.geometry) { const gGeometry = g.geometry[mapName]; delete gGeometry.groups; delete gGeometry.placements; delete gGeometry.points; delete gGeometry.rectangles; if (!gGeometry.x_lines || !gGeometry.y_lines) continue; // No geometry let newMinX = Number.MAX_VALUE; let newMinY = Number.MAX_VALUE; let newMaxX = Number.MIN_VALUE; let newMaxY = Number.MIN_VALUE; for (const [x, y1, y2] of gGeometry.x_lines) { if (x - 1 < newMinX) newMinX = x - 1; if (y1 - 1 < newMinY) newMinY = y1 - 1; if (y2 - 1 < newMinY) newMinY = y2 - 1; if (x + 1 > newMaxX) newMaxX = x + 1; if (y1 + 1 > newMaxY) newMaxY = y1 + 1; if (y2 + 1 > newMaxY) newMaxY = y2 + 1; } for (const [y, x1, x2] of gGeometry.y_lines) { if (x1 - 1 < newMinX) newMinX = x1 - 1; if (x2 - 1 < newMinX) newMinX = x2 - 1; if (y - 1 < newMinY) newMinY = y - 1; if (x1 + 1 > newMaxX) newMaxX = x1 + 1; if (x2 + 1 > newMaxX) newMaxX = x2 + 1; if (y + 1 > newMaxY) newMaxY = y + 1; } gGeometry.min_x = newMinX; gGeometry.min_y = newMinY; gGeometry.max_x = newMaxX; gGeometry.max_y = newMaxY; } // Optimize monsters to reduce file size and subsequent JSON parsing time for (const monsterName in g.monsters) { const gMonster = g.monsters[monsterName]; delete gMonster.explanation; delete gMonster.skin; } return g; } static async startCharacter(cName, sRegion, sID) { if (!this.user) throw new Error("You must login first."); if (!this.characters) await this.updateServersAndCharacters(); if (!this.characters[cName]) throw new Error(`You don't have a character with the name '${cName}'`); if (!this.G) await this.getGData(); const userID = this.user.userID; const userAuth = this.user.userAuth; const characterID = this.characters[cName].id; // Create the player and connect let player; switch (this.characters[cName].type) { case "mage": player = new Mage(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "merchant": player = new Merchant(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "paladin": player = new Paladin(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "priest": player = new Priest(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "ranger": player = new Ranger(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "rogue": player = new Rogue(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; case "warrior": player = new Warrior(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; default: player = new PingCompensatedCharacter(userID, userAuth, characterID, Game.G, this.servers[sRegion][sID]); break; } await player.connect(); return player; } static async startMage(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startMerchant(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startPaladin(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startPriest(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startRanger(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startRogue(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startWarrior(cName, sRegion, sID) { return (await Game.startCharacter(cName, sRegion, sID)); } static async startObserver(region, id) { if (!this.user) throw new Error("You must login first."); if (!this.characters) await this.updateServersAndCharacters(); if (!this.G) await this.getGData(); const observer = new Observer(this.servers[region][id], this.G); await observer.connect(true); return observer; } static async startCharacterObserver(cName) { if (!this.user) throw new Error("You must login first."); if (!this.characters) await this.updateServersAndCharacters(); const cData = this.characters[cName]; if (!cData) throw new Error(`You don't have a character with the name '${cName}'`); if (!cData.online) throw new Error(`The character '${cName}' is not online`); if (!this.G) await this.getGData(); const server = /(US|EU|ASIA)([MDCLXVI]+|PVP)/.exec(cData.server); const serverRegion = server[1]; const serverIdentifier = server[2]; const observer = new Observer(this.servers[serverRegion][serverIdentifier], this.G, cData.secret); await observer.connect(true); return observer; } /** * Retrieves your character list, and a list of available servers. * @returns true if successfully updated */ static async updateServersAndCharacters() { if (!this.user) throw new Error("You must login first."); const data = await axios.post(`${this.url}/api/servers_and_characters`, "method=servers_and_characters&arguments={}", { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } }); if (data.status == 200) { // Populate server information for (const serverData of data.data[0].servers) { if (!this.servers[serverData.region]) this.servers[serverData.region] = {}; this.servers[serverData.region][serverData.name] = serverData; this.servers[serverData.region][serverData.name].secure = this.user.secure; } // Populate character information for (const characterData of data.data[0].characters) { this.characters[characterData.name] = characterData; } return true; } else { console.error(data); } throw new Error(`Error fetching ${this.url}/api/servers_and_characters`); } } //# sourceMappingURL=Game.js.map