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
JavaScript
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