├── src ├── index.js ├── ws-browser.js ├── ConfigManager.js ├── ScreepsAPI.js ├── Socket.js └── RawAPI.js ├── FUNDING.yml ├── .npmignore ├── .gitignore ├── examples ├── README.md ├── download_memory.js ├── download_code.js ├── dev_test.js └── console.js ├── test ├── credentials.js ├── api.raw.register.js ├── api.raw.userMessage.js ├── api.raw.auth.js ├── api.raw.leaderboard.js ├── api.raw.general.js ├── api.raw.game.js ├── api.general.js └── api.raw.user.js ├── tsconfig.json ├── rollup.config.js ├── LICENSE ├── .circleci └── config.yml ├── package.json ├── README.md ├── bin └── screeps-api.js └── docs ├── Websocket_endpoints.md └── Endpoints.md /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './ScreepsAPI.js' 2 | -------------------------------------------------------------------------------- /FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: ags131 2 | github: [ags131] 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | node_modules 3 | auth.js 4 | src 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-* 2 | node_modules 3 | auth.js 4 | dist 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | Warning: These examples may be outdated and plain broken. -------------------------------------------------------------------------------- /test/credentials.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | username: 'screeps-api-testing', 3 | password: 'mG3r3TIRbDnSrraGnOdIQyBek1hfxu', 4 | protocol: 'https', 5 | hostname: 'server1.screepspl.us', 6 | port: 443, 7 | }; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "dist/types", 5 | "rootDir": "src", 6 | "allowJs": true, 7 | }, 8 | "include": ["src/**/*"], 9 | "exclude": ["node_modules/**/*"] 10 | } -------------------------------------------------------------------------------- /examples/download_memory.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { ScreepsAPI } = require('../') 3 | const auth = require('../auth') 4 | const fs = require('fs') 5 | 6 | let api = new ScreepsAPI() 7 | 8 | Promise.resolve() 9 | .then(()=>api.auth(auth.email,auth.password)) 10 | .then(()=>api.memory.get()) 11 | .then(memory=>{ 12 | fs.writeFileSync('memory.json',JSON.stringify(memory)) 13 | }) 14 | .catch(err=>console.error(err)) 15 | -------------------------------------------------------------------------------- /src/ws-browser.js: -------------------------------------------------------------------------------- 1 | const WebSocket = module.exports = window.WebSocket || require('ws') 2 | 3 | WebSocket.prototype.on = function (event, callback) { 4 | this.addEventListener(event, callback) 5 | } 6 | 7 | WebSocket.prototype.off = function (event, callback) { 8 | this.removeEventListener(event, callback) 9 | } 10 | 11 | WebSocket.prototype.once = function (event, callback) { 12 | const self = this 13 | this.addEventListener(event, function handler () { 14 | callback.apply(callback, arguments) 15 | self.removeEventListener(event, handler) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2' 2 | 3 | export default { 4 | input: { 5 | ScreepsAPI: 'src/index.js' 6 | }, 7 | output: { 8 | dir: 'dist', 9 | format: 'cjs', 10 | exports: 'named', 11 | preferConst: true, 12 | globals: { 13 | ws: 'WebSocket' 14 | }, 15 | }, 16 | // external(id){ 17 | // return !!require('./package.json').dependencies[id]; 18 | // }, 19 | external: ['ws', 'fs', 'axios', 'bluebird', 'yamljs', 'url', 'events', 'zlib', 'path','debug', 'util'], 20 | plugins: [ 21 | typescript({ 22 | useTsconfigDeclarationDir: true 23 | }) 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /examples/download_code.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const ScreepsAPI = require('../') 3 | const auth = require('../auth') 4 | const WebSocket = require('ws') 5 | const fs = require('fs') 6 | 7 | let api = new ScreepsAPI(auth) 8 | 9 | api.socket(() => { 10 | }) 11 | 12 | api.on('message', (msg) => { 13 | // console.log('MSG', msg) 14 | if (msg.slice(0, 7) == 'auth ok') { 15 | api.subscribe('/code') 16 | } 17 | }) 18 | 19 | // Upload your code to trigger this. 20 | api.on('code', (msg)=>{ 21 | let [user, data] = msg 22 | fs.mkdirSync(data.branch) 23 | for(let mod in data.modules){ 24 | let file = `${data.branch}/${mod}.js` 25 | fs.writeFileSync(file,data.modules[mod]) 26 | console.log('Wrote',file) 27 | } 28 | }) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/api.raw.register.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.register', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.checkEmail (email)', function() { 12 | it('should do untested things (for now)') 13 | }) 14 | 15 | describe('.checkUsername (username)', function() { 16 | it('should do untested things (for now)') 17 | }) 18 | 19 | describe('.setUsername (username)', function() { 20 | it('should do untested things (for now)') 21 | }) 22 | 23 | describe('.submit (username, email, password, modules)', function() { 24 | it('should do untested things (for now)') 25 | }) 26 | 27 | }) -------------------------------------------------------------------------------- /test/api.raw.userMessage.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.userMessages', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.list (respondent)', function() { 12 | it('should do untested things (for now)') 13 | }) 14 | 15 | describe('.index ()', function() { 16 | it('should do untested things (for now)') 17 | }) 18 | 19 | describe('.unreadCount ()', function() { 20 | it('should do untested things (for now)') 21 | }) 22 | 23 | describe('.send (respondent, text)', function() { 24 | it('should do untested things (for now)') 25 | }) 26 | 27 | describe('.markRead (id)', function() { 28 | it('should do untested things (for now)') 29 | }) 30 | 31 | }) -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | working_directory: ~/repo 5 | docker: 6 | - image: circleci/node:12 7 | steps: 8 | - checkout 9 | - restore_cache: 10 | key: dependency-cache-{{ checksum "package.json" }} 11 | - run: 12 | name: install 13 | command: yarn install 14 | - save_cache: 15 | key: dependency-cache-{{ checksum "package.json" }} 16 | paths: 17 | - ./node_modules 18 | - run: 19 | name: lint 20 | command: yarn lint 21 | - run: 22 | name: test 23 | command: yarn test 24 | - store_artifacts: 25 | path: test-results.xml 26 | prefix: tests 27 | - store_test_results: 28 | path: test-results.xml 29 | - deploy: 30 | name: Maybe Deploy 31 | command: | 32 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 33 | echo "//registry.npmjs.org/:_authToken=$NPM_TOKEN" >> ~/.npmrc 34 | npm run 2npm 35 | fi 36 | workflows: 37 | version: 2 38 | build: 39 | jobs: 40 | - build: 41 | context: screeps -------------------------------------------------------------------------------- /examples/dev_test.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { ScreepsAPI } = require('../') 3 | const auth = require('../auth') 4 | const WebSocket = require('ws') 5 | 6 | let api = new ScreepsAPI() 7 | 8 | Promise.resolve() 9 | .then(()=>api.auth(auth.email,auth.password)) 10 | .then(()=>api.socket.connect()) 11 | .then(()=>{ 12 | api.socket.subscribe('console') 13 | api.socket.subscribe('cpu') 14 | }) 15 | .catch((err)=>{ 16 | console.error('err',err) 17 | }) 18 | 19 | let socketEvents = ['connected','disconnected','message','auth','time','protocol','package','subscribe','unsubscribe','console'] 20 | socketEvents.forEach(ev=>{ 21 | api.socket.on(ev,(data)=>{ 22 | console.log(ev,data) 23 | }) 24 | }) 25 | 26 | api.socket.on('disconnected',()=>{ 27 | api.socket.connect() 28 | }) 29 | 30 | // api.socket.on('console', (msg) => { 31 | // // console.log('CONSOLE', msg) 32 | // let { data } = msg 33 | // if (data.messages) data.messages.log.forEach(l => console.log('console',l)) 34 | // if (data.messages) data.messages.results.forEach(l => console.log('console >', l)) 35 | // if (data.error) console.log('error', data.error) 36 | // }) 37 | 38 | process.on('unhandledRejection', (reason) => { 39 | console.error('err',reason); 40 | process.exit(1); 41 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "screeps-api", 3 | "version": "1.16.1", 4 | "description": "", 5 | "repository": "screepers/node-screeps-api", 6 | "main": "dist/ScreepsAPI.js", 7 | "module": "src/index.js", 8 | "scripts": { 9 | "build": "rollup -c", 10 | "prepublishOnly": "npm run build", 11 | "lint": "standard src/**/*.js", 12 | "lint-fix": "standard src/**/*.js --fix", 13 | "test_": "mocha", 14 | "test": "echo Tests disabled, TODO: FIX THIS!", 15 | "2npm": "publish-if-not-published" 16 | }, 17 | "author": "Adam Shumann", 18 | "license": "ISC", 19 | "bin": { 20 | "screeps-api": "bin/screeps-api.js" 21 | }, 22 | "dependencies": { 23 | "axios": "^0.28.0", 24 | "commander": "^7.2.0", 25 | "debug": "^4.1.1", 26 | "ws": "^7.4.4", 27 | "yamljs": "^0.3.0" 28 | }, 29 | "optionalDependencies": { 30 | "bufferutil": "^4.0.1", 31 | "utf-8-validate": "^5.0.2" 32 | }, 33 | "devDependencies": { 34 | "lodash": "^4.17.11", 35 | "mocha": "^8.3.2", 36 | "publish-if-not-published": "^2.0.0", 37 | "rollup": "^2.44.0", 38 | "rollup-plugin-typescript2": "^0.30.0", 39 | "standard": "*", 40 | "tslib": "^2.2.0", 41 | "typescript": "^4.2.4" 42 | }, 43 | "browser": { 44 | "ws": "./src/ws-browser.js" 45 | }, 46 | "types": "dist/types/index.d.ts" 47 | } 48 | -------------------------------------------------------------------------------- /test/api.raw.auth.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.auth', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.signin (email, password)', function() { 12 | it('should send a POST request to /api/auth/signin and authenticate', async function() { 13 | let opts = _.omit(auth, ['username', 'password']) 14 | let api = new ScreepsAPI(opts) 15 | let res = await api.raw.auth.signin(auth.username, auth.password) 16 | assert(_.has(res, 'token'), 'no token found in server answer') 17 | assert.equal(res.ok, 1, 'res.ok is incorrect') 18 | }) 19 | it('should reject promise if unauthorized', async function() { 20 | try { 21 | let api = new ScreepsAPI() 22 | await api.raw.auth.signin(auth.username, 'invalid_password') 23 | } catch (err) { 24 | assert(err.message.match(/Not authorized/i), 'wrong error message') 25 | } 26 | }) 27 | }) 28 | 29 | describe('.steamTicket (ticket, useNativeAuth = false)', function() { 30 | it('should do things... but I\'m not sure what exactly...') 31 | }) 32 | 33 | describe('.me ()', function() { 34 | it('should return user informations from `/api/auth/me` endpoint', async function() { 35 | let opts = _.omit(auth, ['username', 'password']) 36 | let api = new ScreepsAPI(opts) 37 | await api.auth(auth.username, auth.password) 38 | let res = await api.raw.auth.me() 39 | assert(_.has(res, 'email'), 'response has no email field') 40 | assert(_.has(res, 'badge'), 'response has no badge field') 41 | assert(_.has(res, 'username'), 'response has no username field') 42 | }) 43 | }) 44 | 45 | }) -------------------------------------------------------------------------------- /src/ConfigManager.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import util from 'util' 3 | import YAML from 'yamljs' 4 | import path from 'path' 5 | 6 | const readFileAsync = util.promisify(fs.readFile) 7 | 8 | export class ConfigManager { 9 | async refresh () { 10 | this._config = null 11 | await this.getConfig() 12 | } 13 | 14 | async getServers () { 15 | const conf = await this.getConfig() 16 | return Object.keys(conf.servers) 17 | } 18 | 19 | async getConfig () { 20 | if (this._config) { 21 | return this._config 22 | } 23 | const paths = [] 24 | if (process.env.SCREEPS_CONFIG) { 25 | paths.push(process.env.SCREEPS_CONFIG) 26 | } 27 | const dirs = [__dirname, ''] 28 | for (const dir of dirs) { 29 | paths.push(path.join(dir, '.screeps.yaml')) 30 | paths.push(path.join(dir, '.screeps.yml')) 31 | } 32 | if (process.platform === 'win32') { 33 | paths.push(path.join(process.env.APPDATA, 'screeps/config.yaml')) 34 | paths.push(path.join(process.env.APPDATA, 'screeps/config.yml')) 35 | } else { 36 | if (process.env.XDG_CONFIG_PATH) { 37 | paths.push( 38 | path.join(process.env.XDG_CONFIG_HOME, 'screeps/config.yaml') 39 | ) 40 | paths.push( 41 | path.join(process.env.XDG_CONFIG_HOME, 'screeps/config.yml') 42 | ) 43 | } 44 | if (process.env.HOME) { 45 | paths.push(path.join(process.env.HOME, '.config/screeps/config.yaml')) 46 | paths.push(path.join(process.env.HOME, '.config/screeps/config.yml')) 47 | paths.push(path.join(process.env.HOME, '.screeps.yaml')) 48 | paths.push(path.join(process.env.HOME, '.screeps.yml')) 49 | } 50 | } 51 | for (const path of paths) { 52 | const data = await this.loadConfig(path) 53 | if (data) { 54 | if (!data.servers) { 55 | throw new Error( 56 | `Invalid config: 'servers' object does not exist in '${path}'` 57 | ) 58 | } 59 | this._config = data 60 | this.path = path 61 | return data 62 | } 63 | } 64 | return null 65 | } 66 | 67 | async loadConfig (file) { 68 | try { 69 | const contents = await readFileAsync(file, 'utf8') 70 | return YAML.parse(contents) 71 | } catch (e) { 72 | if (e.code === 'ENOENT') { 73 | return false 74 | } else { 75 | throw e 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /test/api.raw.leaderboard.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.leaderboard', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.list ()', function() { 12 | it('should call /api/leaderboard/list endpoint and return leaderboard inforamtion', async function() { 13 | let opts = _.omit(auth, ['username', 'password']) 14 | let api = new ScreepsAPI(opts) 15 | let res = await api.raw.leaderboard.list() 16 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 17 | assert(_.has(res, 'list'), 'server response should have a list field') 18 | assert(_.has(res, 'count'), 'server response should have a count field') 19 | assert(_.has(res, 'users'), 'server response should have a users field') 20 | if (api.opts.url.includes('screeps.com')) { 21 | assert(_.size(res.list) > 0, 'leaderboard list is empty') 22 | assert(_.size(res.users) > 0, 'leaderboard users is empty') 23 | assert(res.count > 0, 'leaderboard count equals 0 (or maybe is negative)') 24 | } 25 | }) 26 | it('should return leaderboard data based on world or power stats', async function() { 27 | let opts = _.omit(auth, ['username', 'password']) 28 | let api = new ScreepsAPI(opts) 29 | let res1 = await api.raw.leaderboard.list(10, 'world') 30 | let res2 = await api.raw.leaderboard.list(10, 'power') 31 | if (api.opts.url.includes('screeps.com')) { 32 | assert.notEqual(_.first(res1.list), _.first(res2.list), 'same player shouldn\'t be #1') 33 | } 34 | }) 35 | it('should return paginated data', async function() { 36 | let opts = _.omit(auth, ['username', 'password']) 37 | let api = new ScreepsAPI(opts) 38 | let res1 = await api.raw.leaderboard.list(5, 'world') 39 | let res2 = await api.raw.leaderboard.list(10, 'world') 40 | let res3 = await api.raw.leaderboard.list(10, 'world', 9) 41 | if (api.opts.url.includes('screeps.com')) { 42 | assert.equal(_.size(res1.list), 5, 'requested top 5 and got a shorter or longer list') 43 | assert.equal(_.size(res2.list), 10, 'requested top 10 and got a shorter or longer list') 44 | assert.notEqual(_.first(res1.list).user, _.first(res3.list).user, 'offset is not working') 45 | assert.equal(_.first(res1.list).user, _.first(res2.list).user, 'player #1 is incoherent') 46 | assert.equal(_.last(res2.list).user, _.first(res3.list).user, 'player #9 is incoherent') 47 | } 48 | }) 49 | }) 50 | 51 | describe('.find (username, mode = \'world\', season = \'\')', function() { 52 | it('should do untested things (for now)') 53 | }) 54 | 55 | describe('.seasons ()', function() { 56 | it('should do untested things (for now)') 57 | }) 58 | 59 | }) -------------------------------------------------------------------------------- /test/api.raw.general.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.version()', function() { 12 | it('should call /api/version endpoint and return version information', async function() { 13 | let opts = _.omit(auth, ['username', 'password']) 14 | let api = new ScreepsAPI(opts) 15 | let res = await api.raw.version() 16 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 17 | assert(_.has(res, 'protocol'), 'response has no protocol field') 18 | assert(_.has(res, 'serverData.historyChunkSize'), 'response has no serverData.historyChunkSize field') 19 | if (api.opts.hostname === 'screeps.com') { 20 | assert(_.has(res, 'package'), 'response has no package field') 21 | assert(_.has(res, 'serverData.shards'), 'response has no serverData.shards field') 22 | } 23 | }) 24 | }) 25 | 26 | describe('.authmod()', function() { 27 | it('should return server name from /authmod for private servers with authmod', async function() { 28 | let opts = _.omit(auth, ['username', 'password']) 29 | let api = new ScreepsAPI(opts) 30 | let res = await api.raw.authmod() 31 | if (api.opts.hostname === 'screeps.com') { 32 | assert.equal(res.name, 'official', 'invalid name for official server') 33 | } else { 34 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 35 | assert(_.has(res, 'name'), 'server response should have a name field') 36 | assert(_.has(res, 'version'), 'server response should have a version field') 37 | } 38 | }) 39 | }) 40 | 41 | // This API is not implemented for private servers 42 | describe.skip('.history(room, tick)', function() { 43 | it('should return room history as a json file', async function() { 44 | let opts = _.omit(auth, ['username', 'password']) 45 | let api = new ScreepsAPI(opts) 46 | // Get current tick (as history is not kept forever) 47 | let res = await api.raw.game.time('shard1') 48 | let time = res.time - 1000 // history is not available right away 49 | // Make sure that time is not a multiple of 20 or 100 50 | time = (time % 20 === 0) ? time - 10 : time 51 | // Try to get history for W1N1 52 | let json = await api.raw.history('W1N1', time, 'shard1') 53 | // Verify results 54 | assert(_.has(json, 'ticks'), 'result has no ticks field') 55 | assert(_.size(json.ticks) >= 20, 'results are incomplete ; official server usually returns 100 ticks and private servers should return at least 20 ticks') 56 | assert.equal(json.room, 'W1N1', 'result room is incorrect') 57 | assert(_.has(json, 'timestamp'), 'result has no timestamp field') 58 | assert(_.has(json, 'base'), 'result has no base field') 59 | }) 60 | }) 61 | 62 | }) 63 | -------------------------------------------------------------------------------- /examples/console.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const { ScreepsAPI } = require('../') 3 | // const auth = require('../auth') 4 | const readline = require('readline') 5 | const util = require('util') 6 | 7 | const rl = readline.createInterface({ 8 | input: process.stdin, 9 | output: process.stdout, 10 | prompt: 'Screeps> ' 11 | }) 12 | 13 | let auth = {} 14 | let api = new ScreepsAPI() 15 | 16 | if (process.argv.length == 3) { 17 | auth = require(process.argv[2]) 18 | } 19 | 20 | Promise.resolve(auth) 21 | .then(getEmail) 22 | .then(getPassword) 23 | .then(connect) 24 | .then(start) 25 | .catch(() => { 26 | process.exit(1) 27 | }) 28 | 29 | function connect (auth) { 30 | return new Promise((resolve, reject) => { 31 | console.log('Authenticating...') 32 | api.auth(auth.email, auth.password, (err, result) => { 33 | if (result) { 34 | console.log('Authentication succeeded') 35 | resolve() 36 | }else { 37 | console.log('Authentication failed') 38 | reject() 39 | } 40 | }) 41 | }) 42 | } 43 | 44 | function start () { 45 | return new Promise((resolve, reject) => { 46 | run() 47 | api.socket(() => { 48 | console.log('start') 49 | rl.prompt() 50 | }) 51 | }) 52 | } 53 | 54 | function getEmail (auth) { 55 | if (auth.email) return auth 56 | return new Promise((resolve, reject) => { 57 | rl.question('Screeps Email: ', (email) => { 58 | auth.email = email.trim() 59 | resolve(auth) 60 | }) 61 | }) 62 | } 63 | 64 | function getPassword (auth) { 65 | if (auth.password) return auth 66 | return new Promise((resolve, reject) => { 67 | rl.question('Screeps Password: ', (password) => { 68 | auth.password = password.trim() 69 | resolve(auth) 70 | }) 71 | }) 72 | } 73 | 74 | function run () { 75 | rl.on('line', (line) => { 76 | line = line.trim() 77 | if (line == 'exit') { 78 | console.log('Bye') 79 | process.exit() 80 | return 81 | } 82 | api.console(line) 83 | }) 84 | 85 | rl.on('close', () => { 86 | console.log('Bye') 87 | process.exit() 88 | return 89 | }) 90 | 91 | api.on('message', (msg) => { 92 | console.log(msg) 93 | if (msg.slice(0, 7) == 'auth ok') { 94 | api.subscribe('/console') 95 | console.log('Console connected'.green) 96 | } 97 | }) 98 | 99 | api.on('console', (msg) => { 100 | let [user, data] = msg 101 | if (data.messages) data.messages.log.forEach(l => console.log(l)) 102 | if (data.messages) data.messages.results.forEach(l => console.log('>', l.gray)) 103 | if (data.error) console.log(data.error.red) 104 | }) 105 | } 106 | 107 | // Console fix 108 | var fu = function (type, args) { 109 | var t = Math.ceil((rl.line.length + 3) / process.stdout.columns) 110 | var text = util.format.apply(console, args) 111 | rl.output.write('\n\x1B[' + t + 'A\x1B[0J') 112 | rl.output.write(text + '\n') 113 | rl.output.write(new Array(t).join('\n\x1B[E')) 114 | rl._refreshLine() 115 | } 116 | 117 | console.log = function () { 118 | fu('log', arguments) 119 | } 120 | console.warn = function () { 121 | fu('warn', arguments) 122 | } 123 | console.info = function () { 124 | fu('info', arguments) 125 | } 126 | console.error = function () { 127 | fu('error', arguments) 128 | } 129 | -------------------------------------------------------------------------------- /test/api.raw.game.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.userMessages', function() { 7 | 8 | this.slow(2000); 9 | this.timeout(5000); 10 | 11 | describe('.mapStats (rooms, statName, shard = DEFAULT_SHARD)', function() { 12 | it('should do untested things (for now)') 13 | }) 14 | 15 | describe('.genUniqueObjectName (type, shard = DEFAULT_SHARD)', function() { 16 | it('should do untested things (for now)') 17 | }) 18 | 19 | describe('.checkUniqueObjectName (type, name, shard = DEFAULT_SHARD)', function() { 20 | it('should do untested things (for now)') 21 | }) 22 | 23 | describe('.placeSpawn (room, x, y, name, shard = DEFAULT_SHARD)', function() { 24 | it('should do untested things (for now)') 25 | }) 26 | 27 | describe('.createFlag (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD)', function() { 28 | it('should do untested things (for now)') 29 | }) 30 | 31 | describe('.genUniqueFlagName (shard = DEFAULT_SHARD)', function() { 32 | it('should do untested things (for now)') 33 | }) 34 | 35 | describe('.checkUniqueFlagName (name, shard = DEFAULT_SHARD)', function() { 36 | it('should do untested things (for now)') 37 | }) 38 | 39 | describe('.changeFlagColor (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD)', function() { 40 | it('should do untested things (for now)') 41 | }) 42 | 43 | describe('.removeFlag (room, name, shard = DEFAULT_SHARD)', function() { 44 | it('should do untested things (for now)') 45 | }) 46 | 47 | describe('.addObjectIntent (room, name, intent, shard = DEFAULT_SHARD)', function() { 48 | it('should do untested things (for now)') 49 | }) 50 | 51 | describe('.createConstruction (room, x, y, structureType, name, shard = DEFAULT_SHARD)', function() { 52 | it('should do untested things (for now)') 53 | }) 54 | 55 | describe('.setNotifyWhenAttacked (_id, enabled = true, shard = DEFAULT_SHARD)', function() { 56 | it('should do untested things (for now)') 57 | }) 58 | 59 | describe('.createInvader (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD)', function() { 60 | it('should do untested things (for now)') 61 | }) 62 | 63 | describe('.removeInvader (_id, shard = DEFAULT_SHARD)', function() { 64 | it('should do untested things (for now)') 65 | }) 66 | 67 | describe('.time (shard = DEFAULT_SHARD)', function() { 68 | it('should do untested things (for now)') 69 | }) 70 | 71 | describe('.worldSize (shard = DEFAULT_SHARD)', function() { 72 | it('should do untested things (for now)') 73 | }) 74 | 75 | describe('.roomTerrain (room, encoded = 1, shard = DEFAULT_SHARD)', function() { 76 | it('should do untested things (for now)') 77 | }) 78 | 79 | describe('.roomStatus (room, shard = DEFAULT_SHARD)', function() { 80 | it('should do untested things (for now)') 81 | }) 82 | 83 | describe('.roomOverview (room, interval = 8, shard = DEFAULT_SHARD)', function() { 84 | it('should do untested things (for now)') 85 | }) 86 | 87 | describe('.market.ordersIndex (shard = DEFAULT_SHARD)', function() { 88 | it('should do untested things (for now)') 89 | }) 90 | 91 | describe('.market.myOrders ()', function() { 92 | it('should do untested things (for now)') 93 | }) 94 | 95 | describe('.market.orders (resourceType, shard = DEFAULT_SHARD)', function() { 96 | it('should do untested things (for now)') 97 | }) 98 | 99 | describe('.market.stats (resourceType, shard = DEFAULT_SHARD)', function() { 100 | it('should do untested things (for now)') 101 | }) 102 | 103 | // This endpoint is not implemented on S+ 104 | describe.skip('.shards.info ()', function() { 105 | it('should send a request to /api/shards/info and return shards informations', async function() { 106 | let opts = _.omit(auth, ['email', 'password']) 107 | let api = new ScreepsAPI(opts) 108 | let res = await api.raw.game.shards.info() 109 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 110 | assert(_.has(res, 'shards'), 'response has no shards field') 111 | res.shards.forEach((shard, idx) => { 112 | assert(_.has(shard, 'name'), `shard ${idx} has no name field`) 113 | assert(_.has(shard, 'rooms'), `shard ${idx} has no rooms field`) 114 | assert(_.has(shard, 'users'), `shard ${idx} has no users field`) 115 | assert(_.has(shard, 'tick'), `shard ${idx} has no tick field`) 116 | }) 117 | }) 118 | }) 119 | 120 | }) -------------------------------------------------------------------------------- /src/ScreepsAPI.js: -------------------------------------------------------------------------------- 1 | import { Socket } from './Socket' 2 | import { RawAPI } from './RawAPI' 3 | import { ConfigManager } from './ConfigManager' 4 | 5 | const DEFAULTS = { 6 | protocol: 'https', 7 | hostname: 'screeps.com', 8 | port: 443, 9 | path: '/' 10 | } 11 | 12 | const configManager = new ConfigManager() 13 | 14 | export class ScreepsAPI extends RawAPI { 15 | static async fromConfig (server = 'main', config = false, opts = {}) { 16 | const data = await configManager.getConfig() 17 | 18 | if (data) { 19 | if (!data.servers[server]) { 20 | throw new Error(`Server '${server}' does not exist in '${configManager.path}'`) 21 | } 22 | 23 | const conf = data.servers[server] 24 | if (conf.ptr) conf.path = '/ptr' 25 | if (conf.season) conf.path = '/season' 26 | const api = new ScreepsAPI( 27 | Object.assign( 28 | { 29 | hostname: conf.host, 30 | port: conf.port, 31 | protocol: conf.secure ? 'https' : 'http', 32 | token: conf.token, 33 | path: conf.path || '/' 34 | }, 35 | opts 36 | ) 37 | ) 38 | 39 | api.appConfig = (data.configs && data.configs[config]) || {} 40 | 41 | if (!conf.token && conf.username && conf.password) { 42 | await api.auth(conf.username, conf.password) 43 | } 44 | 45 | return api 46 | } 47 | 48 | throw new Error('No valid config found') 49 | } 50 | 51 | constructor (opts) { 52 | opts = Object.assign({}, DEFAULTS, opts) 53 | super(opts) 54 | this.on('token', token => { 55 | this.token = token 56 | this.raw.token = token 57 | }) 58 | const defaultLimit = (limit, period) => ({ 59 | limit, 60 | period, 61 | remaining: limit, 62 | reset: 0, 63 | toReset: 0 64 | }) 65 | this.rateLimits = { 66 | global: defaultLimit(120, 'minute'), 67 | GET: { 68 | '/api/game/room-terrain': defaultLimit(360, 'hour'), 69 | '/api/user/code': defaultLimit(60, 'hour'), 70 | '/api/user/memory': defaultLimit(1440, 'day'), 71 | '/api/user/memory-segment': defaultLimit(360, 'hour'), 72 | '/api/game/market/orders-index': defaultLimit(60, 'hour'), 73 | '/api/game/market/orders': defaultLimit(60, 'hour'), 74 | '/api/game/market/my-orders': defaultLimit(60, 'hour'), 75 | '/api/game/market/stats': defaultLimit(60, 'hour'), 76 | '/api/game/user/money-history': defaultLimit(60, 'hour') 77 | }, 78 | POST: { 79 | '/api/user/console': defaultLimit(360, 'hour'), 80 | '/api/game/map-stats': defaultLimit(60, 'hour'), 81 | '/api/user/code': defaultLimit(240, 'day'), 82 | '/api/user/set-active-branch': defaultLimit(240, 'day'), 83 | '/api/user/memory': defaultLimit(240, 'day'), 84 | '/api/user/memory-segment': defaultLimit(60, 'hour') 85 | } 86 | } 87 | this.on('rateLimit', limits => { 88 | const rate = 89 | this.rateLimits[limits.method][limits.path] || this.rateLimits.global 90 | const copy = Object.assign({}, limits) 91 | delete copy.path 92 | delete copy.method 93 | Object.assign(rate, copy) 94 | }) 95 | this.socket = new Socket(this) 96 | } 97 | 98 | getRateLimit (method, path) { 99 | return this.rateLimits[method][path] || this.rateLimits.global 100 | } 101 | 102 | get rateLimitResetUrl () { 103 | return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice( 104 | 0, 105 | 8 106 | )}` 107 | } 108 | 109 | async me () { 110 | if (this._user) return this._user 111 | const tokenInfo = await this.tokenInfo() 112 | if (tokenInfo.full) { 113 | this._user = await this.raw.auth.me() 114 | } else { 115 | const { username } = await this.raw.user.name() 116 | const { user } = await this.raw.user.find(username) 117 | this._user = user 118 | } 119 | return this._user 120 | } 121 | 122 | async tokenInfo () { 123 | if (this._tokenInfo) { 124 | return this._tokenInfo 125 | } 126 | if (this.opts.token) { 127 | const { token } = await this.raw.auth.queryToken(this.token) 128 | this._tokenInfo = token 129 | } else { 130 | this._tokenInfo = { full: true } 131 | } 132 | return this._tokenInfo 133 | } 134 | 135 | async userID () { 136 | const user = await this.me() 137 | return user._id 138 | } 139 | 140 | get history () { 141 | return this.raw.history 142 | } 143 | 144 | get authmod () { 145 | return this.raw.authmod 146 | } 147 | 148 | get version () { 149 | return this.raw.version 150 | } 151 | 152 | get time () { 153 | return this.raw.game.time 154 | } 155 | 156 | get leaderboard () { 157 | return this.raw.leaderboard 158 | } 159 | 160 | get market () { 161 | return this.raw.game.market 162 | } 163 | 164 | get registerUser () { 165 | return this.raw.register.submit 166 | } 167 | 168 | get code () { 169 | return this.raw.user.code 170 | } 171 | 172 | get memory () { 173 | return this.raw.user.memory 174 | } 175 | 176 | get segment () { 177 | return this.raw.user.memory.segment 178 | } 179 | 180 | get console () { 181 | return this.raw.user.console 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Screeps API 2 | 3 | ## This is a nodejs API for the game Screeps 4 | 5 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | [![License](https://img.shields.io/npm/l/screeps-api.svg)](https://npmjs.com/package/screeps-api) 7 | [![Version](https://img.shields.io/npm/v/screeps-api.svg)](https://npmjs.com/package/screeps-api) 8 | [![Downloads](https://img.shields.io/npm/dw/screeps-api.svg)](https://npmjs.com/package/screeps-api) 9 | [![CircleCI](https://circleci.com/gh/screepers/node-screeps-api/tree/master.svg?style=shield)](https://circleci.com/gh/screepers/node-screeps-api/tree/master) 10 | 11 | ![npm](https://nodei.co/npm/screeps-api.png "NPM") 12 | 13 | ## Notice on authentication 14 | 15 | As of 12/29/2017 Screeps now uses auth tokens obtained via your screeps account settings. 16 | User/pass auth will stop working February 1, 2018! 17 | [Screeps Announcement](http://blog.screeps.com/2017/12/auth-tokens/) 18 | 19 | ## CLI Usage 20 | 21 | As of 1.7.0, a small CLI program (`screeps-api`) is included. 22 | 23 | Server config is specified via a `.screeps.yml` file conforming to the [Unified Credentials File format](https://github.com/screepers/screepers-standards/blob/master/SS3-Unified_Credentials_File.md) 24 | 25 | ``` 26 | screeps-api 27 | 28 | Usage: [options] [command] 29 | 30 | Options: 31 | 32 | -V, --version output the version number 33 | --server Server config to use (default: main) 34 | -h, --help output usage information 35 | 36 | Commands: 37 | 38 | raw [args...] Execute raw API call 39 | memory [options] [path] Get Memory contents 40 | segment [options] Get segment contents. Use 'all' to get all) 41 | download [options] Download code 42 | upload [options] Upload code 43 | 44 | ``` 45 | 46 | 47 | ## API Usage 48 | 49 | As of 1.0, all functions return Promises 50 | 51 | ```javascript 52 | const { ScreepsAPI } = require('screeps-api'); 53 | const fs = require('fs'); 54 | 55 | // Supports @tedivm's [Unified Credentials File format](https://github.com/screepers/screepers-standards/blob/34bd4e6e5c8250fa0794d915d9f78d3c45326076/SS3-Unified_Credentials_File.md) (Pending [screepers-standard PR #8](https://github.com/screepers/screepers-standards/pull/8)) 56 | const api = await ScreepsAPI.fromConfig('main', 'appName') 57 | // This loads the server config 'main' and the configs section 'appName' if it exists 58 | // config section can be accessed like this: 59 | // If making a CLI app, its suggested to have a `--server` argument for selection 60 | console.log(api.appConfig.myConfigVar) 61 | 62 | // All options are optional 63 | const api = new ScreepsAPI({ 64 | token: 'Your Token from Account/Auth Tokens' 65 | protocol: 'https', 66 | hostname: 'screeps.com', 67 | port: 443, 68 | path: '/' // Do no include '/api', it will be added automatically 69 | }); 70 | 71 | // You can overwrite parameters if needed 72 | api.auth('screeps@email.com','notMyPass',{ 73 | protocol: 'https', 74 | hostname: 'screeps.com', 75 | port: 443, 76 | path: '/' // Do no include '/api', it will be added automatically 77 | }) 78 | 79 | // If you want to point to the screeps PTR (Public Test Realm), 80 | // you can set the 'path' option to '/ptr' and it will work fine. 81 | 82 | // Dump Memory 83 | api.memory.get() 84 | .then(memory => { 85 | fs.writeFileSync('memory.json', JSON.stringify(memory)) 86 | }) 87 | .catch(err => console.error(err)); 88 | 89 | 90 | // Dump Memory Path 91 | api.memory.get('rooms.W0N0') 92 | .then(memory => { 93 | fs.writeFileSync('memory.rooms.W0N0.json', JSON.stringify(memory)) 94 | }) 95 | .catch(err => console.error(err)); 96 | 97 | // Get user info 98 | api.me().then((user)=>console.log(user)) 99 | 100 | // Socket API 101 | 102 | api.socket.connect() 103 | // Events have the structure of: 104 | // { 105 | // channel: 'room', 106 | // id: 'E3N3', // Only on certain events 107 | // data: { ... } 108 | // } 109 | api.socket.on('connected',()=>{ 110 | // Do stuff after connected 111 | }) 112 | api.socket.on('auth',(event)=>{ 113 | event.data.status contains either 'ok' or 'failed' 114 | // Do stuff after auth 115 | }) 116 | 117 | // Events: (Not a complete list) 118 | // connected disconnected message auth time protocol package subscribe unsubscribe console 119 | 120 | // Subscribtions can be queued even before the socket connects or auths, 121 | // although you may want to subscribe from the connected or auth callback to better handle reconnects 122 | 123 | api.socket.subscribe('console') 124 | api.socket.on('console',(event)=>{ 125 | event.data.messages.log // List of console.log output for tick 126 | }) 127 | 128 | 129 | // Starting in 1.0, you can also pass a handler straight to subscribe! 130 | api.socket.subscribe('console', (event)=>{ 131 | event.data.messages.log // List of console.log output for tick 132 | }) 133 | 134 | // More common examples 135 | api.socket.subscribe('cpu',(event)=>console.log('cpu',event.data)) 136 | api.code.get('default').then(data=>console.log('code',data)) 137 | api.code.set('default',{ 138 | main: 'module.exports.loop = function(){ ... }' 139 | }) 140 | api.socket.subscribe('memory/stats',(event)=>{ 141 | console.log('stats',event.data) 142 | }) 143 | api.socket.subscribe('memory/rooms.E0N0',(event)=>{ 144 | console.log('E0N0 Memory',event.data) 145 | }) 146 | ``` 147 | 148 | ## Endpoint documentation 149 | 150 | Server endpoints are listed in the `docs` folder: 151 | * [Endpoints.md](/docs/Endpoints.md) for direct access 152 | * [Websocket_endpoints.md](/docs/Websocket_endpoints.md) for web socket endpoints 153 | Those lists are currently not exhaustive. 154 | -------------------------------------------------------------------------------- /bin/screeps-api.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | const { Command } = require('commander') 3 | const { ScreepsAPI } = require('../') 4 | const fs = require('fs') 5 | const util = require('util') 6 | const path = require('path') 7 | 8 | const readFile = util.promisify(fs.readFile) 9 | const writeFile = util.promisify(fs.writeFile) 10 | 11 | async function init(opts) { 12 | return ScreepsAPI.fromConfig(opts.server) 13 | } 14 | 15 | async function json(data) { 16 | process.stdout.write(JSON.stringify(data)) 17 | } 18 | 19 | async function out(data, opts) { 20 | data = await data 21 | data = (data && data.data) || data 22 | if (process.stdout.isTTY) { 23 | console.log(data) 24 | } else { 25 | json(data) 26 | } 27 | } 28 | 29 | async function run() { 30 | const program = new Command() 31 | 32 | /** @param {string} name */ 33 | const commandBase = (name, args = '') => { 34 | const command = new Command(name) 35 | command 36 | .arguments(args) 37 | .option('--server ', 'Server config to use', 'main') 38 | program.addCommand(command) 39 | return command 40 | } 41 | 42 | program 43 | .version(require('../package.json').version) 44 | 45 | commandBase('raw', ' [args...]') 46 | .description('Execute raw API call') 47 | .action(async function (cmd, args, opts) { 48 | try { 49 | const api = await init(opts) 50 | const path = cmd.split('.') 51 | /** @type {function} */ 52 | let fn = api.raw 53 | for (const part of path) { 54 | fn = fn[part] 55 | } 56 | if (!fn || typeof fn !== 'function') { 57 | console.log('Invalid cmd') 58 | return 59 | } 60 | out(fn.apply(api, args)) 61 | } catch (e) { 62 | console.error(e) 63 | } 64 | }) 65 | 66 | commandBase('memory', '[path]') 67 | .description(`Get Memory contents`) 68 | .option('--set ', 'Sets the memory path to the contents of file') 69 | .option('--allow-root', 'Allows writing without path') 70 | .option('-s --shard ', 'Shard to read from', 'shard0') 71 | .option('-f --file ', 'File to write data to') 72 | .action(async function (fpath, opts) { 73 | try { 74 | const api = await init(opts) 75 | if (opts.set) { 76 | if (!fpath && !opts.allowRoot) { 77 | throw new Error('Refusing to write to root! Use --allow-root if you really want this.') 78 | } 79 | const data = await readFile(opts.set, 'utf8') 80 | await api.memory.set(fpath, data, opts.shard) 81 | out('Memory written') 82 | } else { 83 | const data = api.memory.get(fpath, opts.shard) 84 | if (opts.file) { 85 | await writeFile(opts.file, data) 86 | } else { 87 | out(data) 88 | } 89 | } 90 | } catch (e) { 91 | console.error(e) 92 | } 93 | }) 94 | 95 | commandBase('segment', '') 96 | .description(`Get segment contents. Use 'all' to get all)`) 97 | .option('--set ', 'Sets the segment content to the contents of file') 98 | .option('-s --shard ', 'Shard to read from', 'shard0') 99 | .option('-d --dir ', 'Directory to save in. Empty files are not written. (defaults to outputing in console)') 100 | .action(async function (segment, opts) { 101 | try { 102 | const api = await init(opts) 103 | if (opts.set) { 104 | const data = await readFile(opts.set, 'utf8') 105 | await api.memory.segment.set(segment, data, opts.shard) 106 | out('Segment Set') 107 | } else { 108 | if (segment === 'all') segment = Array.from({ length: 100 }, (v, k) => k).join(',') 109 | const { data } = await api.memory.segment.get(segment, opts.shard) 110 | const dir = opts.dir 111 | const segments = data 112 | if (dir) { 113 | if (Array.isArray(segments)) { 114 | await Promise.all(segments.map((d, i) => d && writeFile(path.join(dir, `segment_${i}`), d))) 115 | } else { 116 | await writeFile(path.join(dir, `segment_${segment}`), d) 117 | } 118 | out('Segments Saved') 119 | } else { 120 | out(segments) 121 | } 122 | } 123 | } catch (e) { 124 | console.error(e) 125 | } 126 | }) 127 | 128 | commandBase('download') 129 | .description(`Download code`) 130 | .option('-b --branch ', 'Code branch', 'default') 131 | .option('-d --dir ', 'Directory to save in (defaults to outputing in console)') 132 | .action(async function (opts) { 133 | try { 134 | const api = await init(opts) 135 | const dir = opts.dir 136 | const { modules } = await api.code.get(opts.branch) 137 | if (dir) { 138 | await Promise.all(Object.keys(modules).map(async fn => { 139 | const data = modules[fn] 140 | if (data.binary) { 141 | await writeFile(path.join(dir, `${fn}.wasm`), Buffer.from(data.binary, 'base64')) 142 | } else { 143 | await writeFile(path.join(dir, `${fn}.js`), data) 144 | } 145 | console.log(`Saved ${fn}`) 146 | })) 147 | } else { 148 | out(modules) 149 | } 150 | } catch (e) { 151 | console.error(e) 152 | } 153 | }) 154 | 155 | commandBase('upload', '') 156 | .description(`Upload code`) 157 | .option('-b --branch ', 'Code branch', 'default') 158 | .action(async function (files, opts) { 159 | try { 160 | const api = await init(opts) 161 | const modules = {} 162 | const ps = [] 163 | for (const file of files) { 164 | ps.push((async (file) => { 165 | const { name, ext } = path.parse(file) 166 | const data = await readFile(file) 167 | if (ext === '.js') { 168 | modules[name] = data.toString('utf8') 169 | } 170 | if (ext === '.wasm') { 171 | modules[name] = { binary: data.toString('base64') } 172 | } 173 | })(file)) 174 | } 175 | await Promise.all(ps) 176 | out(api.code.set(opts.branch, modules)) 177 | } catch (e) { 178 | console.error(e) 179 | } 180 | }) 181 | 182 | if (!process.argv.slice(2).length) { 183 | program.outputHelp() 184 | } 185 | 186 | await program.parseAsync() 187 | } 188 | 189 | run().then(data => { 190 | if (!data) return 191 | console.log(JSON.stringify(data.data || data, null, 2)) 192 | }).catch(console.error) 193 | -------------------------------------------------------------------------------- /test/api.general.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert') 2 | const _ = require('lodash') 3 | const { ScreepsAPI } = require('../') 4 | const auth = require('./credentials') 5 | 6 | describe('ScreepsAPI', function () { 7 | this.slow(2000) 8 | this.timeout(5000) 9 | 10 | describe('.constructor()', function () { 11 | it('should save passed options', function () { 12 | let options = { 13 | email: 'screeps@email.com', 14 | // don't use a fake password here or API will try to authenticate 15 | protocol: 'https', 16 | hostname: 'screeps.com', 17 | port: 443, 18 | path: '/' 19 | } 20 | let api = new ScreepsAPI(options) 21 | _.each(options, (value, key) => assert.equal(api.opts[key], value, `invalid ${key} option`)) 22 | }) 23 | it('should assign default options when needed', function () { 24 | const DEFAULTS = { 25 | protocol: 'https', 26 | hostname: 'screeps.com', 27 | port: 443, 28 | path: '/' 29 | } 30 | let api = new ScreepsAPI({}) 31 | _.each(DEFAULTS, (value, key) => assert.equal(api.opts[key], value, `invalid ${key} default option`)) 32 | }) 33 | }) 34 | 35 | describe('.me()', function () { 36 | it('should return user informations from `/api/auth/me` endpoint', async function () { 37 | let opts = _.omit(auth, ['username', 'password']) 38 | let api = new ScreepsAPI(opts) 39 | await api.auth(auth.username, auth.password) 40 | let infos = await api.me() 41 | assert.equal(infos.ok, 1, 'incorrect server answer: ok should be 1') 42 | assert(_.has(infos, 'email'), 'answer has no email field') 43 | assert(_.has(infos, 'badge'), 'answer has no badge field') 44 | assert(_.has(infos, 'username'), 'answer has no username field') 45 | }) 46 | }) 47 | 48 | describe('.mapToShard()', function () { 49 | it('should do things... but I\'m not sure what exactly...') 50 | }) 51 | 52 | describe('.setServer()', function () { 53 | it('should save passed options', function () { 54 | let options = { 55 | email: 'screeps@email.com', 56 | protocol: 'https', 57 | hostname: 'screeps.com', 58 | port: 443, 59 | path: '/' 60 | } 61 | let api = new ScreepsAPI() 62 | api.setServer(options) 63 | _.each(options, (value, key) => assert.equal(api.opts[key], value, `invalid ${key} option`)) 64 | }) 65 | it('should compute opts.url if opts.url wasn\'t provided', function () { 66 | let options1 = { protocol: 'http', hostname: 'screeps.com' } 67 | let options2 = { protocol: 'https', hostname: 'screeps.com', path: '/ptr/' } 68 | let options3 = { protocol: 'https', hostname: 'screeps.com', port: 80, path: '/' } 69 | let api = new ScreepsAPI() 70 | api.setServer(options1) 71 | assert.equal(api.opts['url'], 'http://screeps.com:443/', 'invalid computed url') 72 | api.setServer(options2) 73 | assert.equal(api.opts['url'], 'https://screeps.com:443/ptr/', 'invalid computed url') 74 | api.setServer(options3) 75 | assert.equal(api.opts['url'], 'https://screeps.com:80/', 'invalid computed url') 76 | }) 77 | it('should compute opts.pathname if opts.url wasn\'t provided', function () { 78 | let api = new ScreepsAPI() 79 | api.setServer({ path: '/ptr/' }) 80 | assert.equal(api.opts['pathname'], '/ptr/', 'pathname was not updated') 81 | api.setServer({ path: '/' }) 82 | assert.equal(api.opts['pathname'], '/', 'pathname was not updated') 83 | }) 84 | }) 85 | 86 | describe('.auth()', function () { 87 | it('should save email and password', async function () { 88 | let api = new ScreepsAPI() 89 | await api.auth('screeps@email.com', 'invalid_password').catch(() => { /* do nothing */; }) 90 | assert.equal(api.opts.email, 'screeps@email.com', `invalid email option`) 91 | assert.equal(api.opts.password, 'invalid_password', `invalid email option`) 92 | }) 93 | it('should update options if opt object was passed', async function () { 94 | let options = { 95 | protocol: 'https', 96 | hostname: 'screeps.com', 97 | port: 443 98 | } 99 | let api = new ScreepsAPI() 100 | await api.auth('email', 'password', options).catch(() => { /* do nothing */; }) 101 | _.each(options, (value, key) => assert.equal(api.opts[key], value, `invalid ${key} option`)) 102 | }) 103 | it('should authenticate and get token', async function () { 104 | let event = false 105 | let opts = _.omit(auth, ['username', 'password']) 106 | let api = new ScreepsAPI(opts) 107 | api.on('token', () => event = true) 108 | await api.auth(auth.username, auth.password) 109 | assert(event, 'token event was not emited') 110 | assert(_.has(api, 'token'), 'token was not saved') 111 | assert.equal(api.__authed, true, 'internal state has not changed (api.__authed)') 112 | }) 113 | it('should reject promise in case of error', async function () { 114 | try { 115 | let api = new ScreepsAPI() 116 | await api.auth(auth.username, 'bad password') 117 | } catch (err) { 118 | assert(err.message.match(/Not authorized/i), 'wrong error message') 119 | } 120 | }) 121 | }) 122 | 123 | describe('.req()', function () { 124 | it('should send request to game server and get the answer') 125 | it('can send GET and POST requests') 126 | it('should throw an error in case of 401 and if not authenticated') 127 | it('should read, save and emit authentication token if any') 128 | it('should use opts.path correctly (ie: for PTR)') 129 | // Disabled, offifical PTR is down 130 | // it('should use opts.path correctly (ie: for PTR)', async function() { 131 | // // This test must be run against official server (the only one to use PTR) 132 | // let opts = { 133 | // protocol: 'https', 134 | // hostname: 'screeps.com', 135 | // port: 443, 136 | // } 137 | // // Get official server time 138 | // let api1 = new ScreepsAPI(opts) 139 | // let res1 = await api1.raw.game.time() 140 | // let time1 = res1.time 141 | // // Get PTR time 142 | // opts.path = '/ptr' 143 | // let api2 = new ScreepsAPI(opts) 144 | // let res2 = await api2.raw.game.time() 145 | // let time2 = res2.time 146 | // // Compare them 147 | // assert.notEqual(time1, time2, 'time for official and PTR should be different') 148 | // }) 149 | it('should throw an error if response.ok !== 1') 150 | }) 151 | 152 | describe('.gz()', function () { 153 | it('should unzip data and return JSON') 154 | }) 155 | 156 | describe('.inflate()', function () { 157 | it('should inflate data') 158 | }) 159 | }) 160 | -------------------------------------------------------------------------------- /docs/Websocket_endpoints.md: -------------------------------------------------------------------------------- 1 | # Web sockets endpoints list 2 | 3 | Currently known endpoints are listed below. 4 | * This list is clearly not exhaustive and comes from empirical observations. 5 | * If you want to add more data, feel free to make a pull request. 6 | 7 | ## Shards 8 | 9 | Any subscription taking a room name is different on servers with shards, 10 | the room name should be prefixed with the shard name ex `shard0/E0N0`. 11 | 12 | Note that adding a shard to a server not expecting it may cause an error. 13 | 14 | ## Subscribing to web sockets endpoints 15 | 16 | * Make sure you are authenticated (you should have called `api.auth(...).then(...)` first). 17 | * Connect the socket and wait for connection establishment using `api.socket.connect().then(...)`. 18 | * You can then subscribe to different endpoints using `api.socket.subscribe()`. 19 | * The server will then send periodic events with requested information. 20 | 21 | Complete example: 22 | ```javascript 23 | const { ScreepsAPI } = require('screeps-api'); 24 | 25 | try { 26 | // Setup 27 | const api = new ScreepsAPI(); 28 | await api.auth("your_email", "your_password"); // authenticate 29 | await api.socket.connect(); // connect socket 30 | 31 | // Subscribe to 'cpu' endpoint and get events 32 | api.socket.subscribe('cpu'); 33 | api.socket.on('cpu', event => { 34 | console.log(event.data.cpu) // cpu used last tick 35 | }); 36 | 37 | // You can also put a callback to subscribe() 38 | api.socket.subscribe('console', event => { 39 | event.data.messages.log // List of console.log output for tick 40 | }) 41 | } catch(err) { 42 | console.log(err); 43 | } 44 | ``` 45 | 46 | 47 | ## code 48 | 49 | ### Description: 50 | 51 | Once subscribed, the server will send a new event with full code base every time code base changes. 52 | 53 | ### Parameters of `event.data`: 54 | 55 | Name | Type | Description 56 | --------- | ------ | ------------------ 57 | branch | String | Name of the updated branch 58 | modules | Object | Map of files (using file name as key and file content as value) 59 | timestamp | Number | Date of the modification expressed as the number of milli-seconds since 01/01/1970 60 | hash | Number | Some kind of hash, I guess ? (don't ask me how to compute it #FIXME) 61 | 62 | ### Example: 63 | 64 | ```javascript 65 | // Subscription 66 | api.socket.subscribe('code', event => console.log(JSON.stringify(event.data))); 67 | ``` 68 | ```javascript 69 | // Results 70 | { 71 | "branch": "simulation", 72 | "modules":{ 73 | "main":"console.log(\"Hello world!\");\n", 74 | // other modules... 75 | }, 76 | "timestamp": 1500930041802, 77 | "hash": 424324 78 | } 79 | ``` 80 | 81 | 82 | ## console 83 | 84 | ### Description: 85 | 86 | Once subscribed, the server will send a new event every tick with console logs and return value of commands (if any). 87 | 88 | ### Parameters of `event.data`: 89 | 90 | Name | Type | Description 91 | ---------------- | ------ | ------------------ 92 | messages.log | Array | Lines shown in console (like if printed by `console.log()`) 93 | messages.results | Array | Array of command results 94 | 95 | ### Example: 96 | 97 | ```javascript 98 | // Subscription 99 | api.socket.subscribe('console', event => console.log(JSON.stringify(event.data))); 100 | ``` 101 | ```javascript 102 | // Results after executing `Game.time` in game: 103 | { 104 | "messages": { 105 | "log": [], 106 | "results": ["16746996"] 107 | } 108 | } 109 | // (`Game.time` does not show any log in the console, which is why `messages.log` is empty) 110 | ``` 111 | 112 | 113 | ## cpu 114 | 115 | ### Description: 116 | 117 | Once subscribed, the server will send a new event every tick with cpu and memory usage. 118 | 119 | ### Parameters of `event.data`: 120 | 121 | Name | Type | Description 122 | ------ | ------ | ------------------ 123 | cpu | Number | Cpu used last tick 124 | memory | Number | Current memory usage 125 | 126 | ### Example: 127 | 128 | ```javascript 129 | // Subscription 130 | api.socket.subscribe('cpu', event => console.log(JSON.stringify(event.data))); 131 | ``` 132 | ```javascript 133 | // Results every tick 134 | { 135 | "cpu": 32, 136 | "memory": 126435 137 | } 138 | ``` 139 | 140 | 141 | ## room:ROOM_NAME 142 | 143 | ### Description: 144 | 145 | Once subscribed, the server will send a new event every tick with the RoomObjects of present in selected room (`ROOM_NAME`). 146 | RoomObjects seem to have the same properties as within game scripts. 147 | 148 | **Atention**, only the first event actually returns the object full properties. 149 | Subsequent events only return the modified properties. 150 | 151 | ### Parameters of `event.data`: 152 | 153 | Name | Type | Description 154 | -------- | ------ | --------------------------------- 155 | objects | Object | Map of RoomObjects indexed by id 156 | gameTime | Number | Current game tick 157 | info | Object | Contains game mode (usually `"world"`) 158 | visual | Object | Room visuals (contents currently unknown #FIXME) 159 | 160 | ### Example: 161 | 162 | ```javascript 163 | // Subscription 164 | api.socket.subscribe('room:W7N7', event => console.log(JSON.stringify(event.data))); // For non-sharded servers 165 | api.socket.subscribe('room:shard0/W7N7', event => console.log(JSON.stringify(event.data))); // For sharded servers 166 | ``` 167 | ```javascript 168 | // First event results: 169 | { 170 | "objects": { 171 | "58dbc28b8283ff5308a3c0ba": { 172 | "_id": "58dbc28b8283ff5308a3c0ba", 173 | "room": "W97S73", 174 | "type": "source", 175 | "x": 12, "y": 14, 176 | "energy": 1908, 177 | "energyCapacity": 3000, 178 | "ticksToRegeneration": 300, 179 | "nextRegenerationTime": 20308471, 180 | "invaderHarvested": 45324 181 | }, 182 | "59663a8e82b5ab1b911ca1a9": { 183 | "_id": "59663a8e82b5ab1b911ca1a9", 184 | "type": "road", 185 | "x": 17,"y": 42, 186 | "room": "W97S73", 187 | "notifyWhenAttacked": true, 188 | "hits": 22080, 189 | "hitsMax": 25000, 190 | "nextDecayTime": 20308833 191 | }, 192 | // other RoomObjects... 193 | }, 194 | "gameTime": 20307112, 195 | "info": { "mode": "world" }, 196 | "visual": "" 197 | } 198 | ``` 199 | ```javascript 200 | // Results for subsequent events: 201 | { 202 | "objects": { 203 | "58dbc28b8283ff5308a3c0ba": { "energy": 948, "invaderHarvested": 34284 }, 204 | "5967d460eebe3d6404c26852": { "nextDecayTime": 20307861 }, 205 | // Other modified RoomObjects... 206 | }, 207 | "gameTime": 20307112, 208 | "info": { "mode": "world" }, 209 | "visual": "" 210 | } 211 | ``` 212 | -------------------------------------------------------------------------------- /src/Socket.js: -------------------------------------------------------------------------------- 1 | import WebSocket from 'ws' 2 | import { URL } from 'url' 3 | import { EventEmitter } from 'events' 4 | import Debug from 'debug' 5 | 6 | const debug = Debug('screepsapi:socket') 7 | 8 | const DEFAULTS = { 9 | reconnect: true, 10 | resubscribe: true, 11 | keepAlive: true, 12 | maxRetries: 10, 13 | maxRetryDelay: 60 * 1000 // in milli-seconds 14 | } 15 | 16 | export class Socket extends EventEmitter { 17 | constructor (ScreepsAPI) { 18 | super() 19 | this.api = ScreepsAPI 20 | this.opts = Object.assign({}, DEFAULTS) 21 | this.on('error', () => {}) // catch to prevent unhandled-exception errors 22 | this.reset() 23 | this.on('auth', ev => { 24 | if (ev.data.status === 'ok') { 25 | while (this.__queue.length) { 26 | this.emit(this.__queue.shift()) 27 | } 28 | clearInterval(this.keepAliveInter) 29 | if (this.opts.keepAlive) { 30 | this.keepAliveInter = setInterval(() => this.ws && this.ws.ping(1), 10000) 31 | } 32 | } 33 | }) 34 | } 35 | 36 | reset () { 37 | this.authed = false 38 | this.connected = false 39 | this.reconnecting = false 40 | clearInterval(this.keepAliveInter) 41 | this.keepAliveInter = 0 42 | this.__queue = [] // pending messages (to send once authenticated) 43 | this.__subQueue = [] // pending subscriptions (to request once authenticated) 44 | this.__subs = {} // number of callbacks for each subscription 45 | } 46 | 47 | async connect (opts = {}) { 48 | Object.assign(this.opts, opts) 49 | if (!this.api.token) { 50 | throw new Error('No token! Call api.auth() before connecting the socket!') 51 | } 52 | return new Promise((resolve, reject) => { 53 | const baseURL = this.api.opts.url.replace('http', 'ws') 54 | const wsurl = new URL('socket/websocket', baseURL) 55 | this.ws = new WebSocket(wsurl) 56 | this.ws.on('open', () => { 57 | this.connected = true 58 | this.reconnecting = false 59 | if (this.opts.resubscribe) { 60 | this.__subQueue.push(...Object.keys(this.__subs)) 61 | } 62 | debug('connected') 63 | this.emit('connected') 64 | resolve(this.auth(this.api.token)) 65 | }) 66 | this.ws.on('close', () => { 67 | clearInterval(this.keepAliveInter) 68 | this.authed = false 69 | this.connected = false 70 | debug('disconnected') 71 | this.emit('disconnected') 72 | if (this.opts.reconnect) { 73 | this.reconnect().catch(() => { /* error emitted in reconnect() */ }) 74 | } 75 | }) 76 | this.ws.on('error', (err) => { 77 | this.ws.terminate() 78 | this.emit('error', err) 79 | debug(`error ${err}`) 80 | if (!this.connected) { 81 | reject(err) 82 | } 83 | }) 84 | this.ws.on('unexpected-response', (req, res) => { 85 | const err = new Error(`WS Unexpected Response: ${res.statusCode} ${res.statusMessage}`) 86 | this.emit('error', err) 87 | reject(err) 88 | }) 89 | this.ws.on('message', (data) => this.handleMessage(data)) 90 | }) 91 | } 92 | 93 | async reconnect () { 94 | if (this.reconnecting) { 95 | return 96 | } 97 | this.reconnecting = true 98 | let retries = 0 99 | let retry 100 | do { 101 | let time = Math.pow(2, retries) * 100 102 | if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay 103 | await this.sleep(time) 104 | if (!this.reconnecting) return // reset() called in-between 105 | try { 106 | await this.connect() 107 | retry = false 108 | } catch (err) { 109 | retry = true 110 | } 111 | retries++ 112 | debug(`reconnect ${retries}/${this.opts.maxRetries}`) 113 | } while (retry && retries < this.opts.maxRetries) 114 | if (retry) { 115 | const err = new Error(`Reconnection failed after ${this.opts.maxRetries} retries`) 116 | this.reconnecting = false 117 | debug('reconnect failed') 118 | this.emit('error', err) 119 | throw err 120 | } else { 121 | // Resume existing subscriptions on the new socket 122 | Object.keys(this.__subs).forEach(sub => this.subscribe(sub)) 123 | } 124 | } 125 | 126 | disconnect () { 127 | debug('disconnect') 128 | clearInterval(this.keepAliveInter) 129 | this.ws.removeAllListeners() // remove listeners first or we may trigger reconnection & Co. 130 | this.ws.terminate() 131 | this.reset() 132 | this.emit('disconnected') 133 | } 134 | 135 | sleep (time) { 136 | return new Promise((resolve, reject) => { 137 | setTimeout(resolve, time) 138 | }) 139 | } 140 | 141 | handleMessage (msg) { 142 | msg = msg.data || msg // Handle ws/browser difference 143 | if (msg.slice(0, 3) === 'gz:') { msg = this.api.inflate(msg) } 144 | debug(`message ${msg}`) 145 | if (msg[0] === '[') { 146 | msg = JSON.parse(msg) 147 | let [, type, id, channel] = msg[0].match(/^(.+):(.+?)(?:\/(.+))?$/) 148 | channel = channel || type 149 | const event = { channel, id, type, data: msg[1] } 150 | this.emit(msg[0], event) 151 | this.emit(event.channel, event) 152 | this.emit('message', event) 153 | } else { 154 | const [channel, ...data] = msg.split(' ') 155 | const event = { type: 'server', channel, data } 156 | if (channel === 'auth') { event.data = { status: data[0], token: data[1] } } 157 | if (['protocol', 'time', 'package'].includes(channel)) { event.data = { [channel]: data[0] } } 158 | this.emit(channel, event) 159 | this.emit('message', event) 160 | } 161 | } 162 | 163 | async gzip (bool) { 164 | this.send(`gzip ${bool ? 'on' : 'off'}`) 165 | } 166 | 167 | async send (data) { 168 | if (!this.connected) { 169 | this.__queue.push(data) 170 | } else { 171 | this.ws.send(data) 172 | } 173 | } 174 | 175 | auth (token) { 176 | return new Promise((resolve, reject) => { 177 | this.send(`auth ${token}`) 178 | this.once('auth', (ev) => { 179 | const { data } = ev 180 | if (data.status === 'ok') { 181 | this.authed = true 182 | this.emit('token', data.token) 183 | this.emit('authed') 184 | while (this.__subQueue.length) { 185 | this.send(this.__subQueue.shift()) 186 | } 187 | resolve() 188 | } else { 189 | reject(new Error('socket auth failed')) 190 | } 191 | }) 192 | }) 193 | } 194 | 195 | async subscribe (path, cb) { 196 | if (!path) return 197 | const userID = await this.api.userID() 198 | if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}` } 199 | if (this.authed) { 200 | this.send(`subscribe ${path}`) 201 | } else { 202 | this.__subQueue.push(`subscribe ${path}`) 203 | } 204 | this.emit('subscribe', path) 205 | this.__subs[path] = this.__subs[path] || 0 206 | this.__subs[path]++ 207 | if (cb) this.on(path, cb) 208 | } 209 | 210 | async unsubscribe (path) { 211 | if (!path) return 212 | const userID = await this.api.userID() 213 | if (!path.match(/^(\w+):(.+?)$/)) { path = `user:${userID}/${path}` } 214 | this.send(`unsubscribe ${path}`) 215 | this.emit('unsubscribe', path) 216 | if (this.__subs[path]) this.__subs[path]-- 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /test/api.raw.user.js: -------------------------------------------------------------------------------- 1 | const assert = require('assert'); 2 | const _ = require('lodash'); 3 | const { ScreepsAPI } = require('../'); 4 | const auth = require('./credentials') 5 | 6 | describe('api.raw.user', function() { 7 | 8 | this.slow(3000); 9 | this.timeout(5000); 10 | 11 | describe('.badge (badge)', function() { 12 | it('should send a request to /api/user/badge which sets user badge', async function() { 13 | let opts = _.omit(auth, ['username', 'password']) 14 | let api = new ScreepsAPI(opts) 15 | await api.auth(auth.username, auth.password) 16 | // Save previous badge 17 | let res = await api.me() 18 | let initialBadge = res.badge 19 | // Set new badge 20 | let newBadge = { type: 16, color1: '#000000', color2: '#000000', color3:'#000000', param: 100, flip: false } 21 | res = await api.raw.user.badge(newBadge) 22 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 23 | // Check that badge was effectively changed 24 | res = await api.me() 25 | _.each(res.badge, (value, key) => { 26 | assert.equal(value, newBadge[key], `badge ${key} is incorrect`) 27 | }) 28 | // Reset badge 29 | res = await api.raw.user.badge(initialBadge) 30 | }) 31 | }) 32 | 33 | describe('.respawn ()', function() { 34 | it('should do untested things (for now)') 35 | }) 36 | 37 | describe('.branches ()', function() { 38 | it('should send a request to /api/user/branches and return branches list', async function() { 39 | let opts = _.omit(auth, ['username', 'password']) 40 | let api = new ScreepsAPI(opts) 41 | await api.auth(auth.username, auth.password) 42 | let res = await api.raw.user.branches() 43 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 44 | assert(res.list.length > 0, 'no branch found') 45 | }) 46 | }) 47 | 48 | describe('.cloneBranch (branch, newName, defaultModules)', function() { 49 | it('should send a request to /api/user/clone-branch in order to clone @branch into @newName', async function() { 50 | let opts = _.omit(auth, ['username', 'password']) 51 | let api = new ScreepsAPI(opts) 52 | await api.auth(auth.username, auth.password) 53 | // Create a new branch 54 | let res = await api.raw.user.cloneBranch('default', 'screeps-api-testing') 55 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 56 | // Check if branch was indeed created 57 | res = await api.raw.user.branches() 58 | let found = _.find(res.list, { branch: 'screeps-api-testing' }) 59 | assert(found != null, 'branch was not cloned') 60 | }) 61 | }) 62 | 63 | describe('.setActiveBranch (branch, activeName)', function() { 64 | it('should send a request to /api/user/set-active-branch in order to define @branch as active', async function() { 65 | let opts = _.omit(auth, ['username', 'password']) 66 | let api = new ScreepsAPI(opts) 67 | await api.auth(auth.username, auth.password) 68 | // Find current active branch for simulator 69 | let res = await api.raw.user.branches() 70 | let initialBranch = _.find(res.list, { activeSim: true }) 71 | assert(initialBranch != null, 'cannot find current active branch for simulator') 72 | // Change active branch for simulator 73 | res = await api.raw.user.setActiveBranch('screeps-api-testing', 'activeSim') 74 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 75 | // Check if branch was indeed changed 76 | res = await api.raw.user.branches() 77 | let found = _.find(res.list, { activeSim: true }) 78 | assert.equal(found.branch, 'screeps-api-testing', 'branch was not set') 79 | // Reset branch back to initial state 80 | await api.raw.user.setActiveBranch(initialBranch.branch, 'activeSim') 81 | }) 82 | }) 83 | 84 | describe('.deleteBranch (branch)', function() { 85 | it('should send a request to /api/user/delete-branch in order to delete @branch', async function() { 86 | let opts = _.omit(auth, ['username', 'password']) 87 | let api = new ScreepsAPI(opts) 88 | await api.auth(auth.username, auth.password) 89 | // Delete 'screeps-api-testing' branch 90 | let res = await api.raw.user.deleteBranch('screeps-api-testing') 91 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 92 | // Check if branch was indeed deleted 93 | res = await api.raw.user.branches() 94 | let found = _.find(res.list, { branch: 'screeps-api-testing' }) 95 | assert(found == null, 'branch was not deleted') 96 | }) 97 | }) 98 | 99 | describe('.notifyPrefs (prefs)', function() { 100 | it('should send a request to /api/user/notify-prefs which sets user preferences', async function() { 101 | let opts = _.omit(auth, ['username', 'password']) 102 | let api = new ScreepsAPI(opts) 103 | await api.auth(auth.username, auth.password) 104 | let defaults = { disabled: false, disabledOnMessages: false, sendOnline: true, interval: 5, errorsInterval: 30 } 105 | // Save previous prefs 106 | let res = await api.me() 107 | let initialPrefs = _.merge(defaults, res.notifyPrefs) 108 | // Set new preferences 109 | let newPrefs = { disabled: true, disabledOnMessages: true, sendOnline: false, interval: 60, errorsInterval: 60 } 110 | res = await api.raw.user.notifyPrefs(newPrefs) 111 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 112 | // Check that preferences were indeed changed 113 | res = await api.me() 114 | _.each(res.notifyPrefs, (value, key) => { 115 | assert.equal(value, newPrefs[key], `preference ${key} is incorrect`) 116 | }) 117 | // Reset preferences 118 | res = await api.raw.user.notifyPrefs(initialPrefs) 119 | }) 120 | }) 121 | 122 | describe('.tutorialDone ()', function() { 123 | it('should do untested things (for now)') 124 | }) 125 | 126 | describe('.email (email)', function() { 127 | it('should do untested things (for now)') 128 | }) 129 | 130 | describe('.worldStartRoom (shard)', function() { 131 | it('should do untested things (for now)') 132 | }) 133 | 134 | describe('.worldStatus ()', function() { 135 | it('should do untested things (for now)') 136 | }) 137 | 138 | describe('.code.get (branch)', function() { 139 | it('should do untested things (for now)') 140 | it('should send a GET request to /api/user/code and return user code from specified branch.', async function() { 141 | let opts = _.omit(auth, ['username', 'password']) 142 | let api = new ScreepsAPI(opts) 143 | await api.auth(auth.username, auth.password) 144 | let res = await api.raw.user.code.get('default') 145 | assert.equal(res.ok, 1, 'incorrect server response: ok should be 1') 146 | assert(_.has(res, 'modules'), 'response has no modules field') 147 | assert(_.has(res, 'branch'), 'response has no branch field') 148 | assert.equal(res.branch, 'default', 'branch is incorrect') 149 | }) 150 | }) 151 | 152 | describe('.code.set (branch, modules, _hash)', function() { 153 | it('should do untested things (for now)') 154 | }) 155 | 156 | describe('.respawnProhibitedRooms ()', function() { 157 | it('should do untested things (for now)') 158 | }) 159 | 160 | describe('.memory.get (path, shard = DEFAULT_SHARD)', function() { 161 | it('should do untested things (for now)') 162 | }) 163 | 164 | describe('.memory.set (path, value, shard = DEFAULT_SHARD)', function() { 165 | it('should do untested things (for now)') 166 | }) 167 | 168 | describe('.segment.get (segment, shard = DEFAULT_SHARD)', function() { 169 | it('should do untested things (for now)') 170 | }) 171 | 172 | describe('.segment.set (segment, data, shard = DEFAULT_SHARD)', function() { 173 | it('should do untested things (for now)') 174 | }) 175 | 176 | describe('.find (username)', function() { 177 | it('should do untested things (for now)') 178 | }) 179 | 180 | describe('.findById (id)', function() { 181 | it('should do untested things (for now)') 182 | }) 183 | 184 | describe('.stats (interval)', function() { 185 | it('should do untested things (for now)') 186 | }) 187 | 188 | describe('.rooms (id)', function() { 189 | it('should do untested things (for now)') 190 | }) 191 | 192 | describe('.overview (interval, statName)', function() { 193 | it('should do untested things (for now)') 194 | }) 195 | 196 | describe('.moneyHistory (page = 0)', function() { 197 | it('should do untested things (for now)') 198 | }) 199 | 200 | describe('.console (expression, shard = DEFAULT_SHARD)', function() { 201 | it('should do untested things (for now)') 202 | }) 203 | 204 | }) -------------------------------------------------------------------------------- /docs/Endpoints.md: -------------------------------------------------------------------------------- 1 | Copied from [python-screeps](https://github.com/screepers/python-screeps/blob/master/docs/Endpoints.md) 2 | 3 | Start by sending a request to `auth/signin` with your e-mail address and 4 | password in a JSON object in the POST data. The response JSON object contains a 5 | token (a hex string); remember that value. Each time you make a request that 6 | requires authentication (the leaderboard and terrain ones, at least, don't), 7 | send the most recent token value as both the `X-Token` and `X-Username` 8 | headers. The response will contain a new token value in the `X-Token` header 9 | with which you should replace your saved token. (You can send the token on every 10 | request and just check for a new one in the response, so you don't have to know 11 | exactly which calls require authentication.) 12 | 13 | Example request parameters are given below, along with schemata for the server's 14 | responses. 15 | 16 | Memory and console endpoints are from 17 | [bzy-xyz](https://gist.github.com/bzy-xyz/9c4d8c9f9498a2d7983d). 18 | 19 | You can access the PTR by changing `screeps.com` to `screeps.com/ptr` in all URLs. 20 | 21 | # Enumeration values 22 | When an endpoint takes `interval` or `statName` as an argument, the valid values 23 | are the ones listed below. 24 | 25 | - interval: 8, 180, 1440 (8 for 1h, 180 for 24h and 1440 for 7 days) 26 | - statName: creepsLost, creepsProduced, energyConstruction, energyControl, energyCreeps, energyHarvested 27 | 28 | # Authentication 29 | - [POST] `https://screeps.com/api/auth/signin` 30 | - post data: `{ email, password }` 31 | - response: `{ ok, token }` 32 | 33 | # Room information 34 | - `https://screeps.com/api/game/room-overview?interval=8&room=E1N8` 35 | - `{ ok, owner: { username, badge: { type, color1, color2, color3, param, flip } }, stats: { energyHarvested: [ { value, endTime } ], energyConstruction: [ { value, endTime } ], energyCreeps: [ { value, endTime } ], energyControl: [ { value, endTime } ], creepsProduced: [ { value, endTime } ], creepsLost: [ { value, endTime } ] }, statsMax: { energy1440, energyCreeps1440, energy8, energyControl8, creepsLost180, energyHarvested8, energy180, energyConstruction180, creepsProduced8, energyControl1440, energyCreeps8, energyHarvested1440, creepsLost1440, energyConstruction1440, energyHarvested180, creepsProduced180, creepsProduced1440, energyCreeps180, energyControl180, energyConstruction8, creepsLost8 } }` 36 | 37 | - `https://screeps.com/api/game/room-terrain?room=E1N8` 38 | - `{ ok, terrain: [ { room, x, y, type } ] }` 39 | - `type` in each element of `terrain` can be "wall" or "swamp" 40 | 41 | - `https://screeps.com/api/game/room-terrain?room=E1N8&encoded=1` 42 | - `{ ok, terrain: [ { _id, room, terrain, type } ] }` 43 | - `terrain` is a string of digits, giving the terrain left-to-right and top-to-bottom 44 | - 0: plain, 1: wall, 2: swamp, 3: also wall 45 | - encoded argument can be anything non-empty 46 | 47 | - `https://screeps.com/api/game/room-status?room=E1N8` 48 | - `{ _id, status, novice }` 49 | - `status` can at least be "normal" or "out of borders" 50 | - if the room is in a novice area, `novice` will contain the Unix timestamp of the end of the protection (otherwise it is absent) 51 | 52 | - `https://screeps.com/api/experimental/pvp?interval=50` 53 | - `{ ok, time, rooms: [ { _id, lastPvpTime } ] }` 54 | - `time` is the current server tick 55 | - `_id` contains the room name for each room, and `lastPvpTime` contains the last tick pvp occurred 56 | - 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. 57 | 58 | - `https://screeps.com/api/experimental/pvp?start=14787157` 59 | - `{ ok, time, rooms: [ { _id, lastPvpTime } ] }` 60 | 61 | # Market information 62 | 63 | - `https://screeps.com/api/game/market/orders-index` 64 | - `{"ok":1,"list":[{"_id":"XUHO2","count":2}]}` 65 | - `_id` is the resource type, and there will only be one of each type. 66 | - `count` is the number of orders. 67 | 68 | - `https://screeps.com/api/game/market/my-orders` 69 | - `{ ok, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }` 70 | 71 | - `https://screeps.com/api/game/market/orders?resourceType=Z` 72 | - `{ ok, list: [ { _id, created, user, active, type, amount, remainingAmount, resourceType, price, totalAmount, roomName } ] }` 73 | - `resourceType` is one of the RESOURCE_* constants. 74 | 75 | - `https://screeps.com/api/user/money-history` 76 | - `{"ok":1,"page":0,"list":[ { _id, date, tick, user, type, balance, change, market: {} } ] }` 77 | - `page` used for pagination. 78 | - `hasMore` is true if there are more pages to view. 79 | - `market` 80 | - New Order- `{ order: { type, resourceType, price, totalAmount, roomName } }` 81 | - Extended Order- `{ extendOrder: { orderId, addAmount } }` 82 | - Fulfilled Order- `{ resourceType, roomName, targetRoomName, price, npc, amount }` 83 | - Price Change - `{ changeOrderPrice: { orderId, oldPrice, newPrice } }` 84 | 85 | # Leaderboard 86 | - `https://screeps.com/api/leaderboard/seasons` 87 | - `{ ok, seasons: [ { _id, name, date } ] }` 88 | - the `_id` returned here is used for the season name in the other leaderboard calls 89 | 90 | - `https://screeps.com/api/leaderboard/find?mode=world&season=2015-09&username=danny` 91 | - `{ ok, _id, season, user, score, rank }` 92 | - `user` (not `_id`) is the user's _id, as returned by `me` and `user/find` 93 | - `rank` is 0-based 94 | 95 | - `https://screeps.com/api/leaderboard/find?mode=world&username=danny` 96 | - `{ ok, list: [ _id, season, user, score, rank ] }` 97 | - lists ranks in all seasons 98 | 99 | - `https://screeps.com/api/leaderboard/list?limit=10&mode=world&offset=10&season=2015-09` 100 | - `{ ok, list: [ { _id, season, user, score, rank } ], count, users: { : { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } } }` 101 | 102 | # Messages 103 | - `https://screeps.com/api/user/messages/index` 104 | - `{ ok, messages: [ { _id, message: { _id, user, respondent, date, type, text, unread } } ], users: { : { _id, username, badge: { type, color1, color2, color3, param, flip } } } }` 105 | 106 | - `https://screeps.com/api/user/messages/list?respondent=55605a6654db1fa952de8d90` 107 | - `{ ok, messages: [ { _id, date, type, text, unread } ] }` 108 | 109 | - [POST] `https://screeps.com/api/user/messages/send` 110 | - post data: `{ respondent, text }` 111 | - `respondent` is the long _id of the user, not the username 112 | - response: `{ ok }` 113 | 114 | - `https://screeps.com/api/user/messages/unread-count` 115 | - `{ ok, count }` 116 | 117 | # User information 118 | - `https://screeps.com/api/auth/me` 119 | - `{ ok, _id, email, username, cpu, badge: { type, color1, color2, color3, param, flip }, password, notifyPrefs: { sendOnline, errorsInterval, disabledOnMessages, disabled, interval }, gcl, credits, lastChargeTime, lastTweetTime, github: { id, username }, twitter: { username, followers_count } }` 120 | 121 | - `https://screeps.com/api/user/find?username=danny` 122 | - `{ ok, user: { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } }` 123 | 124 | - `https://screeps.com/api/user/find?id=55c91dc66e36d62a6167e1b5` 125 | - `{ ok, user: { _id, username, badge: { type, color1, color2, color3, param, flip }, gcl } }` 126 | 127 | - `https://screeps.com/api/user/overview?interval=1440&statName=energyControl` 128 | - `{ ok, rooms: [ ], stats: { : [ { value, endTime } ] }, statsMax }` 129 | 130 | - `https://screeps.com/api/user/respawn-prohibited-rooms` 131 | - `{ ok, rooms: [ ] }` 132 | 133 | - `https://screeps.com/api/user/world-status` 134 | - `{ ok, status }` 135 | - `status` can be `"lost"`, `"empty"` or `"normal"`, lost is when you loose all your spawns, empty is when you have respawned and not placed your spawn yet.; 136 | 137 | - `https://screeps.com/api/user/world-start-room` 138 | - `{ ok, room: [ ] }` 139 | - `room` is an array, but seems to always contain only one element 140 | 141 | - `https://screeps.com/api/xsolla/user` 142 | - `{ ok, active }` 143 | - `active` is the Unix timestamp of the end of your current subscribed time 144 | 145 | - [POST] `https://screeps.com/api/user/console` 146 | - post data: `{ expression }` 147 | - response: `{ ok, result: { ok, n }, ops: [ { user, expression, _id } ], insertedCount, insertedIds: [ ] }` 148 | 149 | - `https://screeps.com/api/user/memory?path=flags.Flag1` 150 | - `{ ok, data }` 151 | - `data` is the string `gz:` followed by base64-encoded gzipped JSON encoding of the requested memory path 152 | - the path may be empty or absent to retrieve all of Memory 153 | 154 | - [POST] `https://screeps.com/api/user/memory` 155 | - post data: `{ path, value }` 156 | - response: `{ ok, result: { ok, n }, ops: [ { user, expression, hidden } ], data, insertedCount, insertedIds }` 157 | 158 | - `https://screeps.com/api/user/memory-segment?segment=[0-99]` 159 | - `{ okay, data }` 160 | - response: `{ ok, data: '' }` 161 | 162 | - [POST ]`https://screeps.com/api/user/memory-segment` 163 | - post data: `{ segment, data }` 164 | 165 | 166 | # Manipulating the game world 167 | - [POST] `https://screeps.com/api/game/gen-unique-object-name` 168 | - post data: `{ type }` 169 | - response: `{ ok, name }` 170 | - `type` can be at least "flag" or "spawn" 171 | 172 | - [POST] `https://screeps.com/api/game/create-flag` 173 | - post data: `{ room, x, y, name, color, secondaryColor }` 174 | - response: `{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }` 175 | - if the name is new, `result.upserted[0]._id` is the game id of the created flag 176 | - if not, this moves the flag and the response does not contain the id (but the id doesn't change) 177 | - `connection` looks like some internal MongoDB thing that is irrelevant to us 178 | 179 | - [POST] `https://screeps.com/api/game/change-flag` 180 | - post data: `{ _id, room, x, y }` 181 | - response: `{ ok, result: { nModified, ok, n }, connection: { host, id, port } }` 182 | 183 | - [POST] `https://screeps.com/api/game/change-flag-color` 184 | - post data: `{ _id, color, secondaryColor }` 185 | - response: `{ ok, result: { nModified, ok, n }, connection: { host, id, port } }` 186 | 187 | - [POST] `https://screeps.com/api/game/add-object-intent` 188 | - post data: `{ _id, room, name, intent }` 189 | - response: `{ ok, result: { nModified, ok, upserted: [ { index, _id } ], n }, connection: { host, id, port } }` 190 | - `_id` is the game id of the object to affect (except for destroying structures), `room` is the name of the room it's in 191 | - this method is used for a variety of actions, depending on the `name` and `intent` parameters 192 | - remove flag: `name = "remove"`, `intent = {}` 193 | - destroy structure: `_id = "room"`, `name = "destroyStructure"`, `intent = [ {id: , roomName, , user: } ]` 194 | - can destroy multiple structures at once 195 | - suicide creep: `name = "suicide"`, `intent = {id: }` 196 | - unclaim controller: `name = "unclaim"`, `intent = {id: }` 197 | - intent can be an empty object for suicide and unclaim, but the web interface sends the id in it, as described 198 | - remove construction site: `name = "remove"`, `intent = {}` 199 | 200 | - [POST] `https://screeps.com/api/game/set-notify-when-attacked` 201 | - post data: `{ _id, enabled }` 202 | - `enabled` is either `true` or `false` (literal values, not strings) 203 | - response: `{ ok, result: { ok, nModified, n }, connection: { id, host, port } }` 204 | 205 | - [POST] `https://screeps.com/api/game/create-construction` 206 | - post data: `{ room, structureType, x, y}` 207 | - `structureType` is the same value as one of the in-game STRUCTURE_* constants ('road', 'spawn', etc.) 208 | - `{ ok, result: { ok, n }, ops: [ { type, room, x, y, structureType, user, progress, progressTotal, _id } ], insertedCount, insertedIds }` 209 | 210 | # Other 211 | - `https://screeps.com/api/game/time` 212 | - `{ ok, time }` 213 | 214 | - [GET/POST] `https://screeps.com/api/user/code` 215 | - for pushing or pulling code, as documented at http://support.screeps.com/hc/en-us/articles/203022612 216 | 217 | - [POST] `https://screeps.com/api/game/map-stats` 218 | - post data: `{ rooms: [ ], statName: "..."}` 219 | - statName can be "owner0", "claim0", or a stat (see the enumeration above) followed by an interval 220 | - if it is owner0 or claim0, there's no separate stat block in the response 221 | - response: `{ ok, stats: { : { status, novice, own: { user, level }, : [ { user, value } ] } }, users: { : { _id, username, badge: { type, color1, color2, color3, param, flip } } } }` 222 | - `status` and `novice` are as per the `room-status` call 223 | - `level` can be 0 to indicate a reserved room 224 | 225 | - `https://screeps.com/room-history/E1N8/12340.json` 226 | - `{ timestamp, room, base, ticks: {