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.
467 lines • 19.3 kB
JavaScript
import axios from "axios";
import fs from "fs";
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";
import { Tools } from "./Tools.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:");
}
}
static get authHeader() {
return { headers: { cookie: `auth=${this.user.userID}-${this.user.userAuth}` } };
}
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`, { mid: mailID }, this.authHeader);
const data = response.data.infs[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`, { name: characterName }, this.authHeader);
return response.data.success === true || response.data.reason === "character_not_in_game";
}
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 {
// 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`, {}, this.authHeader);
const mail = [];
while (response.data.infs[0].mail.length > 0) {
mail.push(...response.data.infs[0].mail);
if (all && response.data.infs[0].more) {
// Get more mail
response = await axios.post(`${this.url}/api/pull_mail`, { cursor: response.data.infs[0].cursor }, this.authHeader);
}
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`, {}, this.authHeader);
for (const datum of data.data.infs) {
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 { region, identifier } = Tools.parseServerKey(merchant.server);
promises.push(PlayerModel.updateOne({ lastSeen: { $lt: informationDate }, name: merchant.name }, {
lastSeen: informationDate,
map: merchant.map,
serverRegion: region,
serverIdentifier: identifier,
// 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`, { mail: mailID }, this.authHeader);
return response.data.infs[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 login = await axios.post(`${this.url}/api/signup_or_login`, {
email,
only_login: true,
password,
});
if (login.data.success !== true) {
throw new Error(login.data.reason || "Failed logging in");
}
console.debug("Logged in!");
this.user = {
secure,
userAuth: login.data.auth,
userID: login.data.user,
};
}
return this.updateServersAndCharacters();
}
static async loginJSONFile(path, secure = true) {
let fileData;
try {
fileData = fs.readFileSync(path, "utf8");
}
catch {
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 {
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`, {}, this.authHeader);
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 { region, identifier } = Tools.parseServerKey(cData.server);
const observer = new Observer(this.servers[region][identifier], 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`, {}, this.authHeader);
if (data.status != 200) {
console.error(data);
throw new Error(`Error fetching ${this.url}/api/servers_and_characters`);
}
// Populate server information
for (const serverData of data.data.infs[0].servers) {
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.infs[0].characters) {
this.characters[characterData.name] = characterData;
}
return true;
}
}
//# sourceMappingURL=Game.js.map