dredless 0.2.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/LICENSE +21 -0
- package/README.md +48 -0
- package/docs/API_REFERENCE.md +2481 -0
- package/docs/BROWSER_API.md +98 -0
- package/docs/NODE_API.md +359 -0
- package/package.json +173 -0
- package/src/browser.d.ts +212 -0
- package/src/browser.js +133 -0
- package/src/client/core.js +1202 -0
- package/src/client/domains/index.js +1818 -0
- package/src/client/index.d.ts +2 -0
- package/src/client/index.js +1 -0
- package/src/compression/lz4.d.ts +1 -0
- package/src/compression/lz4.js +112 -0
- package/src/constants.js +74 -0
- package/src/crypto/chacha-native.d.ts +9 -0
- package/src/crypto/chacha-native.js +48 -0
- package/src/crypto/chacha.d.ts +25 -0
- package/src/crypto/chacha.js +274 -0
- package/src/data/item-schema.js +2235 -0
- package/src/data/official-client-data.js +156 -0
- package/src/events.js +39 -0
- package/src/index.d.ts +2277 -0
- package/src/index.js +125 -0
- package/src/network/browser-session.js +291 -0
- package/src/network/connection.d.ts +1 -0
- package/src/network/connection.js +31 -0
- package/src/network/cookies.js +85 -0
- package/src/network/http.js +34 -0
- package/src/network/index.d.ts +4 -0
- package/src/network/index.js +4 -0
- package/src/network/servers.d.ts +7 -0
- package/src/network/servers.js +133 -0
- package/src/network/session.d.ts +7 -0
- package/src/network/session.js +320 -0
- package/src/network/ships.d.ts +12 -0
- package/src/network/ships.js +73 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +8 -0
- package/src/protocol/codec/binary.d.ts +3 -0
- package/src/protocol/codec/binary.js +52 -0
- package/src/protocol/codec/msgpack.d.ts +3 -0
- package/src/protocol/codec/msgpack.js +555 -0
- package/src/protocol/inbound/frame.d.ts +1 -0
- package/src/protocol/inbound/frame.js +8 -0
- package/src/protocol/inbound/world-payload.d.ts +3 -0
- package/src/protocol/inbound/world-payload.js +21 -0
- package/src/protocol/index.d.ts +11 -0
- package/src/protocol/index.js +19 -0
- package/src/protocol/outbound/blueprint.d.ts +22 -0
- package/src/protocol/outbound/blueprint.js +36 -0
- package/src/protocol/outbound/commands.d.ts +3 -0
- package/src/protocol/outbound/commands.js +149 -0
- package/src/protocol/outbound/comms.d.ts +28 -0
- package/src/protocol/outbound/comms.js +40 -0
- package/src/protocol/outbound/inventory.d.ts +64 -0
- package/src/protocol/outbound/inventory.js +147 -0
- package/src/protocol/outbound/ship-management.d.ts +84 -0
- package/src/protocol/outbound/ship-management.js +221 -0
- package/src/protocol/outbound/sign.d.ts +18 -0
- package/src/protocol/outbound/sign.js +45 -0
- package/src/protocol/outbound/ui-config.d.ts +157 -0
- package/src/protocol/outbound/ui-config.js +578 -0
- package/src/runtime.js +36 -0
- package/src/state/generator-maze.d.ts +32 -0
- package/src/state/generator-maze.js +370 -0
- package/src/state/index.d.ts +3 -0
- package/src/state/index.js +3 -0
- package/src/state/items.js +20 -0
- package/src/state/model/decoder.js +1554 -0
- package/src/state/model/index.d.ts +33 -0
- package/src/state/model/index.js +2 -0
- package/src/state/model/loader-config.js +1020 -0
- package/src/state/model/reader.js +69 -0
- package/src/state/model/state.js +1257 -0
- package/src/state/overworld.js +72 -0
- package/src/state/tilesets.js +53 -0
- package/src/state/world/index.d.ts +29 -0
- package/src/state/world/index.js +180 -0
- package/src/state/world/state.js +297 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import {DEFAULT_BASE_URL} from "../constants.js";
|
|
2
|
+
import {hostName, normalizeBaseUrl} from "../runtime.js";
|
|
3
|
+
import {HttpClient} from "./http.js";
|
|
4
|
+
|
|
5
|
+
const cache = new Map();
|
|
6
|
+
|
|
7
|
+
class ServerDirectory {
|
|
8
|
+
constructor(baseUrl = DEFAULT_BASE_URL) {
|
|
9
|
+
this.baseUrl = normalizeBaseUrl(baseUrl);
|
|
10
|
+
this.meta = null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async load() {
|
|
14
|
+
if (cache.has(this.baseUrl)) {
|
|
15
|
+
this.meta = await cache.get(this.baseUrl);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
const promise = this.#fetchMeta();
|
|
19
|
+
cache.set(this.baseUrl, promise);
|
|
20
|
+
this.meta = await promise;
|
|
21
|
+
return this;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async version() {
|
|
25
|
+
await this.load();
|
|
26
|
+
if (!this.meta.version) throw new Error("Unable to locate GAME_VERSION in drednot bundle");
|
|
27
|
+
return this.meta.version;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async list() {
|
|
31
|
+
return this.servers();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async servers() {
|
|
35
|
+
await this.load();
|
|
36
|
+
if (!this.meta.serverInfo) throw new Error("Unable to locate server list in drednot bundle");
|
|
37
|
+
return this.meta.serverInfo.servers;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async get(id = null) {
|
|
41
|
+
return this.pick(id);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async pick(id = null) {
|
|
45
|
+
const servers = await this.servers();
|
|
46
|
+
return servers.find((server) => server.index === Number(id)) || servers[0] || null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async #fetchMeta() {
|
|
50
|
+
const response = await new HttpClient({baseUrl: this.baseUrl}).request("index.html");
|
|
51
|
+
if (!response.ok) throw new Error(`Failed to fetch bundle: ${response.status} ${response.statusText}`);
|
|
52
|
+
const text = await response.text();
|
|
53
|
+
return {text, version: parseGameVersion(text), serverInfo: parseServerInfo(text, this.baseUrl)};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const fetchGameVersion = (baseUrl = DEFAULT_BASE_URL) => new ServerDirectory(baseUrl).version();
|
|
58
|
+
export const fetchServers = (baseUrl = DEFAULT_BASE_URL) => new ServerDirectory(baseUrl).servers();
|
|
59
|
+
|
|
60
|
+
export async function fetchNoticeVersion(baseUrl = DEFAULT_BASE_URL) {
|
|
61
|
+
const response = await new HttpClient({baseUrl}).request("index.html");
|
|
62
|
+
if (!response.ok) throw new Error(`Failed to fetch notice version: ${response.status} ${response.statusText}`);
|
|
63
|
+
const text = await response.text();
|
|
64
|
+
const version = parseNoticeVersion(text);
|
|
65
|
+
if (version == null) throw new Error("Unable to locate notice version in drednot page");
|
|
66
|
+
return version;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function resolveServer(server, baseUrl = DEFAULT_BASE_URL) {
|
|
70
|
+
if (server && typeof server === "object") return server;
|
|
71
|
+
const id = Number(server);
|
|
72
|
+
const servers = await fetchServers(baseUrl);
|
|
73
|
+
return servers.find((item) => item.index === id) || null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function serverId(server) {
|
|
77
|
+
if (server && typeof server === "object") return Number(server.index ?? server.id);
|
|
78
|
+
return Number(server);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseGameVersion(text) {
|
|
82
|
+
return text.match(/Object\.defineProperty\(exports,"GAME_VERSION",\{enumerable:true,get:\(\)=>e\}\);const e="([^"]+)"/)?.[1] || null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseNoticeVersion(text) {
|
|
86
|
+
const moduleMatch = text.match(/Object\.defineProperty\(exports,"checkNotice"[\s\S]{0,8000}?setCookie\)\("notice_version",([A-Za-z_$][\w$]*)\)/);
|
|
87
|
+
if (moduleMatch) {
|
|
88
|
+
const constant = moduleMatch[1];
|
|
89
|
+
const prefix = text.slice(Math.max(0, moduleMatch.index - 1000), moduleMatch.index + moduleMatch[0].length);
|
|
90
|
+
const constantMatch = prefix.match(new RegExp(`const\\s+${constant}\\s*=\\s*(\\d+)\\s*;`));
|
|
91
|
+
if (constantMatch) return Number(constantMatch[1]);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const patterns = [
|
|
95
|
+
/notice_version["']?\s*[:=]\s*["']?(\d+)/i,
|
|
96
|
+
/noticeVersion["']?\s*[:=]\s*["']?(\d+)/i,
|
|
97
|
+
/NOTICE_VERSION["']?\s*[:=]\s*["']?(\d+)/,
|
|
98
|
+
/notice[^0-9]{0,40}version[^0-9]{0,20}(\d+)/i
|
|
99
|
+
];
|
|
100
|
+
for (const pattern of patterns) {
|
|
101
|
+
const match = text.match(pattern);
|
|
102
|
+
if (match) return Number(match[1]);
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseServerInfo(text, baseUrl) {
|
|
108
|
+
const host = hostName(baseUrl);
|
|
109
|
+
const match = text.match(/const n=(true|false);const \$="([^"]+)";const o="([^"]+)";const T="([^"]+)";const t="([^"]+)";const S=\[([^\]]+)\];const c=\[([^\]]+)\];const A="([^"]+)";const I="([^"]+)"/s);
|
|
110
|
+
if (!match) return null;
|
|
111
|
+
const [localDomain, baseDomain, testDomain, testGameDomain] = match.slice(2, 6);
|
|
112
|
+
const domains = [...match[6].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
113
|
+
const names = [...match[7].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((m) => m[1]);
|
|
114
|
+
const meta = {
|
|
115
|
+
localDomain,
|
|
116
|
+
baseDomain,
|
|
117
|
+
testDomain,
|
|
118
|
+
testGameDomain,
|
|
119
|
+
masterDomain: match[8],
|
|
120
|
+
testMasterDomain: match[9]
|
|
121
|
+
};
|
|
122
|
+
if (host === localDomain) return {mode: "local", servers: [server(0, localDomain, "Local Test 1")], meta};
|
|
123
|
+
if (host === testDomain) return {mode: "test", servers: [server(0, testGameDomain, "Public Test")], meta};
|
|
124
|
+
return {
|
|
125
|
+
mode: "production",
|
|
126
|
+
servers: domains.map((domain, index) => server(index, domain, names[index] || `Server ${index + 1}`)),
|
|
127
|
+
meta
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function server(index, domain, description) {
|
|
132
|
+
return {index, domain, description, playerCount: 0, maxPlayerCount: 0, ping: null};
|
|
133
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import {DEFAULT_BASE_URL, DEFAULT_NOTICE_VERSION, JOIN_USER_AGENT} from "../constants.js";
|
|
2
|
+
import {asNumber, isNode, normalizeBaseUrl} from "../runtime.js";
|
|
3
|
+
import {cookieName, cookiePrefix, readSetCookies, setCookieValues} from "./cookies.js";
|
|
4
|
+
import {HttpClient} from "./http.js";
|
|
5
|
+
import {fetchGameVersion, resolveServer, serverId} from "./servers.js";
|
|
6
|
+
import {createInviteShipSpec, createShipSpec, fetchShipList, fetchShips, shipRef} from "./ships.js";
|
|
7
|
+
import {Connection} from "./connection.js";
|
|
8
|
+
import {DredlessClient} from "../client/index.js";
|
|
9
|
+
|
|
10
|
+
export class Session {
|
|
11
|
+
constructor(gameSession = null, gameVersion = null, baseUrl = DEFAULT_BASE_URL) {
|
|
12
|
+
this.baseUrl = baseUrl;
|
|
13
|
+
this.cookies = setCookieValues(this.baseUrl, {});
|
|
14
|
+
this.gameVersion = gameVersion || null;
|
|
15
|
+
this._noticeVersion = null;
|
|
16
|
+
this.account = null;
|
|
17
|
+
this.geoServer = null;
|
|
18
|
+
this.upgraded = false;
|
|
19
|
+
this.isRegistered = false;
|
|
20
|
+
this.showAds = false;
|
|
21
|
+
this.forceTutorial = false;
|
|
22
|
+
this.ban = null;
|
|
23
|
+
if (gameSession) this.cookies.set(cookieName(this.baseUrl, "game_session"), String(gameSession));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get gameSession() {
|
|
27
|
+
return this.cookies.get(cookieName(this.baseUrl, "game_session")) || "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get gameToken() {
|
|
31
|
+
return this.cookies.get(cookieName(this.baseUrl, "game_token")) || "";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get noticeVersion() {
|
|
35
|
+
return this._noticeVersion ?? this.cookies.get(cookieName(this.baseUrl, "notice_version")) ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
set noticeVersion(value) {
|
|
39
|
+
this._noticeVersion = value == null ? null : Number(value);
|
|
40
|
+
const key = cookieName(this.baseUrl, "notice_version");
|
|
41
|
+
if (value == null) this.cookies.delete(key);
|
|
42
|
+
else this.cookies.set(key, String(value));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
request(path, init = {}) {
|
|
46
|
+
return new HttpClient({baseUrl: this.baseUrl, session: this}).request(path, init);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async fetchAccountStatus() {
|
|
50
|
+
const response = await this.request("account/status", {
|
|
51
|
+
method: "GET",
|
|
52
|
+
mode: "cors",
|
|
53
|
+
credentials: "include",
|
|
54
|
+
referrer: `${this.baseUrl}/`,
|
|
55
|
+
headers: statusHeaders(this.baseUrl)
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok) throw new Error(`account/status failed: ${response.status} ${response.statusText}`);
|
|
58
|
+
this.mergeSetCookies(response);
|
|
59
|
+
const raw = await response.json();
|
|
60
|
+
this.#readStatus(raw);
|
|
61
|
+
return raw;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async fetchShips(server) {
|
|
65
|
+
return fetchShips(this, server);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async fetchShipList(server) {
|
|
69
|
+
return fetchShipList(this, server);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async joinShipConnection(server, ship = null) {
|
|
73
|
+
return this.#startShipConnection(server, ship, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async startShipConnection(server, ship = null) {
|
|
77
|
+
return this.#startShipConnection(server, ship, false);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async startNewShipConnection(server, name = "", color = "") {
|
|
81
|
+
return this.#startShipConnection(server, createShipSpec(name, color), false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async joinInviteConnection(server, code) {
|
|
85
|
+
return this.#startShipConnection(server, createInviteShipSpec(code), false);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async joinShip(server, ship = null) {
|
|
89
|
+
return readyClient(await this.joinShipConnection(server, ship));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async startShip(server, ship = null) {
|
|
93
|
+
return readyClient(await this.startShipConnection(server, ship));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async startNewShip(server, name = "", color = "") {
|
|
97
|
+
return readyClient(await this.startNewShipConnection(server, name, color));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async joinInvite(server, code) {
|
|
101
|
+
return readyClient(await this.joinInviteConnection(server, code));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
toJSON() {
|
|
105
|
+
return {
|
|
106
|
+
baseUrl: this.baseUrl,
|
|
107
|
+
gameSession: this.gameSession,
|
|
108
|
+
gameToken: this.gameToken,
|
|
109
|
+
gameVersion: this.gameVersion,
|
|
110
|
+
noticeVersion: this.noticeVersion,
|
|
111
|
+
cookies: Object.fromEntries(this.cookies),
|
|
112
|
+
account: this.account,
|
|
113
|
+
geoServer: this.geoServer,
|
|
114
|
+
upgraded: this.upgraded,
|
|
115
|
+
isRegistered: this.isRegistered,
|
|
116
|
+
showAds: this.showAds,
|
|
117
|
+
forceTutorial: this.forceTutorial,
|
|
118
|
+
ban: this.ban,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async #startShipConnection(server, ship, neverLoad) {
|
|
123
|
+
if (server == null) throw new Error("A server id or server object is required");
|
|
124
|
+
const resolvedServer = await resolveServer(server, this.baseUrl);
|
|
125
|
+
if (!resolvedServer) throw new Error(`Unable to resolve server ${serverId(server)}`);
|
|
126
|
+
if (!this.gameVersion) this.gameVersion = await fetchGameVersion(this.baseUrl);
|
|
127
|
+
if (this.noticeVersion == null) this.noticeVersion = DEFAULT_NOTICE_VERSION;
|
|
128
|
+
|
|
129
|
+
const joinShip = ship == null ? createShipSpec("", "") : shipRef(ship);
|
|
130
|
+
const response = await this.request("join", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
mode: "cors",
|
|
133
|
+
credentials: "include",
|
|
134
|
+
referrer: `${this.baseUrl}/`,
|
|
135
|
+
body: {
|
|
136
|
+
join_info: {
|
|
137
|
+
game_version: this.gameVersion,
|
|
138
|
+
hide_badges: false,
|
|
139
|
+
never_load: Boolean(neverLoad),
|
|
140
|
+
server_id: resolvedServer.index,
|
|
141
|
+
ship: joinShip
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
headers: joinHeaders(this.baseUrl)
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) throw new Error(`Join request failed: ${await response.text()}`);
|
|
147
|
+
this.mergeSetCookies(response, ["game_session", "anon_key", "game_token"]);
|
|
148
|
+
const join = await response.json();
|
|
149
|
+
if (join.reject != null) {
|
|
150
|
+
const error = new Error(joinRejectMessage(join.reject, {
|
|
151
|
+
neverLoad,
|
|
152
|
+
ship: joinShip,
|
|
153
|
+
server: resolvedServer
|
|
154
|
+
}));
|
|
155
|
+
error.code = join.reject;
|
|
156
|
+
error.join = join;
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
if (join.okay !== true) throw new Error("Bad join result");
|
|
160
|
+
|
|
161
|
+
const token = this.gameToken;
|
|
162
|
+
if (!token) throw new Error("Join did not return a game_token cookie");
|
|
163
|
+
return new Connection(this, token, join.net_port, Number.isInteger(join.server_id) ? join.server_id : resolvedServer.index, resolvedServer);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
mergeSetCookies(response, names = ["game_session", "anon_key"]) {
|
|
167
|
+
const prefix = cookiePrefix(this.baseUrl);
|
|
168
|
+
for (const [name, value] of readSetCookies(response.headers, names, this.baseUrl)) {
|
|
169
|
+
const cookieName = name.startsWith(prefix) ? name : `${prefix}${name}`;
|
|
170
|
+
this.cookies.set(cookieName, value);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#readStatus(raw) {
|
|
175
|
+
const account = raw?.account && typeof raw.account === "object" ? raw.account : null;
|
|
176
|
+
this.account = account ? {
|
|
177
|
+
name: account.name || account.username || account.display_name || account.displayName || account.handle || "Unknown",
|
|
178
|
+
color: asNumber(account.color, 0),
|
|
179
|
+
game_rank: asNumber(account.game_rank, 0),
|
|
180
|
+
user_badges: Array.isArray(account.user_badges) ? account.user_badges.filter((item) => typeof item === "string") : [],
|
|
181
|
+
is_registered: Boolean(account.is_registered)
|
|
182
|
+
} : null;
|
|
183
|
+
this.geoServer = asNumber(raw?.geo_server ?? raw?.geoServer, null);
|
|
184
|
+
this.upgraded = Boolean(raw?.upgraded ?? account?.upgraded);
|
|
185
|
+
this.isRegistered = Boolean(account?.is_registered);
|
|
186
|
+
this.showAds = Boolean(raw?.show_ads ?? raw?.showAds);
|
|
187
|
+
this.forceTutorial = Boolean(raw?.force_tutorial ?? raw?.forceTutorial);
|
|
188
|
+
this.ban = raw?.ban || null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export class AnonSession extends Session {
|
|
193
|
+
constructor(gameSession = null, anonKey = null, gameVersion = null, baseUrl = DEFAULT_BASE_URL) {
|
|
194
|
+
super(gameSession, gameVersion, baseUrl);
|
|
195
|
+
if (anonKey) this.cookies.set(cookieName(this.baseUrl, "anon_key"), String(anonKey));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
get anonKey() {
|
|
199
|
+
return this.cookies.get(cookieName(this.baseUrl, "anon_key")) || "";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
toJSON() {
|
|
203
|
+
return {
|
|
204
|
+
...super.toJSON(),
|
|
205
|
+
anonKey: this.anonKey
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function createSession(noticeVersion = null, baseUrl = DEFAULT_BASE_URL) {
|
|
211
|
+
const resolvedBaseUrl = normalizeBaseUrl(arguments.length >= 2 ? arguments[1] : baseUrl);
|
|
212
|
+
const session = new Session(null, null, resolvedBaseUrl);
|
|
213
|
+
session.noticeVersion = await internalNoticeVersion(noticeVersion, resolvedBaseUrl);
|
|
214
|
+
await session.fetchAccountStatus();
|
|
215
|
+
return session;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export async function createAnonToken(noticeVersion = null, baseUrl = DEFAULT_BASE_URL) {
|
|
219
|
+
const resolvedBaseUrl = normalizeBaseUrl(arguments.length >= 2 ? arguments[1] : baseUrl);
|
|
220
|
+
const session = new AnonSession(null, null, null, resolvedBaseUrl);
|
|
221
|
+
session.noticeVersion = await internalNoticeVersion(noticeVersion, resolvedBaseUrl);
|
|
222
|
+
const response = await session.request("account/login/anon", {
|
|
223
|
+
method: "POST",
|
|
224
|
+
mode: "cors",
|
|
225
|
+
credentials: "include",
|
|
226
|
+
referrer: `${session.baseUrl}/`,
|
|
227
|
+
headers: anonHeaders(session.baseUrl)
|
|
228
|
+
});
|
|
229
|
+
if (response.status === 503) throw new Error("Anon token creation is rate limited by the server");
|
|
230
|
+
if (!response.ok) throw new Error(`account/login/anon failed: ${response.status} ${response.statusText}`);
|
|
231
|
+
session.mergeSetCookies(response, ["anon_key", "game_session"]);
|
|
232
|
+
if (!session.anonKey) throw new Error("Unable to obtain anon_key");
|
|
233
|
+
return session.anonKey;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export async function createAnonSession(anonKey = null, noticeVersion = null, baseUrl = DEFAULT_BASE_URL) {
|
|
237
|
+
if (typeof noticeVersion === "string" && noticeVersion.includes("://") && (baseUrl == null || baseUrl === DEFAULT_BASE_URL)) {
|
|
238
|
+
baseUrl = noticeVersion;
|
|
239
|
+
noticeVersion = null;
|
|
240
|
+
}
|
|
241
|
+
const resolvedBaseUrl = normalizeBaseUrl(arguments.length >= 3 ? arguments[2] : baseUrl);
|
|
242
|
+
const resolvedAnonKey = anonKey || await createAnonToken(noticeVersion, resolvedBaseUrl);
|
|
243
|
+
const session = new AnonSession(null, resolvedAnonKey, null, resolvedBaseUrl);
|
|
244
|
+
session.baseUrl = resolvedBaseUrl;
|
|
245
|
+
session.noticeVersion = await internalNoticeVersion(noticeVersion, resolvedBaseUrl);
|
|
246
|
+
await session.fetchAccountStatus();
|
|
247
|
+
return session;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function readyClient(connection) {
|
|
251
|
+
const client = new DredlessClient(connection);
|
|
252
|
+
await client.whenReady();
|
|
253
|
+
return client;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function internalNoticeVersion(value, baseUrl = DEFAULT_BASE_URL) {
|
|
257
|
+
if (value != null) return value;
|
|
258
|
+
try {
|
|
259
|
+
const {fetchNoticeVersion} = await import("./servers.js");
|
|
260
|
+
return await fetchNoticeVersion(baseUrl);
|
|
261
|
+
} catch (_) {
|
|
262
|
+
return DEFAULT_NOTICE_VERSION;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function statusHeaders(baseUrl) {
|
|
267
|
+
const headers = {accept: "*/*"};
|
|
268
|
+
if (isNode()) {
|
|
269
|
+
Object.assign(headers, {
|
|
270
|
+
"user-agent": JOIN_USER_AGENT,
|
|
271
|
+
"accept-language": "en-US,en;q=0.9",
|
|
272
|
+
referer: `${normalizeBaseUrl(baseUrl)}/`,
|
|
273
|
+
dnt: "1",
|
|
274
|
+
"sec-gpc": "1",
|
|
275
|
+
"sec-fetch-dest": "empty",
|
|
276
|
+
"sec-fetch-mode": "cors",
|
|
277
|
+
"sec-fetch-site": "same-origin"
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
return headers;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function anonHeaders(baseUrl) {
|
|
284
|
+
const origin = normalizeBaseUrl(baseUrl);
|
|
285
|
+
return {
|
|
286
|
+
accept: "*/*",
|
|
287
|
+
origin,
|
|
288
|
+
referer: `${origin}/`
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function joinHeaders(baseUrl) {
|
|
293
|
+
const headers = {
|
|
294
|
+
accept: "*/*",
|
|
295
|
+
"accept-language": "en-US,en;q=0.9",
|
|
296
|
+
"content-type": "application/json",
|
|
297
|
+
origin: normalizeBaseUrl(baseUrl),
|
|
298
|
+
referer: `${normalizeBaseUrl(baseUrl)}/`,
|
|
299
|
+
pragma: "no-cache",
|
|
300
|
+
"cache-control": "no-cache",
|
|
301
|
+
dnt: "1",
|
|
302
|
+
"sec-gpc": "1",
|
|
303
|
+
"sec-fetch-dest": "empty",
|
|
304
|
+
"sec-fetch-mode": "cors",
|
|
305
|
+
"sec-fetch-site": "same-origin",
|
|
306
|
+
priority: "u=0"
|
|
307
|
+
};
|
|
308
|
+
if (isNode()) headers["user-agent"] = JOIN_USER_AGENT;
|
|
309
|
+
return headers;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function joinRejectMessage(code, {neverLoad, ship, server} = {}) {
|
|
313
|
+
if (code === 4007 && neverLoad) {
|
|
314
|
+
const name = ship?.name ? ` "${ship.name}"` : "";
|
|
315
|
+
const id = ship?.id != null ? ` (${ship.id})` : "";
|
|
316
|
+
const serverText = server?.description ? ` on ${server.description}` : "";
|
|
317
|
+
return `join rejected: 4007. ${name}${id}${serverText} could not be joined with never_load=true. Use session.startShip(server, ship) instead of session.joinShip(server, ship) when loading or starting a saved ship.`;
|
|
318
|
+
}
|
|
319
|
+
return `join rejected: ${code}`;
|
|
320
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
fetchShips,
|
|
3
|
+
fetchShipList,
|
|
4
|
+
type Ship,
|
|
5
|
+
type ShipList,
|
|
6
|
+
type ShipRef,
|
|
7
|
+
type ShipSpec
|
|
8
|
+
} from "../index.js";
|
|
9
|
+
|
|
10
|
+
export function createShipSpec(name?: string, color?: string): ShipSpec;
|
|
11
|
+
|
|
12
|
+
export function createInviteShipSpec(code: string): ShipSpec;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {DEFAULT_BASE_URL} from "../constants.js";
|
|
2
|
+
import {asNumber} from "../runtime.js";
|
|
3
|
+
import {HttpClient} from "./http.js";
|
|
4
|
+
|
|
5
|
+
export function createShipSpec(name = "", color = "") {
|
|
6
|
+
return {type: "new", name: String(name ?? ""), color: String(color ?? "")};
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function createInviteShipSpec(code) {
|
|
10
|
+
return {type: "invite", code: String(code ?? "")};
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function shipRef(ship) {
|
|
14
|
+
if (ship == null) return null;
|
|
15
|
+
if (typeof ship !== "object") return {type: "join_or_load", id: ship};
|
|
16
|
+
if (ship.type) return ship;
|
|
17
|
+
const id = ship.id ?? ship.hexCode ?? ship.hex_code;
|
|
18
|
+
if (id != null) return {type: "join_or_load", id};
|
|
19
|
+
return ship;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const normalizeShipSpec = shipRef;
|
|
23
|
+
|
|
24
|
+
export function normalizeShip(raw, key = null) {
|
|
25
|
+
if (!raw || typeof raw !== "object") return null;
|
|
26
|
+
const id = Number(raw.id ?? key);
|
|
27
|
+
return {
|
|
28
|
+
id: Number.isFinite(id) ? id : null,
|
|
29
|
+
hexCode: raw.hex_code || raw.hexCode || raw.id || key || null,
|
|
30
|
+
name: raw.team_name || "Unknown Ship",
|
|
31
|
+
iconUrl: raw.icon_path || null,
|
|
32
|
+
playerCount: asNumber(raw.player_count, 0),
|
|
33
|
+
owned: Boolean(raw.owned),
|
|
34
|
+
saved: Boolean(raw.saved),
|
|
35
|
+
color: raw.color || "",
|
|
36
|
+
time: asNumber(raw.time, null)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractShips(data) {
|
|
41
|
+
if (Array.isArray(data)) return data.map((ship, index) => [ship?.id || String(index), ship]);
|
|
42
|
+
if (Array.isArray(data?.ships)) return data.ships.map((ship, index) => [ship?.id || String(index), ship]);
|
|
43
|
+
if (data?.ships && typeof data.ships === "object") return Object.entries(data.ships);
|
|
44
|
+
return [];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function fetchShips(session, server, baseUrl = session?.baseUrl || DEFAULT_BASE_URL) {
|
|
48
|
+
const data = await fetchShipList(session, server, baseUrl);
|
|
49
|
+
return Array.isArray(data) ? data : Array.isArray(data?.ships) ? data.ships : [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function fetchShipList(session, server, baseUrl = session?.baseUrl || DEFAULT_BASE_URL) {
|
|
53
|
+
if (!session) throw new Error("fetchShipList requires a session");
|
|
54
|
+
const serverId = Number(server && typeof server === "object" ? server.index ?? server.id : server ?? session.geoServer ?? 0);
|
|
55
|
+
const data = await new HttpClient({baseUrl, session}).json(`shiplist?server=${serverId}`, {
|
|
56
|
+
headers: {accept: "application/json"}
|
|
57
|
+
});
|
|
58
|
+
const ships = normalizeShipList(data);
|
|
59
|
+
if (Array.isArray(data)) return {ships};
|
|
60
|
+
if (data && typeof data === "object") {
|
|
61
|
+
return {
|
|
62
|
+
playerCount: asNumber(data.player_count, 0),
|
|
63
|
+
maxPlayerCount: asNumber(data.max_player_count, 0),
|
|
64
|
+
isMuted: Boolean(data.is_muted),
|
|
65
|
+
ships
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {ships};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeShipList(data) {
|
|
72
|
+
return extractShips(data).map(([key, raw]) => normalizeShip(raw, key)).filter(Boolean);
|
|
73
|
+
}
|
package/src/node.d.ts
ADDED
package/src/node.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import {enableNativeChaCha20} from "./crypto/chacha-native.js";
|
|
2
|
+
|
|
3
|
+
// Node resolves the package root through this entry, so opting into the native
|
|
4
|
+
// ChaCha20 backend here keeps `node:crypto` out of the browser build.
|
|
5
|
+
enableNativeChaCha20();
|
|
6
|
+
|
|
7
|
+
export * from "./index.js";
|
|
8
|
+
export {default} from "./index.js";
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import {encoder} from "../../constants.js";
|
|
2
|
+
|
|
3
|
+
function objectTag(value) {
|
|
4
|
+
return Object.prototype.toString.call(value);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function isArrayBufferLike(value) {
|
|
8
|
+
const tag = objectTag(value);
|
|
9
|
+
return tag === "[object ArrayBuffer]" || tag === "[object SharedArrayBuffer]";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function toUint8Array(value) {
|
|
13
|
+
if (value instanceof Uint8Array) return value;
|
|
14
|
+
if (isArrayBufferLike(value)) return new Uint8Array(value);
|
|
15
|
+
if (ArrayBuffer.isView(value)) {
|
|
16
|
+
return new Uint8Array(value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength));
|
|
17
|
+
}
|
|
18
|
+
if (Array.isArray(value)) return Uint8Array.from(value);
|
|
19
|
+
if (typeof value === "string") return encoder.encode(value);
|
|
20
|
+
throw new Error(`Unsupported binary value: ${objectTag(value)}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function concatBytes(parts) {
|
|
24
|
+
const total = parts.reduce((sum, part) => sum + part.length, 0);
|
|
25
|
+
const out = new Uint8Array(total);
|
|
26
|
+
let offset = 0;
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
out.set(part, offset);
|
|
29
|
+
offset += part.length;
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function valuesEqual(a, b) {
|
|
35
|
+
if (a === b) return true;
|
|
36
|
+
if (a == null || b == null) return a == null && b == null;
|
|
37
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
38
|
+
const aKeys = Object.keys(a);
|
|
39
|
+
const bKeys = Object.keys(b);
|
|
40
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
41
|
+
for (const key of aKeys) {
|
|
42
|
+
if (!valuesEqual(a[key], b[key])) return false;
|
|
43
|
+
}
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
toUint8Array,
|
|
50
|
+
concatBytes,
|
|
51
|
+
valuesEqual
|
|
52
|
+
};
|