Some checks failed
Lint / pre-commit Linting (push) Failing after 42s
1538 lines
50 KiB
JavaScript
1538 lines
50 KiB
JavaScript
'use strict';
|
|
|
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
|
const WebSocket = require('ws');
|
|
const URL = require('url');
|
|
const events = require('events');
|
|
const Debug = require('debug');
|
|
const zlib = require('zlib');
|
|
const axios = require('axios');
|
|
const util = require('util');
|
|
const fs = require('fs');
|
|
const YAML = require('yamljs');
|
|
const path = require('path');
|
|
|
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|
|
|
const WebSocket__default = /*#__PURE__*/_interopDefaultLegacy(WebSocket);
|
|
const URL__default = /*#__PURE__*/_interopDefaultLegacy(URL);
|
|
const Debug__default = /*#__PURE__*/_interopDefaultLegacy(Debug);
|
|
const zlib__default = /*#__PURE__*/_interopDefaultLegacy(zlib);
|
|
const axios__default = /*#__PURE__*/_interopDefaultLegacy(axios);
|
|
const util__default = /*#__PURE__*/_interopDefaultLegacy(util);
|
|
const fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
|
|
const YAML__default = /*#__PURE__*/_interopDefaultLegacy(YAML);
|
|
const path__default = /*#__PURE__*/_interopDefaultLegacy(path);
|
|
|
|
const debug = Debug__default['default']('screepsapi:socket');
|
|
|
|
const DEFAULTS$1 = {
|
|
reconnect: true,
|
|
resubscribe: true,
|
|
keepAlive: true,
|
|
maxRetries: 10,
|
|
maxRetryDelay: 60 * 1000 // in milli-seconds
|
|
};
|
|
|
|
class Socket extends events.EventEmitter {
|
|
constructor (ScreepsAPI) {
|
|
super();
|
|
this.api = ScreepsAPI;
|
|
this.opts = Object.assign({}, DEFAULTS$1);
|
|
this.on('error', () => {}); // catch to prevent unhandled-exception errors
|
|
this.reset();
|
|
this.on('auth', ev => {
|
|
if (ev.data.status === 'ok') {
|
|
while (this.__queue.length) {
|
|
this.emit(this.__queue.shift());
|
|
}
|
|
clearInterval(this.keepAliveInter);
|
|
if (this.opts.keepAlive) {
|
|
this.keepAliveInter = setInterval(() => this.ws && this.ws.ping(1), 10000);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
reset () {
|
|
this.authed = false;
|
|
this.connected = false;
|
|
this.reconnecting = false;
|
|
clearInterval(this.keepAliveInter);
|
|
this.keepAliveInter = 0;
|
|
this.__queue = []; // pending messages (to send once authenticated)
|
|
this.__subQueue = []; // pending subscriptions (to request once authenticated)
|
|
this.__subs = {}; // number of callbacks for each subscription
|
|
}
|
|
|
|
async connect (opts = {}) {
|
|
Object.assign(this.opts, opts);
|
|
if (!this.api.token) {
|
|
throw new Error('No token! Call api.auth() before connecting the socket!')
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
const baseURL = this.api.opts.url.replace('http', 'ws');
|
|
const wsurl = new URL.URL('socket/websocket', baseURL);
|
|
this.ws = new WebSocket__default['default'](wsurl);
|
|
this.ws.on('open', () => {
|
|
this.connected = true;
|
|
this.reconnecting = false;
|
|
if (this.opts.resubscribe) {
|
|
this.__subQueue.push(...Object.keys(this.__subs));
|
|
}
|
|
debug('connected');
|
|
this.emit('connected');
|
|
resolve(this.auth(this.api.token));
|
|
});
|
|
this.ws.on('close', () => {
|
|
clearInterval(this.keepAliveInter);
|
|
this.authed = false;
|
|
this.connected = false;
|
|
debug('disconnected');
|
|
this.emit('disconnected');
|
|
if (this.opts.reconnect) {
|
|
this.reconnect().catch(() => { /* error emitted in reconnect() */ });
|
|
}
|
|
});
|
|
this.ws.on('error', (err) => {
|
|
this.ws.terminate();
|
|
this.emit('error', err);
|
|
debug(`error ${err}`);
|
|
if (!this.connected) {
|
|
reject(err);
|
|
}
|
|
});
|
|
this.ws.on('unexpected-response', (req, res) => {
|
|
const err = new Error(`WS Unexpected Response: ${res.statusCode} ${res.statusMessage}`);
|
|
this.emit('error', err);
|
|
reject(err);
|
|
});
|
|
this.ws.on('message', (data) => this.handleMessage(data));
|
|
})
|
|
}
|
|
|
|
async reconnect () {
|
|
if (this.reconnecting) {
|
|
return
|
|
}
|
|
this.reconnecting = true;
|
|
let retries = 0;
|
|
let retry;
|
|
do {
|
|
let time = Math.pow(2, retries) * 100;
|
|
if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay;
|
|
await this.sleep(time);
|
|
if (!this.reconnecting) return // reset() called in-between
|
|
try {
|
|
await this.connect();
|
|
retry = false;
|
|
} catch (err) {
|
|
retry = true;
|
|
}
|
|
retries++;
|
|
debug(`reconnect ${retries}/${this.opts.maxRetries}`);
|
|
} while (retry && retries < this.opts.maxRetries)
|
|
if (retry) {
|
|
const err = new Error(`Reconnection failed after ${this.opts.maxRetries} retries`);
|
|
this.reconnecting = false;
|
|
debug('reconnect failed');
|
|
this.emit('error', err);
|
|
throw err
|
|
} else {
|
|
// Resume existing subscriptions on the new socket
|
|
Object.keys(this.__subs).forEach(sub => this.subscribe(sub));
|
|
}
|
|
}
|
|
|
|
disconnect () {
|
|
debug('disconnect');
|
|
clearInterval(this.keepAliveInter);
|
|
this.ws.removeAllListeners(); // remove listeners first or we may trigger reconnection & Co.
|
|
this.ws.terminate();
|
|
this.reset();
|
|
this.emit('disconnected');
|
|
}
|
|
|
|
sleep (time) {
|
|
return new Promise((resolve, reject) => {
|
|
setTimeout(resolve, time);
|
|
})
|
|
}
|
|
|
|
handleMessage (msg) {
|
|
msg = msg.data || msg; // Handle ws/browser difference
|
|
if (msg.slice(0, 3) === 'gz:') { msg = this.api.inflate(msg); }
|
|
debug(`message ${msg}`);
|
|
if (msg[0] === '[') {
|
|
msg = JSON.parse(msg);
|
|
let [, type, id, channel] = msg[0].match(/^(.+):(.+?)(?:\/(.+))?$/);
|
|
channel = channel || type;
|
|
const event = { channel, id, type, data: msg[1] };
|
|
this.emit(msg[0], event);
|
|
this.emit(event.channel, event);
|
|
this.emit('message', event);
|
|
} else {
|
|
const [channel, ...data] = msg.split(' ');
|
|
const event = { type: 'server', channel, data };
|
|
if (channel === 'auth') { event.data = { status: data[0], token: data[1] }; }
|
|
if (['protocol', 'time', 'package'].includes(channel)) { event.data = { [channel]: data[0] }; }
|
|
this.emit(channel, event);
|
|
this.emit('message', event);
|
|
}
|
|
}
|
|
|
|
async gzip (bool) {
|
|
this.send(`gzip ${bool ? 'on' : 'off'}`);
|
|
}
|
|
|
|
async send (data) {
|
|
if (!this.connected) {
|
|
this.__queue.push(data);
|
|
} else {
|
|
this.ws.send(data);
|
|
}
|
|
}
|
|
|
|
auth (token) {
|
|
return new Promise((resolve, reject) => {
|
|
this.send(`auth ${token}`);
|
|
this.once('auth', (ev) => {
|
|
const { data } = ev;
|
|
if (data.status === 'ok') {
|
|
this.authed = true;
|
|
this.emit('token', data.token);
|
|
this.emit('authed');
|
|
while (this.__subQueue.length) {
|
|
this.send(this.__subQueue.shift());
|
|
}
|
|
resolve();
|
|
} else {
|
|
reject(new Error('socket auth failed'));
|
|
}
|
|
});
|
|
})
|
|
}
|
|
|
|
async subscribe (path, cb) {
|
|
if (!path) return
|
|
const userID = await this.api.userID();
|
|
if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; }
|
|
if (this.authed) {
|
|
this.send(`subscribe ${path}`);
|
|
} else {
|
|
this.__subQueue.push(`subscribe ${path}`);
|
|
}
|
|
this.emit('subscribe', path);
|
|
this.__subs[path] = this.__subs[path] || 0;
|
|
this.__subs[path]++;
|
|
if (cb) this.on(path, cb);
|
|
}
|
|
|
|
async unsubscribe (path) {
|
|
if (!path) return
|
|
const userID = await this.api.userID();
|
|
if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}`; }
|
|
this.send(`unsubscribe ${path}`);
|
|
this.emit('unsubscribe', path);
|
|
if (this.__subs[path]) this.__subs[path]--;
|
|
}
|
|
}
|
|
|
|
const debugHttp = Debug__default['default']('screepsapi:http');
|
|
const debugRateLimit = Debug__default['default']('screepsapi:ratelimit');
|
|
|
|
const { format } = URL__default['default'];
|
|
|
|
const gunzipAsync = util__default['default'].promisify(zlib__default['default'].gunzip);
|
|
const inflateAsync = util__default['default'].promisify(zlib__default['default'].inflate);
|
|
|
|
const DEFAULT_SHARD = 'shard0';
|
|
const OFFICIAL_HISTORY_INTERVAL = 100;
|
|
const PRIVATE_HISTORY_INTERVAL = 20;
|
|
|
|
const sleep = ms => new Promise(resolve => setInterval(resolve, ms));
|
|
|
|
class RawAPI extends events.EventEmitter {
|
|
constructor (opts = {}) {
|
|
super();
|
|
this.setServer(opts);
|
|
const self = this;
|
|
this.raw = {
|
|
|
|
/**
|
|
* GET /api/version
|
|
* @returns {{
|
|
* ok:1, package:number, protocol: number,
|
|
* serverData: {
|
|
* customObjectTypes,
|
|
* historyChunkSize:number,
|
|
* features,
|
|
* shards: string[]
|
|
* },
|
|
* users:number
|
|
* }}
|
|
*/
|
|
version () {
|
|
return self.req('GET', '/api/version')
|
|
},
|
|
/**
|
|
* GET /api/authmod
|
|
* @returns {Object}
|
|
*/
|
|
authmod () {
|
|
if (self.isOfficialServer()) {
|
|
return Promise.resolve({ name: 'official' })
|
|
}
|
|
return self.req('GET', '/api/authmod')
|
|
},
|
|
/**
|
|
* Official:
|
|
* GET /room-history/${shard}/${room}/${tick}.json
|
|
* Private:
|
|
* GET /room-history
|
|
* @param {string} room
|
|
* @param {number} tick
|
|
* @param {string} shard
|
|
* @returns {Object} A json file with history data
|
|
*/
|
|
history (room, tick, shard = DEFAULT_SHARD) {
|
|
if (self.isOfficialServer()) {
|
|
tick -= tick % OFFICIAL_HISTORY_INTERVAL;
|
|
return self.req('GET', `/room-history/${shard}/${room}/${tick}.json`)
|
|
} else {
|
|
tick -= tick % PRIVATE_HISTORY_INTERVAL;
|
|
return self.req('GET', '/room-history', { room, time: tick })
|
|
}
|
|
},
|
|
servers: {
|
|
/**
|
|
* POST /api/servers/list
|
|
* A list of community servers
|
|
* @returns {{
|
|
* ok:number,
|
|
* servers:{
|
|
* _id:string,
|
|
* settings:{
|
|
* host:string,
|
|
* port:string,
|
|
* pass:string
|
|
* },
|
|
* name:string,
|
|
* status:"active"|string
|
|
* likeCount:number
|
|
* }[]
|
|
* }}
|
|
*/
|
|
list () {
|
|
return self.req('POST', '/api/servers/list', {})
|
|
}
|
|
},
|
|
auth: {
|
|
/**
|
|
* POST /api/auth/signin
|
|
* @param {string} email
|
|
* @param {string} password
|
|
* @returns {{ok:number, token:string}}
|
|
*/
|
|
signin (email, password) {
|
|
return self.req('POST', '/api/auth/signin', { email, password })
|
|
},
|
|
/**
|
|
* POST /api/auth/steam-ticket
|
|
* @param {*} ticket
|
|
* @param {*} useNativeAuth
|
|
* @returns {Object}
|
|
*/
|
|
steamTicket (ticket, useNativeAuth = false) {
|
|
return self.req('POST', '/api/auth/steam-ticket', { ticket, useNativeAuth })
|
|
},
|
|
/**
|
|
* GET /api/auth/me
|
|
* @returns {{
|
|
* ok: number;
|
|
* _id: string;
|
|
* email: string;
|
|
* username: string;
|
|
* cpu: number;
|
|
* badge: Badge;
|
|
* password: string;
|
|
* notifyPrefs: { sendOnline: any; errorsInterval: any; disabledOnMessages: any; disabled: any; interval: any };
|
|
* gcl: number;
|
|
* credits: number;
|
|
* lastChargeTime: any;
|
|
* lastTweetTime: any;
|
|
* github: { id: any; username: any };
|
|
* twitter: { username: string; followers_count: number };
|
|
*}}
|
|
*/
|
|
me () {
|
|
return self.req('GET', '/api/auth/me')
|
|
},
|
|
/**
|
|
* GET /api/auth/query-token
|
|
* @param {string} token
|
|
* @returns {Object}
|
|
*/
|
|
queryToken (token) {
|
|
return self.req('GET', '/api/auth/query-token', { token })
|
|
}
|
|
},
|
|
register: {
|
|
/**
|
|
* GET /api/register/check-email
|
|
* @param {string} email
|
|
* @returns {Object}
|
|
*/
|
|
checkEmail (email) {
|
|
return self.req('GET', '/api/register/check-email', { email })
|
|
},
|
|
/**
|
|
* GET /api/register/check-username
|
|
* @param {string} username
|
|
* @returns {Object}
|
|
*/
|
|
checkUsername (username) {
|
|
return self.req('GET', '/api/register/check-username', { username })
|
|
},
|
|
/**
|
|
* POST /api/register/set-username
|
|
* @param {string} username
|
|
* @returns {Object}
|
|
*/
|
|
setUsername (username) {
|
|
return self.req('POST', '/api/register/set-username', { username })
|
|
},
|
|
/**
|
|
* POST /api/register/submit
|
|
* @param {string} username
|
|
* @param {string} email
|
|
* @param {string} password
|
|
* @param {*} modules
|
|
* @returns {Object}
|
|
*/
|
|
submit (username, email, password, modules) {
|
|
return self.req('POST', '/api/register/submit', { username, email, password, modules })
|
|
}
|
|
},
|
|
userMessages: {
|
|
/**
|
|
* GET /api/user/messages/list?respondent={userId}
|
|
* @param {string} respondent the long `_id` of the user, not the username
|
|
* @returns {{ ok, messages: [ { _id, date, type, text, unread } ] }}
|
|
*/
|
|
list (respondent) {
|
|
return self.req('GET', '/api/user/messages/list', { respondent })
|
|
},
|
|
/**
|
|
* GET /api/user/messages/index
|
|
* @returns {{ ok, messages: [ { _id, message: { _id, user, respondent, date, type, text, unread } } ], users: { <user's _id>: { _id, username, badge: Badge } } }}
|
|
*/
|
|
index () {
|
|
return self.req('GET', '/api/user/messages/index')
|
|
},
|
|
/**
|
|
* GET /api/user/messages/unread-count
|
|
* @returns {{ ok, count:number }}
|
|
*/
|
|
unreadCount () {
|
|
return self.req('GET', '/api/user/messages/unread-count')
|
|
},
|
|
/**
|
|
* POST /api/user/messages/send
|
|
* @param {string} respondent the long `_id` of the user, not the username
|
|
* @param {string} text
|
|
* @returns {{ ok }}
|
|
*/
|
|
send (respondent, text) {
|
|
return self.req('POST', '/api/user/messages/send', { respondent, text })
|
|
},
|
|
/**
|
|
* POST /api/user/messages/mark-read
|
|
* @param {string} id
|
|
* @returns {Object}
|
|
*/
|
|
markRead (id) {
|
|
return self.req('POST', '/api/user/messages/mark-read', { id })
|
|
}
|
|
},
|
|
game: {
|
|
/**
|
|
* @typedef {"creepsLost"|"creepsProduced"|"energyConstruction"|"energyControl"|"energyCreeps"|"energyHarvested"} stat
|
|
* @param {string[]} rooms An array of room names
|
|
* @param {"owner0"|"claim0"|stat} statName
|
|
* @param {string} shard
|
|
* @returns {{
|
|
* ok:number,
|
|
* stats: {
|
|
* [roomName:string]: {
|
|
* status,
|
|
* novice,
|
|
* own: { user, level },
|
|
* <stat>: [ { user, value }]
|
|
* }
|
|
* }
|
|
* , users: { [userId:string]: { _id, username, badge: Badge } } }}
|
|
* The return type is not mapped correctly
|
|
*/
|
|
mapStats (rooms, statName, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/map-stats', { rooms, statName, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/gen-unique-object-name
|
|
* @param {"flag"|"spawn"|string} type can be at least "flag" or "spawn"
|
|
* @param {string} shard
|
|
* @returns { ok, name:string }
|
|
*/
|
|
genUniqueObjectName (type, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/gen-unique-object-name', { type, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/check-unique-object-name
|
|
* @param {string} type
|
|
* @param {string} name
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
checkUniqueObjectName (type, name, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/check-unique-object-name', { type, name, shard })
|
|
},
|
|
/**
|
|
* @param {string} room
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {string} name
|
|
* @param {string?} shard
|
|
*/
|
|
placeSpawn (room, x, y, name, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/place-spawn', { name, room, x, y, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/create-flag
|
|
* @param {string} room
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {string} name
|
|
* @param {FlagColor} color
|
|
* @param {FlagColor} secondaryColor
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }}
|
|
* - if the name is new, result.upserted[0]._id is the game id of the created flag
|
|
* - if not, this moves the flag and the response does not contain the id (but the id doesn't change)
|
|
* - `connection` looks like some internal MongoDB thing that is irrelevant to us
|
|
*/
|
|
createFlag (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/create-flag', { name, room, x, y, color, secondaryColor, shard })
|
|
},
|
|
/**
|
|
* POST/api/game/gen-unique-flag-name
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
genUniqueFlagName (shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/gen-unique-flag-name', { shard })
|
|
},
|
|
/**
|
|
* POST /api/game/check-unique-flag-name
|
|
* @param {string} name
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
checkUniqueFlagName (name, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/check-unique-flag-name', { name, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/change-flag-color
|
|
* @param {FlagColor} color
|
|
* @param {FlagColor} secondaryColor
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { nModified, ok, n }, connection: { host, id, port } }}
|
|
*/
|
|
changeFlagColor (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/change-flag-color', { color, secondaryColor, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/remove-flag
|
|
* @param {string} room
|
|
* @param {string} name
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
removeFlag (room, name, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/remove-flag', { name, room, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/add-object-intent
|
|
* [Missing parameter] _id is the game id of the object to affect (except for destroying structures), room is the name of the room it's in
|
|
* this method is used for a variety of actions, depending on the `name` and `intent` parameters
|
|
* @example remove flag: name = "remove", intent = {}
|
|
* @example destroy structure: _id = "room", name = "destroyStructure", intent = [ {id: <structure id>, roomName, <room name>, user: <user id>} ]
|
|
can destroy multiple structures at once
|
|
* @example suicide creep: name = "suicide", intent = {id: <creep id>}
|
|
* @example unclaim controller: name = "unclaim", intent = {id: <controller id>}
|
|
intent can be an empty object for suicide and unclaim, but the web interface sends the id in it, as described
|
|
* @example remove construction site: name = "remove", intent = {}
|
|
* @param {string} room
|
|
* @param {string} name
|
|
* @param {string} intent
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }}
|
|
*/
|
|
addObjectIntent (room, name, intent, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/add-object-intent', { room, name, intent, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/create-construction
|
|
* @param {string} room
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {string} structureType the same value as one of the in-game STRUCTURE_* constants ('road', 'spawn', etc.)
|
|
* @param {string} name
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { ok, n }, ops: [ { type, room, x, y, structureType, user, progress, progressTotal, _id } ], insertedCount, insertedIds }}
|
|
*/
|
|
createConstruction (room, x, y, structureType, name, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/create-construction', { room, x, y, structureType, name, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/set-notify-when-attacked
|
|
* @param {string} _id
|
|
* @param {bool} enabled is either true or false (literal values, not strings)
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { ok, nModified, n }, connection: { id, host, port } }}
|
|
*/
|
|
setNotifyWhenAttacked (_id, enabled = true, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/set-notify-when-attacked', { _id, enabled, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/create-invader
|
|
* @param {string} room
|
|
* @param {number} x
|
|
* @param {number} y
|
|
* @param {*} size
|
|
* @param {*} type
|
|
* @param {boolean} boosted
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
createInvader (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/create-invader', { room, x, y, size, type, boosted, shard })
|
|
},
|
|
/**
|
|
* POST /api/game/remove-invader
|
|
* @param {string} _id
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
removeInvader (_id, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/game/remove-invader', { _id, shard })
|
|
},
|
|
/**
|
|
* GET /api/game/time
|
|
* @param {string} shard
|
|
* @returns {{ ok:number, time:number }}
|
|
*/
|
|
time (shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/time', { shard })
|
|
},
|
|
/**
|
|
* GET /api/game/world-size
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
worldSize (shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/world-size', { shard })
|
|
},
|
|
/**
|
|
* GET /api/game/room-decorations
|
|
* @param {string} room
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
roomDecorations (room, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/room-decorations', { room, shard })
|
|
},
|
|
/**
|
|
* GET /api/game/room-objects
|
|
* @param {string} room
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
roomObjects (room, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/room-objects', { room, shard })
|
|
},
|
|
/**
|
|
* @param {string} room
|
|
* @param {*} encoded can be anything non-empty
|
|
* @param {string} shard
|
|
* @returns {{ ok, terrain: [ { room:string, x:number, y:number, type:"wall"|"swamp" } ] }
|
|
* | { ok, terrain: [ { _id,room:string, terrain:string, type:"wall"|"swamp" } ] }}
|
|
* terrain is a string of digits, giving the terrain left-to-right and top-to-bottom
|
|
* 0: plain, 1: wall, 2: swamp, 3: also wall
|
|
*/
|
|
roomTerrain (room, encoded = 1, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/room-terrain', { room, encoded, shard })
|
|
},
|
|
/**
|
|
* @param {string} room
|
|
* @param {string} shard
|
|
* @returns {{ _id, status:"normal"|"out of borders"|string, novice:string }}
|
|
* `status` can at least be "normal" or "out of borders"
|
|
* if the room is in a novice area, novice will contain the Unix timestamp of the end of the protection (otherwise it is absent)
|
|
*/
|
|
roomStatus (room, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/room-status', { room, shard })
|
|
},
|
|
/**
|
|
* GET /api/game/room-overview
|
|
* @param {string} room
|
|
* @param {number} interval
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
roomOverview (room, interval = 8, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/room-overview', { room, interval, shard })
|
|
},
|
|
market: {
|
|
/**
|
|
* GET /api/game/market/orders-index
|
|
* @param {string} shard
|
|
* @returns {{ok:1,list:[{_id:string,count:number}]}}
|
|
* - _id is the resource type, and there will only be one of each type.
|
|
* - `count` is the number of orders.
|
|
*/
|
|
ordersIndex (shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/market/orders-index', { shard })
|
|
},
|
|
/**
|
|
* GET /api/game/market/my-orders
|
|
* @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }}
|
|
* `resourceType` is one of the RESOURCE_* constants.
|
|
*/
|
|
myOrders () {
|
|
return self.req('GET', '/api/game/market/my-orders').then(self.mapToShard)
|
|
},
|
|
/**
|
|
* GET /api/game/market/orders
|
|
* @param {string} resourceType one of the RESOURCE_* constants.
|
|
* @param {string} shard
|
|
* @returns {{ ok:number, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }}
|
|
* `resourceType` is one of the RESOURCE_* constants.
|
|
*/
|
|
orders (resourceType, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/market/orders', { resourceType, shard })
|
|
},
|
|
/**
|
|
* GET /api/game/market/stats
|
|
* @param {*} resourceType
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
stats (resourceType, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/game/market/stats', { resourceType, shard })
|
|
}
|
|
},
|
|
shards: {
|
|
/**
|
|
* GET /api/game/shards/info
|
|
* @returns {{ok:number, shards:[{name:string,lastTicks:number[],cpuLimimt:number,rooms:number,users:number,tick:number}]}}
|
|
*/
|
|
info () {
|
|
return self.req('GET', '/api/game/shards/info')
|
|
}
|
|
}
|
|
},
|
|
leaderboard: {
|
|
/**
|
|
* GET /api/leaderboard/list
|
|
* @param {number} limit
|
|
* @param {"world"|"power"} mode
|
|
* @param {number?} offset
|
|
* @param {string?} season
|
|
* @returns {{ ok, list: [ { _id, season, user, score, rank } ], count, users: { <user's _id>: { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } } }}
|
|
*/
|
|
list (limit = 10, mode = 'world', offset = 0, season) {
|
|
if (mode !== 'world' && mode !== 'power') throw new Error('incorrect mode parameter')
|
|
if (!season) season = self.currentSeason();
|
|
return self.req('GET', '/api/leaderboard/list', { limit, mode, offset, season })
|
|
},
|
|
/**
|
|
* GET /api/leaderboard/find
|
|
* @param {string} username
|
|
* @param {"world"|string} mode
|
|
* @param {string?} season An optional date in the format YYYY-MM, if not supplied all ranks in all seasons is returned.
|
|
* @returns {{ ok, _id, season, user, score, rank }}
|
|
* - `user` (not `_id`) is the user's _id, as returned by `me` and `user/find`
|
|
* - `rank` is 0-based
|
|
*/
|
|
find (username, mode = 'world', season = '') {
|
|
return self.req('GET', '/api/leaderboard/find', { season, mode, username })
|
|
},
|
|
/**
|
|
* GET /api/leaderboard/seasons
|
|
* @returns {{ ok, seasons: [ { _id, name, date } ] }}
|
|
* The _id returned here is used for the season name in the other leaderboard calls
|
|
*/
|
|
seasons () {
|
|
return self.req('GET', '/api/leaderboard/seasons')
|
|
}
|
|
},
|
|
user: {
|
|
/**
|
|
* @param {Badge} badge
|
|
* @returns {{ ok?:number,error?:string}}
|
|
*/
|
|
badge (badge) {
|
|
return self.req('POST', '/api/user/badge', { badge })
|
|
},
|
|
/**
|
|
* POST /api/user/respawn
|
|
* @returns {Object}
|
|
*/
|
|
respawn () {
|
|
return self.req('POST', '/api/user/respawn')
|
|
},
|
|
/**
|
|
* POST /api/user/set-active-branch
|
|
* @param {string} branch
|
|
* @param {string} activeName
|
|
* @returns {Object}
|
|
*/
|
|
setActiveBranch (branch, activeName) {
|
|
return self.req('POST', '/api/user/set-active-branch', { branch, activeName })
|
|
},
|
|
/**
|
|
* POST /api/user/clone-branch
|
|
* @param {string} branch
|
|
* @param {string} newName
|
|
* @param {*} defaultModules
|
|
* @returns {Object}
|
|
*/
|
|
cloneBranch (branch, newName, defaultModules) {
|
|
return self.req('POST', '/api/user/clone-branch', { branch, newName, defaultModules })
|
|
},
|
|
/**
|
|
* POST /api/user/delete-branch
|
|
* @param {string} branch
|
|
* @returns {Object}
|
|
*/
|
|
deleteBranch (branch) {
|
|
return self.req('POST', '/api/user/delete-branch', { branch })
|
|
},
|
|
/**
|
|
* POST /api/user/notify-prefs
|
|
* @param {*} prefs
|
|
* @returns {Object}
|
|
*/
|
|
notifyPrefs (prefs) {
|
|
// disabled,disabledOnMessages,sendOnline,interval,errorsInterval
|
|
return self.req('POST', '/api/user/notify-prefs', prefs)
|
|
},
|
|
/**
|
|
* POST /api/user/tutorial-done
|
|
* @returns {Object}
|
|
*/
|
|
tutorialDone () {
|
|
return self.req('POST', '/api/user/tutorial-done')
|
|
},
|
|
/**
|
|
* POST /api/user/email
|
|
* @param {string} email
|
|
* @returns {Object}
|
|
*/
|
|
email (email) {
|
|
return self.req('POST', '/api/user/email', { email })
|
|
},
|
|
/**
|
|
* GET /api/user/world-start-room
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
worldStartRoom (shard) {
|
|
return self.req('GET', '/api/user/world-start-room', { shard })
|
|
},
|
|
/**
|
|
* returns a world status
|
|
* - 'normal'
|
|
* - 'lost' when you loose all your spawns
|
|
* - 'empty' when you have respawned and not placed your spawn yet
|
|
* @returns {{ ok: number; status: "normal" | "lost" | "empty" }} */
|
|
worldStatus () {
|
|
return self.req('GET', '/api/user/world-status')
|
|
},
|
|
/**
|
|
* GET /api/user/branches
|
|
* @returns {{ ok:number, list: [{
|
|
* _id: string;
|
|
* branch: string;
|
|
* activeWorld: boolean;
|
|
* activeSim: boolean;
|
|
* }]}
|
|
* }
|
|
*/
|
|
branches () {
|
|
return self.req('GET', '/api/user/branches')
|
|
},
|
|
code: {
|
|
/**
|
|
* GET /api/user/code
|
|
* for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612
|
|
* @param {string} branch
|
|
* @returns code
|
|
*/
|
|
get (branch) {
|
|
return self.req('GET', '/api/user/code', { branch })
|
|
},
|
|
/**
|
|
* POST /api/user/code
|
|
* for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612
|
|
* @param {string} branch
|
|
* @param {*} modules
|
|
* @param {*} _hash
|
|
* @returns {Object}
|
|
*/
|
|
set (branch, modules, _hash) {
|
|
if (!_hash) _hash = Date.now();
|
|
return self.req('POST', '/api/user/code', { branch, modules, _hash })
|
|
}
|
|
},
|
|
decorations: {
|
|
/**
|
|
* GET /api/user/decorations/inventory
|
|
* @returns {Object}
|
|
*/
|
|
inventory () {
|
|
return self.req('GET', '/api/user/decorations/inventory')
|
|
},
|
|
/**
|
|
* GET /api/user/decorations/themes
|
|
* @returns {Object}
|
|
*/
|
|
themes () {
|
|
return self.req('GET', '/api/user/decorations/themes')
|
|
},
|
|
/**
|
|
* POST /api/user/decorations/convert
|
|
* @param {*} decorations decorations is a string array of ids
|
|
* @returns {Object}
|
|
*/
|
|
convert (decorations) {
|
|
return self.req('POST', '/api/user/decorations/convert', { decorations })
|
|
},
|
|
/**
|
|
* POST /api/user/decorations/pixelize
|
|
* @param {number} count
|
|
* @param {string} theme
|
|
* @returns {Object}
|
|
*/
|
|
pixelize (count, theme = '') {
|
|
return self.req('POST', '/api/user/decorations/pixelize', { count, theme })
|
|
},
|
|
/**
|
|
* POST /api/user/decorations/activate
|
|
* @param {string} _id
|
|
* @param {*} active
|
|
* @returns {Object}
|
|
*/
|
|
activate (_id, active) {
|
|
return self.req('POST', '/api/user/decorations/activate', { _id, active })
|
|
},
|
|
/**
|
|
* POST /api/user/decorations/deactivate
|
|
* @param {*} decorations decorations is a string array of ids
|
|
* @returns {Object}
|
|
*/
|
|
deactivate (decorations) {
|
|
return self.req('POST', '/api/user/decorations/deactivate', { decorations })
|
|
}
|
|
},
|
|
/**
|
|
* GET /api/user/respawn-prohibited-rooms
|
|
* @returns {{ ok, rooms: [ ] }}
|
|
* - `room` is an array, but seems to always contain only one element
|
|
*/
|
|
respawnProhibitedRooms () {
|
|
return self.req('GET', '/api/user/respawn-prohibited-rooms')
|
|
},
|
|
memory: {
|
|
/**
|
|
* GET /api/user/memory?path={path}
|
|
* @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1
|
|
* @param {string} shard
|
|
* @returns {string} gz: followed by base64-encoded gzipped JSON encoding of the requested memory path
|
|
*/
|
|
get (path, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/user/memory', { path, shard })
|
|
},
|
|
/**
|
|
* POST /api/user/memory
|
|
* @param {string} path the path may be empty or absent to retrieve all of Memory, Example: flags.Flag1
|
|
* @param {*} value
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { ok, n }, ops: [ { user, expression, hidden } ], data, insertedCount, insertedIds }}
|
|
*/
|
|
set (path, value, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/user/memory', { path, value, shard })
|
|
},
|
|
segment: {
|
|
/**
|
|
* GET /api/user/memory-segment?segment=[0-99]
|
|
* @param {number} segment A number from 0-99
|
|
* @param {string} shard
|
|
* @returns {{ ok, data: string }}
|
|
*/
|
|
get (segment, shard = DEFAULT_SHARD) {
|
|
return self.req('GET', '/api/user/memory-segment', { segment, shard })
|
|
},
|
|
/**
|
|
* POST /api/user/memory-segment
|
|
* @param {number} segment A number from 0-99
|
|
* @param {*} data
|
|
* @param {string} shard
|
|
* @returns {Object}
|
|
*/
|
|
set (segment, data, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/user/memory-segment', { segment, data, shard })
|
|
}
|
|
}
|
|
},
|
|
/**
|
|
* GET /api/user/find?username={username}
|
|
* @param {string} username
|
|
* @returns {{ ok, user: { _id, username, badge: Badge, gcl } }}
|
|
*/
|
|
find (username) {
|
|
return self.req('GET', '/api/user/find', { username })
|
|
},
|
|
/**
|
|
* GET /api/user/find?id={userId}
|
|
* @param {string} id
|
|
* @returns {{ ok, user: { _id, username, badge: Badge, gcl } }}
|
|
*/
|
|
findById (id) {
|
|
return self.req('GET', '/api/user/find', { id })
|
|
},
|
|
/**
|
|
* GET /api/user/stats
|
|
* @param {number} interval
|
|
* @returns {Object}
|
|
*/
|
|
stats (interval) {
|
|
return self.req('GET', '/api/user/stats', { interval })
|
|
},
|
|
/**
|
|
* GET /api/user/rooms
|
|
* @param {string} id
|
|
* @returns {Object}
|
|
*/
|
|
rooms (id) {
|
|
return self.req('GET', '/api/user/rooms', { id }).then(self.mapToShard)
|
|
},
|
|
/**
|
|
* GET /api/user/overview?interval={interval}&statName={statName}
|
|
* @param {number} interval
|
|
* @param {string} statName energyControl
|
|
* @returns {{{ ok, rooms: [ <room name> ], stats: { <room name>: [ { value, endTime } ] }, statsMax }}}
|
|
*/
|
|
overview (interval, statName) {
|
|
return self.req('GET', '/api/user/overview', { interval, statName })
|
|
},
|
|
/**
|
|
* GET /api/user/money-history
|
|
* @param {number} page Used for pagination
|
|
* @returns {{"ok":1,"page":0,"list":[ { _id, date, tick, user, type, balance, change, market: {} } ] }}
|
|
* - page used for pagination.
|
|
* - hasMore is true if there are more pages to view.
|
|
* - market
|
|
* - New Order- { order: { type, resourceType, price, totalAmount, roomName } }
|
|
* - Extended Order- { extendOrder: { orderId, addAmount } }
|
|
* - Fulfilled Order- { resourceType, roomName, targetRoomName, price, npc, amount }
|
|
* - Price Change - { changeOrderPrice: { orderId, oldPrice, newPrice } }
|
|
*/
|
|
moneyHistory (page = 0) {
|
|
return self.req('GET', '/api/user/money-history', { page })
|
|
},
|
|
/**
|
|
* POST /api/user/console
|
|
* @param {*} expression
|
|
* @param {string} shard
|
|
* @returns {{ ok, result: { ok, n }, ops: [ { user, expression, _id } ], insertedCount, insertedIds: [ <mongodb id> ] }}
|
|
*/
|
|
console (expression, shard = DEFAULT_SHARD) {
|
|
return self.req('POST', '/api/user/console', { expression, shard })
|
|
},
|
|
/**
|
|
* GET /api/user/name
|
|
* @returns {Object}
|
|
*/
|
|
name () {
|
|
return self.req('GET', '/api/user/name')
|
|
}
|
|
},
|
|
experimental: {
|
|
// https://screeps.com/api/experimental/pvp?start=14787157 seems to not be implemented in the api
|
|
/**
|
|
* @param {number} interval
|
|
* @returns {{ ok, time, rooms: [ { _id, lastPvpTime } ] }}
|
|
* time is the current server tick
|
|
* _id contains the room name for each room, and lastPvpTime contains the last tick pvp occurred
|
|
* if neither a valid interval nor a valid start argument is provided, the result of the call is still ok, but with an empty rooms array.
|
|
*/
|
|
pvp (interval = 100) {
|
|
return self.req('GET', '/api/experimental/pvp', { interval }).then(self.mapToShard)
|
|
},
|
|
/**
|
|
* GET /api/experimental/nukes
|
|
* @returns {Object}
|
|
*/
|
|
nukes () {
|
|
return self.req('GET', '/api/experimental/nukes').then(self.mapToShard)
|
|
}
|
|
},
|
|
warpath: {
|
|
/**
|
|
* GET /api/warpath/battles
|
|
* @param {number} interval
|
|
* @returns {Object}
|
|
*/
|
|
battles (interval = 100) {
|
|
return self.req('GET', '/api/warpath/battles', { interval })
|
|
}
|
|
},
|
|
scoreboard: {
|
|
/**
|
|
* GET /api/scoreboard/list
|
|
* @param {number} limit
|
|
* @param {number} offset
|
|
* @returns {Object}
|
|
*/
|
|
list (limit = 20, offset = 0) {
|
|
return self.req('GET', '/api/scoreboard/list', { limit, offset })
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
currentSeason () {
|
|
const now = new Date();
|
|
const year = now.getFullYear();
|
|
let month = (now.getUTCMonth() + 1).toString();
|
|
if (month.length === 1) month = `0${month}`;
|
|
return `${year}-${month}`
|
|
}
|
|
|
|
isOfficialServer () {
|
|
return this.opts.url.match(/screeps\.com/) !== null
|
|
}
|
|
|
|
mapToShard (res) {
|
|
if (!res.shards) {
|
|
res.shards = {
|
|
privSrv: res.list || res.rooms
|
|
};
|
|
}
|
|
return res
|
|
}
|
|
|
|
setServer (opts) {
|
|
if (!this.opts) {
|
|
this.opts = {};
|
|
}
|
|
Object.assign(this.opts, opts);
|
|
if (opts.path && !opts.pathname) {
|
|
this.opts.pathname = this.opts.path;
|
|
}
|
|
if (!opts.url) {
|
|
this.opts.url = format(this.opts);
|
|
if (!this.opts.url.endsWith('/')) this.opts.url += '/';
|
|
}
|
|
if (opts.token) {
|
|
this.token = opts.token;
|
|
}
|
|
this.http = axios__default['default'].create({
|
|
baseURL: this.opts.url
|
|
});
|
|
}
|
|
|
|
async auth (email, password, opts = {}) {
|
|
this.setServer(opts);
|
|
if (email && password) {
|
|
Object.assign(this.opts, { email, password });
|
|
}
|
|
const res = await this.raw.auth.signin(this.opts.email, this.opts.password);
|
|
this.emit('token', res.token);
|
|
this.emit('auth');
|
|
this.__authed = true;
|
|
return res
|
|
}
|
|
|
|
async req (method, path, body = {}) {
|
|
const opts = {
|
|
method,
|
|
url: path,
|
|
headers: {}
|
|
};
|
|
debugHttp(`${method} ${path} ${JSON.stringify(body)}`);
|
|
if (this.token) {
|
|
Object.assign(opts.headers, {
|
|
'X-Token': this.token,
|
|
'X-Username': this.token
|
|
});
|
|
}
|
|
if (method === 'GET') {
|
|
opts.params = body;
|
|
} else {
|
|
opts.data = body;
|
|
}
|
|
try {
|
|
const res = await this.http(opts);
|
|
const token = res.headers['x-token'];
|
|
if (token) {
|
|
this.emit('token', token);
|
|
}
|
|
const rateLimit = this.buildRateLimit(method, path, res);
|
|
this.emit('rateLimit', rateLimit);
|
|
debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
|
|
if (typeof res.data.data === 'string' && res.data.data.slice(0, 3) === 'gz:') {
|
|
res.data.data = await this.gz(res.data.data);
|
|
}
|
|
this.emit('response', res);
|
|
return res.data
|
|
} catch (err) {
|
|
const res = err.response || {};
|
|
const rateLimit = this.buildRateLimit(method, path, res);
|
|
this.emit('rateLimit', rateLimit);
|
|
debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
|
|
if (res.status === 401) {
|
|
if (this.__authed && this.opts.email && this.opts.password) {
|
|
this.__authed = false;
|
|
await this.auth(this.opts.email, this.opts.password);
|
|
return this.req(method, path, body)
|
|
} else {
|
|
throw new Error('Not Authorized')
|
|
}
|
|
}
|
|
if (res.status === 429 && !res.headers['x-ratelimit-limit'] && this.opts.experimentalRetry429) {
|
|
await sleep(Math.floor(Math.random() * 500) + 200);
|
|
return this.req(method, path, body)
|
|
}
|
|
if (err.response) {
|
|
throw new Error(res.data)
|
|
}
|
|
throw new Error(err.message)
|
|
}
|
|
}
|
|
|
|
async gz (data) {
|
|
const buf = Buffer.from(data.slice(3), 'base64');
|
|
const ret = await gunzipAsync(buf);
|
|
return JSON.parse(ret.toString())
|
|
}
|
|
|
|
async inflate (data) { // es
|
|
const buf = Buffer.from(data.slice(3), 'base64');
|
|
const ret = await inflateAsync(buf);
|
|
return JSON.parse(ret.toString())
|
|
}
|
|
|
|
buildRateLimit (method, path, res) {
|
|
const {
|
|
headers: {
|
|
'x-ratelimit-limit': limit,
|
|
'x-ratelimit-remaining': remaining,
|
|
'x-ratelimit-reset': reset
|
|
} = {}
|
|
} = res;
|
|
return {
|
|
method,
|
|
path,
|
|
limit: +limit,
|
|
remaining: +remaining,
|
|
reset: +reset,
|
|
toReset: reset - Math.floor(Date.now() / 1000)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {{
|
|
* "color1": string;
|
|
* "color2": string;
|
|
* "color3": string;
|
|
* "flip": boolean;
|
|
* "param": number;
|
|
* "type": number|{ path1:string, path2:string};
|
|
*}} Badge
|
|
*/
|
|
|
|
/**
|
|
* @typedef {1|2|3|4|5|6|7|8|9|10} FlagColor
|
|
* - Red = 1,
|
|
* - Purple = 2,
|
|
* - Blue = 3,
|
|
* - Cyan = 4,
|
|
* - Green = 5,
|
|
* - Yellow = 6,
|
|
* - Orange = 7,
|
|
* - Brown = 8,
|
|
* - Grey = 9,
|
|
* - White = 10
|
|
*/
|
|
|
|
const readFileAsync = util__default['default'].promisify(fs__default['default'].readFile);
|
|
|
|
class ConfigManager {
|
|
async refresh () {
|
|
this._config = null;
|
|
await this.getConfig();
|
|
}
|
|
|
|
async getServers () {
|
|
const conf = await this.getConfig();
|
|
return Object.keys(conf.servers)
|
|
}
|
|
|
|
async getConfig () {
|
|
if (this._config) {
|
|
return this._config
|
|
}
|
|
const paths = [];
|
|
if (process.env.SCREEPS_CONFIG) {
|
|
paths.push(process.env.SCREEPS_CONFIG);
|
|
}
|
|
const dirs = [__dirname, ''];
|
|
for (const dir of dirs) {
|
|
paths.push(path__default['default'].join(dir, '.screeps.yaml'));
|
|
paths.push(path__default['default'].join(dir, '.screeps.yml'));
|
|
}
|
|
if (process.platform === 'win32') {
|
|
paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yaml'));
|
|
paths.push(path__default['default'].join(process.env.APPDATA, 'screeps/config.yml'));
|
|
} else {
|
|
if (process.env.XDG_CONFIG_PATH) {
|
|
paths.push(
|
|
path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yaml')
|
|
);
|
|
paths.push(
|
|
path__default['default'].join(process.env.XDG_CONFIG_HOME, 'screeps/config.yml')
|
|
);
|
|
}
|
|
if (process.env.HOME) {
|
|
paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yaml'));
|
|
paths.push(path__default['default'].join(process.env.HOME, '.config/screeps/config.yml'));
|
|
paths.push(path__default['default'].join(process.env.HOME, '.screeps.yaml'));
|
|
paths.push(path__default['default'].join(process.env.HOME, '.screeps.yml'));
|
|
}
|
|
}
|
|
for (const path of paths) {
|
|
const data = await this.loadConfig(path);
|
|
if (data) {
|
|
if (!data.servers) {
|
|
throw new Error(
|
|
`Invalid config: 'servers' object does not exist in '${path}'`
|
|
)
|
|
}
|
|
this._config = data;
|
|
this.path = path;
|
|
return data
|
|
}
|
|
}
|
|
return null
|
|
}
|
|
|
|
async loadConfig (file) {
|
|
try {
|
|
const contents = await readFileAsync(file, 'utf8');
|
|
return YAML__default['default'].parse(contents)
|
|
} catch (e) {
|
|
if (e.code === 'ENOENT') {
|
|
return false
|
|
} else {
|
|
throw e
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const DEFAULTS = {
|
|
protocol: 'https',
|
|
hostname: 'screeps.com',
|
|
port: 443,
|
|
path: '/'
|
|
};
|
|
|
|
const configManager = new ConfigManager();
|
|
|
|
class ScreepsAPI extends RawAPI {
|
|
static async fromConfig (server = 'main', config = false, opts = {}) {
|
|
const data = await configManager.getConfig();
|
|
|
|
if (data) {
|
|
if (!data.servers[server]) {
|
|
throw new Error(`Server '${server}' does not exist in '${configManager.path}'`)
|
|
}
|
|
|
|
const conf = data.servers[server];
|
|
if (conf.ptr) conf.path = '/ptr';
|
|
if (conf.season) conf.path = '/season';
|
|
const api = new ScreepsAPI(
|
|
Object.assign(
|
|
{
|
|
hostname: conf.host,
|
|
port: conf.port,
|
|
protocol: conf.secure ? 'https' : 'http',
|
|
token: conf.token,
|
|
path: conf.path || '/'
|
|
},
|
|
opts
|
|
)
|
|
);
|
|
|
|
api.appConfig = (data.configs && data.configs[config]) || {};
|
|
|
|
if (!conf.token && conf.username && conf.password) {
|
|
await api.auth(conf.username, conf.password);
|
|
}
|
|
|
|
return api
|
|
}
|
|
|
|
throw new Error('No valid config found')
|
|
}
|
|
|
|
constructor (opts) {
|
|
opts = Object.assign({}, DEFAULTS, opts);
|
|
super(opts);
|
|
this.on('token', token => {
|
|
this.token = token;
|
|
this.raw.token = token;
|
|
});
|
|
const defaultLimit = (limit, period) => ({
|
|
limit,
|
|
period,
|
|
remaining: limit,
|
|
reset: 0,
|
|
toReset: 0
|
|
});
|
|
this.rateLimits = {
|
|
global: defaultLimit(120, 'minute'),
|
|
GET: {
|
|
'/api/game/room-terrain': defaultLimit(360, 'hour'),
|
|
'/api/user/code': defaultLimit(60, 'hour'),
|
|
'/api/user/memory': defaultLimit(1440, 'day'),
|
|
'/api/user/memory-segment': defaultLimit(360, 'hour'),
|
|
'/api/game/market/orders-index': defaultLimit(60, 'hour'),
|
|
'/api/game/market/orders': defaultLimit(60, 'hour'),
|
|
'/api/game/market/my-orders': defaultLimit(60, 'hour'),
|
|
'/api/game/market/stats': defaultLimit(60, 'hour'),
|
|
'/api/game/user/money-history': defaultLimit(60, 'hour')
|
|
},
|
|
POST: {
|
|
'/api/user/console': defaultLimit(360, 'hour'),
|
|
'/api/game/map-stats': defaultLimit(60, 'hour'),
|
|
'/api/user/code': defaultLimit(240, 'day'),
|
|
'/api/user/set-active-branch': defaultLimit(240, 'day'),
|
|
'/api/user/memory': defaultLimit(240, 'day'),
|
|
'/api/user/memory-segment': defaultLimit(60, 'hour')
|
|
}
|
|
};
|
|
this.on('rateLimit', limits => {
|
|
const rate =
|
|
this.rateLimits[limits.method][limits.path] || this.rateLimits.global;
|
|
const copy = Object.assign({}, limits);
|
|
delete copy.path;
|
|
delete copy.method;
|
|
Object.assign(rate, copy);
|
|
});
|
|
this.socket = new Socket(this);
|
|
}
|
|
|
|
getRateLimit (method, path) {
|
|
return this.rateLimits[method][path] || this.rateLimits.global
|
|
}
|
|
|
|
get rateLimitResetUrl () {
|
|
return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice(
|
|
0,
|
|
8
|
|
)}`
|
|
}
|
|
|
|
async me () {
|
|
if (this._user) return this._user
|
|
const tokenInfo = await this.tokenInfo();
|
|
if (tokenInfo.full) {
|
|
this._user = await this.raw.auth.me();
|
|
} else {
|
|
const { username } = await this.raw.user.name();
|
|
const { user } = await this.raw.user.find(username);
|
|
this._user = user;
|
|
}
|
|
return this._user
|
|
}
|
|
|
|
async tokenInfo () {
|
|
if (this._tokenInfo) {
|
|
return this._tokenInfo
|
|
}
|
|
if (this.opts.token) {
|
|
const { token } = await this.raw.auth.queryToken(this.token);
|
|
this._tokenInfo = token;
|
|
} else {
|
|
this._tokenInfo = { full: true };
|
|
}
|
|
return this._tokenInfo
|
|
}
|
|
|
|
async userID () {
|
|
const user = await this.me();
|
|
return user._id
|
|
}
|
|
|
|
get history () {
|
|
return this.raw.history
|
|
}
|
|
|
|
get authmod () {
|
|
return this.raw.authmod
|
|
}
|
|
|
|
get version () {
|
|
return this.raw.version
|
|
}
|
|
|
|
get time () {
|
|
return this.raw.game.time
|
|
}
|
|
|
|
get leaderboard () {
|
|
return this.raw.leaderboard
|
|
}
|
|
|
|
get market () {
|
|
return this.raw.game.market
|
|
}
|
|
|
|
get registerUser () {
|
|
return this.raw.register.submit
|
|
}
|
|
|
|
get code () {
|
|
return this.raw.user.code
|
|
}
|
|
|
|
get memory () {
|
|
return this.raw.user.memory
|
|
}
|
|
|
|
get segment () {
|
|
return this.raw.user.memory.segment
|
|
}
|
|
|
|
get console () {
|
|
return this.raw.user.console
|
|
}
|
|
}
|
|
|
|
exports.ScreepsAPI = ScreepsAPI;
|