@screepts/screeps-api 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,859 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ import Debug from "debug";
3
+ import { inflate } from "pako";
4
+ import yaml from "js-yaml";
5
+ //#region src/Socket.ts
6
+ const debug = Debug("screepsapi:socket");
7
+ const DEFAULTS$1 = {
8
+ reconnect: true,
9
+ resubscribe: true,
10
+ keepAlive: true,
11
+ maxRetries: 10,
12
+ maxRetryDelay: 60 * 1e3
13
+ };
14
+ var Socket = class extends EventEmitter {
15
+ ws;
16
+ api;
17
+ opts;
18
+ authed = false;
19
+ connected = false;
20
+ reconnecting = false;
21
+ keepAliveInter = 0;
22
+ __queue = [];
23
+ __subQueue = [];
24
+ __subs = {};
25
+ constructor(api) {
26
+ super();
27
+ this.api = api;
28
+ this.opts = Object.assign({}, DEFAULTS$1);
29
+ this.on("error", () => {});
30
+ this.reset();
31
+ this.on("auth", (ev) => {
32
+ if (ev.data.status === "ok") {
33
+ while (this.__queue.length) this.ws?.send(this.__queue.shift());
34
+ clearInterval(this.keepAliveInter);
35
+ if (this.opts.keepAlive) this.keepAliveInter = setInterval(() => this.ws && this.ws.send(""), 1e4);
36
+ }
37
+ });
38
+ }
39
+ reset() {
40
+ this.authed = false;
41
+ this.connected = false;
42
+ this.reconnecting = false;
43
+ clearInterval(this.keepAliveInter);
44
+ this.keepAliveInter = 0;
45
+ this.__queue = [];
46
+ this.__subQueue = [];
47
+ this.__subs = {};
48
+ }
49
+ async connect(opts = {}) {
50
+ Object.assign(this.opts, opts);
51
+ if (!this.api.token) throw new Error("No token! Call api.auth() before connecting the socket!");
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.addEventListener("open", () => {
57
+ this.connected = true;
58
+ this.reconnecting = false;
59
+ if (this.opts.resubscribe) this.__subQueue.push(...Object.keys(this.__subs));
60
+ debug("connected");
61
+ this.emit("connected");
62
+ resolve(this.auth(this.api.token));
63
+ });
64
+ this.ws.onclose = () => {
65
+ clearInterval(this.keepAliveInter);
66
+ this.authed = false;
67
+ this.connected = false;
68
+ debug("disconnected");
69
+ this.emit("disconnected");
70
+ if (this.opts.reconnect) this.reconnect().catch(() => {});
71
+ };
72
+ this.ws.addEventListener("error", (err) => {
73
+ this.ws?.close();
74
+ this.emit("error", err);
75
+ debug(`error ${err.error || err}`);
76
+ if (!this.connected) reject(err);
77
+ });
78
+ this.ws.addEventListener("message", (data) => this.handleMessage(data));
79
+ });
80
+ }
81
+ async reconnect() {
82
+ if (this.reconnecting) return;
83
+ this.reconnecting = true;
84
+ let retries = 0;
85
+ let retry;
86
+ do {
87
+ let time = Math.pow(2, retries) * 100;
88
+ if (time > this.opts.maxRetryDelay) time = this.opts.maxRetryDelay;
89
+ await this.sleep(time);
90
+ if (!this.reconnecting) return;
91
+ try {
92
+ await this.connect();
93
+ retry = false;
94
+ } catch {
95
+ retry = true;
96
+ }
97
+ retries++;
98
+ debug(`reconnect ${retries}/${this.opts.maxRetries}`);
99
+ } while (retry && retries < this.opts.maxRetries);
100
+ if (retry) {
101
+ const err = /* @__PURE__ */ new Error(`Reconnection failed after ${this.opts.maxRetries} retries`);
102
+ this.reconnecting = false;
103
+ debug("reconnect failed");
104
+ this.emit("error", err);
105
+ throw err;
106
+ } else Object.keys(this.__subs).forEach((sub) => this.subscribe(sub));
107
+ }
108
+ disconnect() {
109
+ debug("disconnect");
110
+ clearInterval(this.keepAliveInter);
111
+ if (this.ws) {
112
+ this.ws.onclose = null;
113
+ this.ws.close();
114
+ }
115
+ this.reset();
116
+ this.emit("disconnected");
117
+ }
118
+ sleep(time) {
119
+ return new Promise((resolve) => setTimeout(resolve, time));
120
+ }
121
+ handleMessage(ev) {
122
+ const msg = this.api.gz(ev.data || ev);
123
+ debug(`message ${msg}`);
124
+ if (msg[0] === "[") {
125
+ const arr = JSON.parse(msg);
126
+ let [, type, id, channel] = arr[0].match(/^(.+):(.+?)(?:\/(.+))?$/);
127
+ channel = channel || type;
128
+ const event = {
129
+ channel,
130
+ id,
131
+ type,
132
+ data: arr[1]
133
+ };
134
+ this.emit(msg[0], event);
135
+ this.emit(event.channel, event);
136
+ this.emit("message", event);
137
+ } else {
138
+ const [channel, ...data] = msg.split(" ");
139
+ const event = {
140
+ type: "server",
141
+ channel,
142
+ data
143
+ };
144
+ if (channel === "auth") event.data = {
145
+ status: data[0],
146
+ token: data[1]
147
+ };
148
+ if ([
149
+ "protocol",
150
+ "time",
151
+ "package"
152
+ ].includes(channel)) event.data = { [channel]: data[0] };
153
+ this.emit(channel, event);
154
+ this.emit("message", event);
155
+ }
156
+ }
157
+ async gzip(bool) {
158
+ await this.send(`gzip ${bool ? "on" : "off"}`);
159
+ }
160
+ async send(data) {
161
+ if (!this.connected) this.__queue.push(data);
162
+ else this.ws.send(data);
163
+ }
164
+ async auth(token) {
165
+ const waitAuth = new Promise((resolve) => this.once("auth", resolve));
166
+ await this.send(`auth ${token}`);
167
+ const { data } = await waitAuth;
168
+ if (data.status !== "ok") throw new Error("socket auth failed");
169
+ this.authed = true;
170
+ this.emit("token", data.token);
171
+ this.emit("authed");
172
+ while (this.__subQueue.length) await this.send(this.__subQueue.shift());
173
+ }
174
+ async subscribe(path, cb) {
175
+ if (!path) return;
176
+ const userID = await this.api.userID();
177
+ if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`;
178
+ if (this.authed) await this.send(`subscribe ${path}`);
179
+ else this.__subQueue.push(`subscribe ${path}`);
180
+ this.emit("subscribe", path);
181
+ this.__subs[path] = this.__subs[path] || 0;
182
+ this.__subs[path]++;
183
+ if (cb) this.on(path, cb);
184
+ }
185
+ async unsubscribe(path) {
186
+ if (!path) return;
187
+ const userID = await this.api.userID();
188
+ if (!path.match(/^(\w+):(.+?)$/)) path = `user:${userID}/${path}`;
189
+ await this.send(`unsubscribe ${path}`);
190
+ this.emit("unsubscribe", path);
191
+ if (this.__subs[path]) this.__subs[path]--;
192
+ }
193
+ };
194
+ //#endregion
195
+ //#region src/RawAPI.ts
196
+ const debugHttp = Debug("screepsapi:http");
197
+ const debugRateLimit = Debug("screepsapi:ratelimit");
198
+ const DEFAULT_SHARD = "shard0";
199
+ const OFFICIAL_HISTORY_INTERVAL = 100;
200
+ const PRIVATE_HISTORY_INTERVAL = 20;
201
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
202
+ const mapToShard = (res) => {
203
+ if (!res.shards) res.shards = { privSrv: res.list || res.rooms };
204
+ return res;
205
+ };
206
+ var RawAPI = class extends EventEmitter {
207
+ opts;
208
+ token;
209
+ __authed = false;
210
+ constructor(opts = {}) {
211
+ super();
212
+ this.opts = {};
213
+ this.setServer(opts);
214
+ }
215
+ raw = {
216
+ token: void 0,
217
+ version: () => this.req("GET", "/api/version"),
218
+ authmod: () => {
219
+ if (this.isOfficialServer()) return Promise.resolve({
220
+ ok: 1,
221
+ name: "official"
222
+ });
223
+ return this.req("GET", "/api/authmod");
224
+ },
225
+ history: (room, tick, shard = DEFAULT_SHARD) => {
226
+ if (this.isOfficialServer()) {
227
+ tick -= tick % OFFICIAL_HISTORY_INTERVAL;
228
+ return this.req("GET", `/room-history/${shard}/${room}/${tick}.json`);
229
+ } else {
230
+ tick -= tick % PRIVATE_HISTORY_INTERVAL;
231
+ return this.req("GET", "/room-history", {
232
+ room,
233
+ time: tick
234
+ });
235
+ }
236
+ },
237
+ servers: { list: () => this.req("POST", "/api/servers/list", {}) },
238
+ auth: {
239
+ signin: (email, password) => this.req("POST", "/api/auth/signin", {
240
+ email,
241
+ password
242
+ }),
243
+ steamTicket: (ticket, useNativeAuth = false) => this.req("POST", "/api/auth/steam-ticket", {
244
+ ticket,
245
+ useNativeAuth
246
+ }),
247
+ me: () => this.req("GET", "/api/auth/me"),
248
+ queryToken: (token) => this.req("GET", "/api/auth/query-token", { token })
249
+ },
250
+ register: {
251
+ checkEmail: (email) => this.req("GET", "/api/register/check-email", { email }),
252
+ checkUsername: (username) => this.req("GET", "/api/register/check-username", { username }),
253
+ setUsername: (username) => this.req("POST", "/api/register/set-username", { username }),
254
+ submit: (username, email, password, modules) => this.req("POST", "/api/register/submit", {
255
+ username,
256
+ email,
257
+ password,
258
+ modules
259
+ })
260
+ },
261
+ userMessages: {
262
+ list: (respondent) => this.req("GET", "/api/user/messages/list", { respondent }),
263
+ index: () => this.req("GET", "/api/user/messages/index"),
264
+ unreadCount: () => this.req("GET", "/api/user/messages/unread-count"),
265
+ send: (respondent, text) => this.req("POST", "/api/user/messages/send", {
266
+ respondent,
267
+ text
268
+ }),
269
+ markRead: (id) => this.req("POST", "/api/user/messages/mark-read", { id })
270
+ },
271
+ game: {
272
+ mapStats: (rooms, statName, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/map-stats", {
273
+ rooms,
274
+ statName,
275
+ shard
276
+ }),
277
+ genUniqueObjectName: (type, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/gen-unique-object-name", {
278
+ type,
279
+ shard
280
+ }),
281
+ checkUniqueObjectName: (type, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/check-unique-object-name", {
282
+ type,
283
+ name,
284
+ shard
285
+ }),
286
+ placeSpawn: (room, x, y, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/place-spawn", {
287
+ name,
288
+ room,
289
+ x,
290
+ y,
291
+ shard
292
+ }),
293
+ createFlag: (room, x, y, name, color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-flag", {
294
+ name,
295
+ room,
296
+ x,
297
+ y,
298
+ color,
299
+ secondaryColor,
300
+ shard
301
+ }),
302
+ genUniqueFlagName: (shard = DEFAULT_SHARD) => this.req("POST", "/api/game/gen-unique-flag-name", { shard }),
303
+ checkUniqueFlagName: (name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/check-unique-flag-name", {
304
+ name,
305
+ shard
306
+ }),
307
+ changeFlagColor: (color = 1, secondaryColor = 1, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/change-flag-color", {
308
+ color,
309
+ secondaryColor,
310
+ shard
311
+ }),
312
+ removeFlag: (room, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/remove-flag", {
313
+ name,
314
+ room,
315
+ shard
316
+ }),
317
+ addObjectIntent: (room, name, intent, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/add-object-intent", {
318
+ room,
319
+ name,
320
+ intent,
321
+ shard
322
+ }),
323
+ createConstruction: (room, x, y, structureType, name, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-construction", {
324
+ room,
325
+ x,
326
+ y,
327
+ structureType,
328
+ name,
329
+ shard
330
+ }),
331
+ setNotifyWhenAttacked: (_id, enabled = true, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/set-notify-when-attacked", {
332
+ _id,
333
+ enabled,
334
+ shard
335
+ }),
336
+ createInvader: (room, x, y, size, type, boosted = false, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/create-invader", {
337
+ room,
338
+ x,
339
+ y,
340
+ size,
341
+ type,
342
+ boosted,
343
+ shard
344
+ }),
345
+ removeInvader: (_id, shard = DEFAULT_SHARD) => this.req("POST", "/api/game/remove-invader", {
346
+ _id,
347
+ shard
348
+ }),
349
+ time: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/time", { shard }),
350
+ worldSize: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/world-size", { shard }),
351
+ roomDecorations: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-decorations", {
352
+ room,
353
+ shard
354
+ }),
355
+ roomObjects: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-objects", {
356
+ room,
357
+ shard
358
+ }),
359
+ roomTerrain: (room, encoded = 1, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-terrain", {
360
+ room,
361
+ encoded,
362
+ shard
363
+ }),
364
+ roomStatus: (room, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-status", {
365
+ room,
366
+ shard
367
+ }),
368
+ roomOverview: (room, interval = 8, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/room-overview", {
369
+ room,
370
+ interval,
371
+ shard
372
+ }),
373
+ market: {
374
+ ordersIndex: (shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/orders-index", { shard }),
375
+ myOrders: () => this.req("GET", "/api/game/market/my-orders").then(mapToShard),
376
+ orders: (resourceType, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/orders", {
377
+ resourceType,
378
+ shard
379
+ }),
380
+ stats: (resourceType, shard = DEFAULT_SHARD) => this.req("GET", "/api/game/market/stats", {
381
+ resourceType,
382
+ shard
383
+ })
384
+ },
385
+ shards: { info: () => this.req("GET", "/api/game/shards/info") }
386
+ },
387
+ leaderboard: {
388
+ list: (limit = 10, mode = "world", offset = 0, season) => {
389
+ if (mode !== "world" && mode !== "power") throw new Error("incorrect mode parameter");
390
+ if (!season) season = this.currentSeason();
391
+ return this.req("GET", "/api/leaderboard/list", {
392
+ limit,
393
+ mode,
394
+ offset,
395
+ season
396
+ });
397
+ },
398
+ find: (username, mode = "world", season = "") => this.req("GET", "/api/leaderboard/find", {
399
+ season,
400
+ mode,
401
+ username
402
+ }),
403
+ seasons: () => this.req("GET", "/api/leaderboard/seasons")
404
+ },
405
+ user: {
406
+ badge: (badge) => this.req("POST", "/api/user/badge", { badge }),
407
+ respawn: () => this.req("POST", "/api/user/respawn"),
408
+ setActiveBranch: (branch, activeName) => this.req("POST", "/api/user/set-active-branch", {
409
+ branch,
410
+ activeName
411
+ }),
412
+ cloneBranch: (branch = "", newName, defaultModules) => this.req("POST", "/api/user/clone-branch", {
413
+ branch,
414
+ newName,
415
+ defaultModules
416
+ }),
417
+ deleteBranch: (branch) => this.req("POST", "/api/user/delete-branch", { branch }),
418
+ notifyPrefs: (prefs) => this.req("POST", "/api/user/notify-prefs", prefs),
419
+ tutorialDone: () => this.req("POST", "/api/user/tutorial-done"),
420
+ email: (email) => this.req("POST", "/api/user/email", { email }),
421
+ worldStartRoom: (shard) => this.req("GET", "/api/user/world-start-room", { shard }),
422
+ worldStatus: () => this.req("GET", "/api/user/world-status"),
423
+ branches: () => this.req("GET", "/api/user/branches"),
424
+ code: {
425
+ get: (branch) => this.req("GET", "/api/user/code", { branch }),
426
+ set: (branch, modules, _hash) => {
427
+ if (!_hash) _hash = Date.now();
428
+ return this.req("POST", "/api/user/code", {
429
+ branch,
430
+ modules,
431
+ _hash
432
+ });
433
+ }
434
+ },
435
+ decorations: {
436
+ inventory: () => this.req("GET", "/api/user/decorations/inventory"),
437
+ themes: () => this.req("GET", "/api/user/decorations/themes"),
438
+ convert: (decorations) => this.req("POST", "/api/user/decorations/convert", { decorations }),
439
+ pixelize: (count, theme = "") => this.req("POST", "/api/user/decorations/pixelize", {
440
+ count,
441
+ theme
442
+ }),
443
+ activate: (_id, active) => this.req("POST", "/api/user/decorations/activate", {
444
+ _id,
445
+ active
446
+ }),
447
+ deactivate: (decorations) => this.req("POST", "/api/user/decorations/deactivate", { decorations })
448
+ },
449
+ respawnProhibitedRooms: () => this.req("GET", "/api/user/respawn-prohibited-rooms"),
450
+ memory: {
451
+ get: (path = "", shard = DEFAULT_SHARD) => this.req("GET", "/api/user/memory", {
452
+ path,
453
+ shard
454
+ }),
455
+ set: (path, value, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/memory", {
456
+ path,
457
+ value,
458
+ shard
459
+ }),
460
+ segment: {
461
+ get: (segment, shard = DEFAULT_SHARD) => this.req("GET", "/api/user/memory-segment", {
462
+ segment,
463
+ shard
464
+ }),
465
+ set: (segment, data, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/memory-segment", {
466
+ segment,
467
+ data,
468
+ shard
469
+ })
470
+ }
471
+ },
472
+ find: (username) => this.req("GET", "/api/user/find", { username }),
473
+ findById: (id) => this.req("GET", "/api/user/find", { id }),
474
+ stats: (interval) => this.req("GET", "/api/user/stats", { interval }),
475
+ rooms: (id) => this.req("GET", "/api/user/rooms", { id }).then(mapToShard),
476
+ overview: (interval, statName) => this.req("GET", "/api/user/overview", {
477
+ interval,
478
+ statName
479
+ }),
480
+ moneyHistory: (page = 0) => this.req("GET", "/api/user/money-history", { page }),
481
+ console: (expression, shard = DEFAULT_SHARD) => this.req("POST", "/api/user/console", {
482
+ expression,
483
+ shard
484
+ }),
485
+ name: () => this.req("GET", "/api/user/name")
486
+ },
487
+ experimental: {
488
+ pvp: (interval = 100) => this.req("GET", "/api/experimental/pvp", { interval }).then(mapToShard),
489
+ nukes: () => this.req("GET", "/api/experimental/nukes").then(mapToShard)
490
+ },
491
+ warpath: { battles: (interval = 100) => this.req("GET", "/api/warpath/battles", { interval }) },
492
+ scoreboard: { list: (limit = 20, offset = 0) => this.req("GET", "/api/scoreboard/list", {
493
+ limit,
494
+ offset
495
+ }) }
496
+ };
497
+ currentSeason() {
498
+ const now = /* @__PURE__ */ new Date();
499
+ const year = now.getFullYear();
500
+ let month = (now.getUTCMonth() + 1).toString();
501
+ if (month.length === 1) month = `0${month}`;
502
+ return `${year}-${month}`;
503
+ }
504
+ isOfficialServer() {
505
+ return this.opts.url.match(/screeps\.com/) !== null;
506
+ }
507
+ mapToShard = mapToShard;
508
+ setServer(opts) {
509
+ if (!this.opts) this.opts = {};
510
+ Object.assign(this.opts, opts);
511
+ if (opts.path && !opts.pathname) this.opts.pathname = opts.path;
512
+ if (opts.port) {
513
+ this.opts.port = String(opts.port);
514
+ if (opts.hostname) this.opts.host = `${opts.hostname}:${opts.port}`;
515
+ }
516
+ if (!opts.url) {
517
+ this.opts.url = urlFormat(this.opts);
518
+ if (!this.opts.url.endsWith("/")) this.opts.url += "/";
519
+ }
520
+ if (opts.token) this.token = opts.token;
521
+ }
522
+ async auth(email, password, opts = {}) {
523
+ this.setServer(opts);
524
+ if (email && password) Object.assign(this.opts, {
525
+ email,
526
+ password
527
+ });
528
+ const res = await this.raw.auth.signin(this.opts.email, this.opts.password);
529
+ this.emit("token", res.token);
530
+ this.emit("auth");
531
+ this.__authed = true;
532
+ return res;
533
+ }
534
+ async req(method, path, body = {}) {
535
+ let url = new URL(path, this.opts.url);
536
+ const opts = {
537
+ method,
538
+ headers: {}
539
+ };
540
+ if (debugHttp.enabled) debugHttp(`${method} ${path}`);
541
+ if (this.token) Object.assign(opts.headers, {
542
+ "X-Token": this.token,
543
+ "X-Username": this.token
544
+ });
545
+ if (method === "GET") Object.entries(body).forEach(([key, value]) => {
546
+ if (value !== void 0) url.searchParams.append(key, String(value));
547
+ });
548
+ else {
549
+ opts.body = body;
550
+ if (typeof body === "object") {
551
+ opts.body = JSON.stringify(body);
552
+ opts.headers["Content-Type"] = "application/json";
553
+ }
554
+ }
555
+ const res = await fetch(url, opts);
556
+ const token = res.headers.get("x-token");
557
+ if (token) this.emit("token", token);
558
+ const rateLimit = this.buildRateLimit(method, path, res);
559
+ this.emit("rateLimit", rateLimit);
560
+ debugRateLimit(`${method} ${path} ${rateLimit.remaining}/${rateLimit.limit} ${rateLimit.toReset}s`);
561
+ if (res.headers.get("content-type")?.includes("application/json")) res.data = await res.json();
562
+ else res.data = await res.text();
563
+ if (!res.ok) {
564
+ if (res.status === 401) if (this.__authed && this.opts.email && this.opts.password) {
565
+ this.__authed = false;
566
+ await this.auth(this.opts.email, this.opts.password);
567
+ return this.req(method, path, body);
568
+ } else throw new Error("Not Authorized");
569
+ else if (res.status === 429 && !res.headers.get("x-ratelimit-limit") && this.opts.experimentalRetry429) {
570
+ await sleep(Math.floor(Math.random() * 500) + 200);
571
+ return this.req(method, path, body);
572
+ }
573
+ throw new Error(res.data);
574
+ }
575
+ this.emit("response", res);
576
+ return res.data;
577
+ }
578
+ gz(data) {
579
+ if (!data.startsWith("gz:")) return data;
580
+ return inflate(Buffer.from(data.slice(3), "base64"), { to: "string" });
581
+ }
582
+ inflate(data) {
583
+ return JSON.parse(this.gz(data));
584
+ }
585
+ buildRateLimit(method, path, res) {
586
+ const limit = Number(res.headers.get("x-ratelimit-limit"));
587
+ const remaining = Number(res.headers.get("x-ratelimit-remaining"));
588
+ const reset = Number(res.headers.get("x-ratelimit-reset"));
589
+ return {
590
+ method,
591
+ path,
592
+ limit,
593
+ remaining,
594
+ reset,
595
+ toReset: reset - Math.floor(Date.now() / 1e3)
596
+ };
597
+ }
598
+ };
599
+ /** Based on Node.js built-in URL format */
600
+ function urlFormat(obj) {
601
+ let protocol = obj.protocol || "";
602
+ if (protocol && !protocol.endsWith(":")) protocol += ":";
603
+ let pathname = obj.pathname || "";
604
+ let host = "";
605
+ if (obj.host) host = obj.host;
606
+ else if (obj.hostname) {
607
+ host = obj.hostname.includes(":") && (obj.hostname[0] !== "[" || obj.hostname[obj.hostname.length - 1] !== "]") ? "[" + obj.hostname + "]" : obj.hostname;
608
+ if (obj.port) host += ":" + obj.port;
609
+ }
610
+ if (pathname.includes("#") || pathname.includes("?")) {
611
+ let newPathname = "";
612
+ let lastPos = 0;
613
+ const len = pathname.length;
614
+ for (let i = 0; i < len; i++) {
615
+ const code = pathname.charAt(i);
616
+ if (code === "#" || code === "?") {
617
+ if (i > lastPos) newPathname += pathname.slice(lastPos, i);
618
+ newPathname += code === "#" ? "%23" : "%3F";
619
+ lastPos = i + 1;
620
+ }
621
+ }
622
+ if (lastPos < len) newPathname += pathname.slice(lastPos);
623
+ pathname = newPathname;
624
+ }
625
+ if (host) {
626
+ if (pathname && pathname[0] !== "/") pathname = "/" + pathname;
627
+ host = "//" + host;
628
+ }
629
+ return protocol + host + pathname;
630
+ }
631
+ //#endregion
632
+ //#region src/ConfigManager.ts
633
+ /**
634
+ * Utility class for loading Unified Credentials Files from disk, environment variables, or other sources.
635
+ * Only platforms without filesystem access, {@link ConfigManager.setConfig}, must be used to provide the config data directly.
636
+ */
637
+ var ConfigManager = class {
638
+ path;
639
+ _config = null;
640
+ /** Custom environment variables, useful for platforms without process.env */
641
+ env = globalThis.process?.env || {};
642
+ /** Custom file reading function, useful for platforms with a virtual filesystem */
643
+ readFile;
644
+ setConfig(data, path) {
645
+ if (!data || typeof data !== "object" || !("servers" in data)) throw new Error(`Invalid config: 'servers' object does not exist in '${path}'`);
646
+ this._config = data;
647
+ this.path = path;
648
+ return this._config;
649
+ }
650
+ async refresh() {
651
+ this._config = null;
652
+ await this.getConfig();
653
+ }
654
+ async getServers() {
655
+ const conf = await this.getConfig();
656
+ return conf ? Object.keys(conf.servers) : [];
657
+ }
658
+ async getConfig() {
659
+ if (this._config) return this._config;
660
+ const paths = [];
661
+ if (this.env.SCREEPS_CONFIG) paths.push(this.env.SCREEPS_CONFIG);
662
+ const dirs = ["", import.meta.dirname];
663
+ for (const dir of dirs) {
664
+ paths.push(join(dir, ".screeps.yaml"));
665
+ paths.push(join(dir, ".screeps.yml"));
666
+ }
667
+ if (process.platform === "win32" && this.env.APPDATA) {
668
+ paths.push(join(this.env.APPDATA, "screeps/config.yaml"));
669
+ paths.push(join(this.env.APPDATA, "screeps/config.yml"));
670
+ } else {
671
+ if (this.env.XDG_CONFIG_HOME) {
672
+ paths.push(join(this.env.XDG_CONFIG_HOME, "screeps/config.yaml"));
673
+ paths.push(join(this.env.XDG_CONFIG_HOME, "screeps/config.yml"));
674
+ }
675
+ if (this.env.HOME) {
676
+ paths.push(join(this.env.HOME, ".config/screeps/config.yaml"));
677
+ paths.push(join(this.env.HOME, ".config/screeps/config.yml"));
678
+ paths.push(join(this.env.HOME, ".screeps.yaml"));
679
+ paths.push(join(this.env.HOME, ".screeps.yml"));
680
+ }
681
+ }
682
+ for (const path of paths) {
683
+ const data = await this.loadConfig(path);
684
+ if (data) return data;
685
+ }
686
+ return null;
687
+ }
688
+ async loadConfig(path) {
689
+ if (!this.readFile) {
690
+ const { readFile } = await import("fs/promises");
691
+ this.readFile = readFile;
692
+ }
693
+ let contents;
694
+ try {
695
+ contents = await this.readFile(path, { encoding: "utf8" });
696
+ } catch (e) {
697
+ if (e.code === "ENOENT") return null;
698
+ else throw e;
699
+ }
700
+ const data = yaml.load(contents);
701
+ return this.setConfig(data, path);
702
+ }
703
+ };
704
+ function join(a, b) {
705
+ return a + (a.endsWith("/") ? "" : "/") + b;
706
+ }
707
+ //#endregion
708
+ //#region src/ScreepsAPI.ts
709
+ const DEFAULTS = {
710
+ protocol: "https",
711
+ hostname: "screeps.com",
712
+ port: 443,
713
+ path: "/"
714
+ };
715
+ const configManager = new ConfigManager();
716
+ var ScreepsAPI = class ScreepsAPI extends RawAPI {
717
+ socket;
718
+ appConfig = {};
719
+ rateLimits;
720
+ _user;
721
+ _tokenInfo;
722
+ static async fromConfig(server = "main", config = false, opts = {}) {
723
+ const data = await configManager.getConfig();
724
+ if (!data) throw new Error("No valid config found");
725
+ if (!server && process.stdin.isTTY && process.stdout.isTTY) {
726
+ const { select, isCancel } = await import("@clack/prompts");
727
+ const selectedServer = await select({
728
+ message: "Select a server:",
729
+ withGuide: false,
730
+ options: Object.entries(data.servers).map(([value, args]) => ({
731
+ value,
732
+ hint: args.host
733
+ }))
734
+ });
735
+ if (isCancel(selectedServer)) throw new Error("Server selection cancelled");
736
+ server = selectedServer;
737
+ }
738
+ const conf = data.servers[server];
739
+ if (!conf) throw new Error(`Server '${server}' does not exist in '${configManager.path}'`);
740
+ if (conf.ptr) conf.path = "/ptr";
741
+ if (conf.season) conf.path = "/season";
742
+ const api = new ScreepsAPI(Object.assign({
743
+ hostname: conf.host,
744
+ protocol: conf.secure ? "https" : "http",
745
+ path: "/"
746
+ }, conf, opts));
747
+ api.appConfig = data.configs && data.configs[config] || {};
748
+ if (!conf.token && conf.username && conf.password) await api.auth(conf.username, conf.password);
749
+ return api;
750
+ }
751
+ constructor(opts = {}) {
752
+ opts = Object.assign({}, DEFAULTS, opts);
753
+ super(opts);
754
+ this.on("token", (token) => {
755
+ this.token = token;
756
+ this.raw.token = token;
757
+ });
758
+ const defaultLimit = (limit, period) => ({
759
+ limit,
760
+ period,
761
+ remaining: limit,
762
+ reset: 0,
763
+ toReset: 0
764
+ });
765
+ this.rateLimits = {
766
+ global: defaultLimit(120, "minute"),
767
+ GET: {
768
+ "/api/game/room-terrain": defaultLimit(360, "hour"),
769
+ "/api/user/code": defaultLimit(60, "hour"),
770
+ "/api/user/memory": defaultLimit(1440, "day"),
771
+ "/api/user/memory-segment": defaultLimit(360, "hour"),
772
+ "/api/game/market/orders-index": defaultLimit(60, "hour"),
773
+ "/api/game/market/orders": defaultLimit(60, "hour"),
774
+ "/api/game/market/my-orders": defaultLimit(60, "hour"),
775
+ "/api/game/market/stats": defaultLimit(60, "hour"),
776
+ "/api/game/user/money-history": defaultLimit(60, "hour")
777
+ },
778
+ POST: {
779
+ "/api/user/console": defaultLimit(360, "hour"),
780
+ "/api/game/map-stats": defaultLimit(60, "hour"),
781
+ "/api/user/code": defaultLimit(240, "day"),
782
+ "/api/user/set-active-branch": defaultLimit(240, "day"),
783
+ "/api/user/memory": defaultLimit(240, "day"),
784
+ "/api/user/memory-segment": defaultLimit(60, "hour")
785
+ }
786
+ };
787
+ this.on("rateLimit", (limits) => {
788
+ const rate = this.rateLimits[limits.method]?.[limits.path] || this.rateLimits.global;
789
+ const copy = Object.assign({}, limits);
790
+ delete copy.path;
791
+ delete copy.method;
792
+ Object.assign(rate, copy);
793
+ });
794
+ this.socket = new Socket(this);
795
+ }
796
+ getRateLimit(method, path) {
797
+ return this.rateLimits[method][path] || this.rateLimits.global;
798
+ }
799
+ get rateLimitResetUrl() {
800
+ return `https://screeps.com/a/#!/account/auth-tokens/noratelimit?token=${this.token.slice(0, 8)}`;
801
+ }
802
+ async me() {
803
+ if (this._user) return this._user;
804
+ if ((await this.tokenInfo()).full) this._user = await this.raw.auth.me();
805
+ else {
806
+ const { username } = await this.raw.user.name();
807
+ const { ok, user } = await this.raw.user.find(username);
808
+ this._user = {
809
+ ...user,
810
+ ok
811
+ };
812
+ }
813
+ return this._user;
814
+ }
815
+ async tokenInfo() {
816
+ if (this._tokenInfo) return this._tokenInfo;
817
+ if ("token" in this.opts) {
818
+ const { token } = await this.raw.auth.queryToken(this.token);
819
+ this._tokenInfo = token;
820
+ } else this._tokenInfo = { full: true };
821
+ return this._tokenInfo;
822
+ }
823
+ async userID() {
824
+ return (await this.me())._id;
825
+ }
826
+ registerUser = this.raw.register.submit;
827
+ history = this.raw.history;
828
+ authmod = this.raw.authmod;
829
+ version = this.raw.version;
830
+ time = this.raw.game.time;
831
+ leaderboard = this.raw.leaderboard;
832
+ market = this.raw.game.market;
833
+ console = this.raw.user.console;
834
+ code = codeRepository(this);
835
+ memory = {
836
+ ...this.raw.user.memory,
837
+ get: async (path, shard) => {
838
+ const { data } = await this.raw.user.memory.get(path, shard);
839
+ return this.gz(data);
840
+ }
841
+ };
842
+ segment = this.raw.user.memory.segment;
843
+ };
844
+ const codeRepository = ({ raw: { user } }) => ({
845
+ get: user.code.get,
846
+ set: async (branch, code) => {
847
+ const { list } = await user.branches();
848
+ if (list.some((b) => b.branch == branch)) return user.code.set(branch, code);
849
+ else return user.cloneBranch("", branch, code);
850
+ },
851
+ branches: user.branches,
852
+ cloneBranch: user.cloneBranch,
853
+ deleteBranch: user.deleteBranch,
854
+ setActiveBranch: user.setActiveBranch
855
+ });
856
+ //#endregion
857
+ export { ConfigManager, ScreepsAPI };
858
+
859
+ //# sourceMappingURL=index.mjs.map