@vaitosoi/minejs 0.3.1 → 0.4.1
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/README.md +5 -8
- package/assets/minecraft/26.2/packets.json +27 -10
- package/dist/base/buffer.d.ts +2 -0
- package/dist/base/buffer.d.ts.map +1 -0
- package/dist/base/buffer.js +12 -0
- package/dist/base/buffer.js.map +1 -0
- package/dist/base/error.d.ts +33 -6
- package/dist/base/error.d.ts.map +1 -1
- package/dist/base/error.js +74 -11
- package/dist/base/error.js.map +1 -1
- package/dist/base/http.d.ts +2 -0
- package/dist/base/http.d.ts.map +1 -0
- package/dist/base/http.js +14 -0
- package/dist/base/http.js.map +1 -0
- package/dist/base/math.d.ts +22 -1
- package/dist/base/math.d.ts.map +1 -1
- package/dist/base/math.js +49 -2
- package/dist/base/math.js.map +1 -1
- package/dist/client/auth.d.ts +41 -7
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/auth.js +96 -39
- package/dist/client/auth.js.map +1 -1
- package/dist/client/client.d.ts +50 -5
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +47 -18
- package/dist/client/client.js.map +1 -1
- package/dist/client/tcp.d.ts +1 -5
- package/dist/client/tcp.d.ts.map +1 -1
- package/dist/client/tcp.js +8 -21
- package/dist/client/tcp.js.map +1 -1
- package/dist/message/link.d.ts +10 -0
- package/dist/message/link.d.ts.map +1 -0
- package/dist/message/link.js +22 -0
- package/dist/message/link.js.map +1 -0
- package/dist/message/sigCache.d.ts +13 -0
- package/dist/message/sigCache.d.ts.map +1 -0
- package/dist/message/sigCache.js +26 -0
- package/dist/message/sigCache.js.map +1 -0
- package/dist/packet/decoder.d.ts +13 -0
- package/dist/packet/decoder.d.ts.map +1 -1
- package/dist/packet/decoder.js +21 -0
- package/dist/packet/decoder.js.map +1 -1
- package/dist/packet/dispatcher.d.ts +8 -1
- package/dist/packet/dispatcher.d.ts.map +1 -1
- package/dist/packet/dispatcher.js +142 -30
- package/dist/packet/dispatcher.js.map +1 -1
- package/dist/physics/player.d.ts +17 -23
- package/dist/physics/player.d.ts.map +1 -1
- package/dist/physics/player.js +59 -61
- package/dist/physics/player.js.map +1 -1
- package/dist/version/codec.d.ts.map +1 -1
- package/dist/version/codec.js +28 -7
- package/dist/version/codec.js.map +1 -1
- package/dist/version/registry.d.ts +1 -1
- package/dist/version/registry.d.ts.map +1 -1
- package/dist/version/registry.js +2 -7
- package/dist/version/registry.js.map +1 -1
- package/dist/world/state.d.ts +26 -2
- package/dist/world/state.d.ts.map +1 -1
- package/dist/world/state.js +58 -3
- package/dist/world/state.js.map +1 -1
- package/package.json +1 -1
- package/src/base/buffer.ts +12 -0
- package/src/base/error.ts +63 -10
- package/src/base/http.ts +10 -0
- package/src/base/math.ts +55 -3
- package/src/client/auth.ts +130 -52
- package/src/client/client.ts +96 -21
- package/src/client/tcp.ts +10 -27
- package/src/message/link.ts +17 -0
- package/src/message/sigCache.ts +25 -0
- package/src/packet/decoder.ts +21 -0
- package/src/packet/dispatcher.ts +169 -35
- package/src/physics/player.ts +83 -58
- package/src/version/codec.ts +32 -11
- package/src/version/registry.ts +2 -7
- package/src/world/state.ts +72 -4
- package/test/client/index.ts +27 -20
package/src/client/auth.ts
CHANGED
|
@@ -1,21 +1,37 @@
|
|
|
1
1
|
import http from "node:http";
|
|
2
|
-
import
|
|
3
|
-
import net from "node:net";
|
|
2
|
+
import crypto from "node:crypto";
|
|
4
3
|
import { exec } from "node:child_process";
|
|
5
|
-
import { AuthDenied,
|
|
4
|
+
import { AuthDenied, OAuthError, AuthTokenExpired, CantGetMsAccessToken, ProfileError, ProfileNotFound, XboxError, AuthClientError, MinecraftError } from "../base/error";
|
|
5
|
+
import { minecraftSha1 } from "../base/math";
|
|
6
|
+
import { getJson } from "../base/http";
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
interface Loopback {
|
|
8
9
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
10
|
+
* This method will provide you a url to click on and do the authentication.
|
|
11
|
+
*
|
|
12
|
+
* This will require an opening port in order to receive the response from Microsoft. If you cant, considering using `device_code` method.
|
|
13
|
+
*/
|
|
14
|
+
method: "loopback",
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The opening port for listening the response
|
|
11
18
|
*/
|
|
12
|
-
|
|
19
|
+
port: number
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Automatedly open browser or print a URL to click on.
|
|
16
23
|
*/
|
|
17
24
|
openBrowser: boolean,
|
|
25
|
+
}
|
|
18
26
|
|
|
27
|
+
interface DeviceCode {
|
|
28
|
+
/**
|
|
29
|
+
* This method will provide you a device code, then you have to type this code in the given URL.
|
|
30
|
+
*/
|
|
31
|
+
method: "device_code",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type AuthOption = (Loopback | DeviceCode) & {
|
|
19
35
|
/**
|
|
20
36
|
* The Client ID
|
|
21
37
|
*
|
|
@@ -101,17 +117,6 @@ function open(url: string, log: (message: string) => any) {
|
|
|
101
117
|
}
|
|
102
118
|
}
|
|
103
119
|
|
|
104
|
-
// Got this from https://github.com/sindresorhus/get-port/blob/main/index.js#L47
|
|
105
|
-
const getFreePort = () => new Promise<number>((resolve) => {
|
|
106
|
-
const server = net.createServer();
|
|
107
|
-
server.listen(0, () => {
|
|
108
|
-
const port = (server.address() as net.AddressInfo).port;
|
|
109
|
-
server.close(() =>
|
|
110
|
-
resolve(port)
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
120
|
const sleep = (ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
116
121
|
|
|
117
122
|
/**
|
|
@@ -121,34 +126,100 @@ export class AuthClient {
|
|
|
121
126
|
private readonly codeVerifier: string;
|
|
122
127
|
private readonly codeChallenge: string;
|
|
123
128
|
// private refreshToken?: string;
|
|
129
|
+
private accessToken: string = "";
|
|
130
|
+
private uuid: string = "";
|
|
124
131
|
|
|
125
132
|
constructor(private option: AuthOption) {
|
|
126
|
-
const codeVerifier =
|
|
127
|
-
const hashed =
|
|
133
|
+
const codeVerifier = crypto.randomBytes(32).toString("hex");
|
|
134
|
+
const hashed = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
128
135
|
this.option.log ||= console.log;
|
|
129
136
|
|
|
130
137
|
this.codeVerifier = codeVerifier;
|
|
131
138
|
this.codeChallenge = hashed;
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
|
|
141
|
+
/**
|
|
142
|
+
* Send a auth notify to Minecraft server, after receive Encryption Request
|
|
143
|
+
*
|
|
144
|
+
* @see https://minecraft.wiki/w/Java_Edition_protocol/Encryption#Authentication
|
|
145
|
+
*/
|
|
146
|
+
public async sendAuth(
|
|
147
|
+
// Sever things
|
|
148
|
+
serverId: string,
|
|
149
|
+
sharedSecret: Buffer,
|
|
150
|
+
publicKey: Buffer,
|
|
151
|
+
) {
|
|
152
|
+
const hash = minecraftSha1(serverId, sharedSecret, publicKey);
|
|
153
|
+
const response = await fetch(
|
|
154
|
+
"https://sessionserver.mojang.com/session/minecraft/join",
|
|
155
|
+
{
|
|
156
|
+
method: "POST",
|
|
157
|
+
headers: {
|
|
158
|
+
"Content-Type": "application/json"
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
accessToken: this.accessToken,
|
|
162
|
+
selectedProfile: this.uuid,
|
|
163
|
+
serverId: hash,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
console.log({
|
|
168
|
+
accessToken: this.accessToken,
|
|
169
|
+
selectedProfile: this.uuid,
|
|
170
|
+
serverId: hash,
|
|
171
|
+
});
|
|
172
|
+
if (response.status === 204) return;
|
|
173
|
+
const json = await getJson(response, "send auth request") as Record<string, string>;
|
|
174
|
+
throw new AuthClientError(json["error"]! + ("errorMessage" in json ? `: ${json["errorMessage"]}` : ""));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Get player key pair for signing stuff
|
|
179
|
+
*
|
|
180
|
+
* @see https://minecraft.wiki/w/Mojang_API#Get_keypair_for_signature
|
|
181
|
+
*/
|
|
182
|
+
public async getKeyPair() {
|
|
183
|
+
const response = await fetch(
|
|
184
|
+
"https://api.minecraftservices.com/player/certificates",
|
|
185
|
+
{
|
|
186
|
+
headers: {
|
|
187
|
+
Authorization: `Bearer ${this.accessToken}`
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
if (response.status !== 200) throw new AuthClientError("can't get the auth key", await response.text());
|
|
192
|
+
const json = await getJson(response, "get key pair") as any;
|
|
193
|
+
console.log(json);
|
|
194
|
+
return {
|
|
195
|
+
publicKey: String(json["keyPair"]["publicKey"]),
|
|
196
|
+
signature: String(json["publicKeySignatureV2"]),
|
|
197
|
+
expiresAt: Number(json["expiresAt"]),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get player UUID
|
|
204
|
+
*/
|
|
205
|
+
public async getUUID() {
|
|
135
206
|
if (this.option.method === "loopback") return this.loopback();
|
|
136
207
|
else return this.deviceCode();
|
|
137
208
|
}
|
|
138
209
|
|
|
139
210
|
private async loopback() {
|
|
140
|
-
const
|
|
141
|
-
const expectState =
|
|
211
|
+
const option = this.option as Loopback;
|
|
212
|
+
const expectState = crypto.randomBytes(8).toString("hex");
|
|
142
213
|
const authUrl = new URL("https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize");
|
|
143
214
|
authUrl.searchParams.append("client_id", this.option.client_id);
|
|
144
215
|
authUrl.searchParams.append("response_type", "code");
|
|
145
|
-
authUrl.searchParams.append("redirect_uri", `http://
|
|
216
|
+
authUrl.searchParams.append("redirect_uri", `http://localhost:${option.port}`);
|
|
146
217
|
authUrl.searchParams.append("scope", "XboxLive.signin offline_access");
|
|
147
218
|
authUrl.searchParams.append("code_challenge", this.codeChallenge);
|
|
148
219
|
authUrl.searchParams.append("code_challenge_method", "S256");
|
|
149
220
|
authUrl.searchParams.append("state", expectState);
|
|
150
221
|
|
|
151
|
-
if (
|
|
222
|
+
if (option.openBrowser === true)
|
|
152
223
|
open(authUrl.toString(), this.option.log!);
|
|
153
224
|
else
|
|
154
225
|
this.option.log!(`please open this url in your browser: ${authUrl.toString()}`);
|
|
@@ -156,32 +227,35 @@ export class AuthClient {
|
|
|
156
227
|
const msAuthToken = await new Promise<string | null>((resolve) => {
|
|
157
228
|
const server = http.createServer((req, res) => {
|
|
158
229
|
const end = (html: string, result: any = null) => {
|
|
159
|
-
res.
|
|
160
|
-
|
|
161
|
-
|
|
230
|
+
if (res.writable)
|
|
231
|
+
res.end(html);
|
|
232
|
+
if (server.listening) {
|
|
233
|
+
server.close();
|
|
234
|
+
return resolve(result);
|
|
235
|
+
}
|
|
162
236
|
};
|
|
163
237
|
|
|
164
|
-
const url = new URL(req.url || "", `http://
|
|
238
|
+
const url = new URL(req.url || "", `http://localhost:${option.port}`);
|
|
165
239
|
const error = url.searchParams.get("error");
|
|
166
240
|
if (error) {
|
|
167
241
|
const description = url.searchParams.get("error_description")!;
|
|
168
|
-
end(ErrorHTML.replace("{error}", error).replace("{error_description}", description));
|
|
242
|
+
return end(ErrorHTML.replace("{error}", error).replace("{error_description}", description));
|
|
169
243
|
}
|
|
170
244
|
|
|
171
245
|
const code = url.searchParams.get("code");
|
|
172
246
|
if (!code)
|
|
173
|
-
end(InvalidHTML.replace("{reason}", "Missing <code>code</code> parameter"));
|
|
247
|
+
return end(InvalidHTML.replace("{reason}", "Missing <code>code</code> parameter"));
|
|
174
248
|
|
|
175
249
|
const state = url.searchParams.get("state");
|
|
176
250
|
if (!state)
|
|
177
|
-
end(InvalidHTML.replace("{reason}", "Missing <code>state</code> parameter"));
|
|
251
|
+
return end(InvalidHTML.replace("{reason}", "Missing <code>state</code> parameter"));
|
|
178
252
|
if (state !== expectState)
|
|
179
|
-
end(InvalidHTML.replace("{reason}", "Mismatch state"));
|
|
253
|
+
return end(InvalidHTML.replace("{reason}", "Mismatch state"));
|
|
180
254
|
|
|
181
255
|
return end(SuccessHTML, code);
|
|
182
256
|
});
|
|
183
257
|
|
|
184
|
-
server.listen(
|
|
258
|
+
server.listen(option.port);
|
|
185
259
|
});
|
|
186
260
|
|
|
187
261
|
if (!msAuthToken)
|
|
@@ -190,9 +264,9 @@ export class AuthClient {
|
|
|
190
264
|
const msTokenUrl = new URL("https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
|
191
265
|
const msTokenBody = new URLSearchParams();
|
|
192
266
|
msTokenBody.append("client_id", this.option.client_id);
|
|
193
|
-
msTokenBody.append("code", msAuthToken);
|
|
194
267
|
msTokenBody.append("scope", "XboxLive.signin offline_access");
|
|
195
|
-
msTokenBody.append("
|
|
268
|
+
msTokenBody.append("code", msAuthToken);
|
|
269
|
+
msTokenBody.append("redirect_uri", `http://localhost:${option.port}`);
|
|
196
270
|
msTokenBody.append("grant_type", "authorization_code");
|
|
197
271
|
msTokenBody.append("code_verifier", this.codeVerifier);
|
|
198
272
|
const msTokenRequest = await fetch(msTokenUrl, {
|
|
@@ -202,9 +276,9 @@ export class AuthClient {
|
|
|
202
276
|
method: "POST",
|
|
203
277
|
body: msTokenBody.toString()
|
|
204
278
|
});
|
|
205
|
-
const msTokenResponse = await msTokenRequest
|
|
279
|
+
const msTokenResponse = await getJson(msTokenRequest, "microsoft token request") as Record<string, any>;
|
|
206
280
|
if ("error" in msTokenResponse)
|
|
207
|
-
throw new
|
|
281
|
+
throw new OAuthError(msTokenResponse["error"]);
|
|
208
282
|
const msToken = msTokenResponse["access_token"];
|
|
209
283
|
|
|
210
284
|
return this.getMcToken(msToken);
|
|
@@ -231,7 +305,7 @@ export class AuthClient {
|
|
|
231
305
|
this.option.log!(message);
|
|
232
306
|
|
|
233
307
|
let msToken;
|
|
234
|
-
const msTokenUrl = new URL("https://login.microsoftonline.com/
|
|
308
|
+
const msTokenUrl = new URL("https://login.microsoftonline.com/consumers/oauth2/v2.0/token");
|
|
235
309
|
const msTokenBody = new URLSearchParams();
|
|
236
310
|
msTokenBody.set("grant_type", "urn:ietf:params:oauth:grant-type:device_code");
|
|
237
311
|
msTokenBody.set("client_id", this.option.client_id);
|
|
@@ -240,9 +314,10 @@ export class AuthClient {
|
|
|
240
314
|
await sleep(interval * 1000);
|
|
241
315
|
const msTokenRequest = await fetch(msTokenUrl, {
|
|
242
316
|
headers: {
|
|
243
|
-
"Content-Type": "application/x-www-form-urlencoded"
|
|
317
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
244
318
|
},
|
|
245
|
-
method: "POST"
|
|
319
|
+
method: "POST",
|
|
320
|
+
body: msTokenBody.toString()
|
|
246
321
|
});
|
|
247
322
|
const msTokenResponse = await msTokenRequest.json() as Record<string, any>;
|
|
248
323
|
if ("error" in msTokenResponse)
|
|
@@ -251,7 +326,7 @@ export class AuthClient {
|
|
|
251
326
|
case "slow_down": interval += 5; continue;
|
|
252
327
|
case "auth_denied": throw new AuthDenied();
|
|
253
328
|
case "expired_token": throw new AuthTokenExpired();
|
|
254
|
-
default: throw new
|
|
329
|
+
default: throw new OAuthError(msTokenResponse["error"]);
|
|
255
330
|
}
|
|
256
331
|
else {
|
|
257
332
|
msToken = msTokenResponse["access_token"];
|
|
@@ -263,10 +338,9 @@ export class AuthClient {
|
|
|
263
338
|
throw new CantGetMsAccessToken();
|
|
264
339
|
|
|
265
340
|
return this.getMcToken(msToken);
|
|
266
|
-
}
|
|
341
|
+
}
|
|
267
342
|
|
|
268
343
|
private async getMcToken(msToken: string): Promise<{
|
|
269
|
-
token: string,
|
|
270
344
|
uuid: string,
|
|
271
345
|
name: string
|
|
272
346
|
}> {
|
|
@@ -287,14 +361,14 @@ export class AuthClient {
|
|
|
287
361
|
}),
|
|
288
362
|
method: "POST"
|
|
289
363
|
});
|
|
290
|
-
const xboxReponse = await xboxRequest
|
|
364
|
+
const xboxReponse = await getJson(xboxRequest, "xbox request") as Record<string, any>;
|
|
291
365
|
const xboxToken = xboxReponse["Token"];
|
|
292
366
|
const userHash = xboxReponse["DisplayClaims"]["xui"][0]["uhs"];
|
|
293
367
|
|
|
294
368
|
const xboxSecurityRequest = await fetch("https://xsts.auth.xboxlive.com/xsts/authorize", {
|
|
295
369
|
headers: {
|
|
296
370
|
"Content-Type": "application/json",
|
|
297
|
-
Accept: "application/json"
|
|
371
|
+
"Accept": "application/json",
|
|
298
372
|
},
|
|
299
373
|
body: JSON.stringify({
|
|
300
374
|
"Properties": {
|
|
@@ -306,7 +380,7 @@ export class AuthClient {
|
|
|
306
380
|
}),
|
|
307
381
|
method: "POST"
|
|
308
382
|
});
|
|
309
|
-
const xboxSecurityReponse = await xboxSecurityRequest
|
|
383
|
+
const xboxSecurityReponse = await getJson(xboxSecurityRequest, "xbox security request") as Record<string, any>;
|
|
310
384
|
if ("XErr" in xboxSecurityReponse)
|
|
311
385
|
throw new XboxError(xboxSecurityReponse["XErr"]);
|
|
312
386
|
const xboxSecurityToken = xboxSecurityReponse["Token"];
|
|
@@ -314,14 +388,17 @@ export class AuthClient {
|
|
|
314
388
|
const minecraftRequest = await fetch("https://api.minecraftservices.com/authentication/login_with_xbox", {
|
|
315
389
|
headers: {
|
|
316
390
|
"Content-Type": "application/json",
|
|
317
|
-
Accept: "application/json"
|
|
391
|
+
"Accept": "application/json",
|
|
392
|
+
'User-Agent': 'MinecraftLauncher/2.2.10675'
|
|
318
393
|
},
|
|
319
394
|
body: JSON.stringify({
|
|
320
|
-
"identityToken": `XBL3.0 x=${userHash};${xboxSecurityToken}
|
|
395
|
+
"identityToken": `XBL3.0 x=${userHash};${xboxSecurityToken}`,
|
|
321
396
|
}),
|
|
322
397
|
method: "POST"
|
|
323
398
|
});
|
|
324
|
-
const minecraftResponse = await minecraftRequest
|
|
399
|
+
const minecraftResponse = await getJson(minecraftRequest, "minecraft request") as Record<string, any>;
|
|
400
|
+
if ("errorMessage" in minecraftResponse)
|
|
401
|
+
throw new MinecraftError(minecraftResponse["errorMessage"]);
|
|
325
402
|
const minecraftToken = minecraftResponse["access_token"];
|
|
326
403
|
|
|
327
404
|
const mcProfileRequest = await fetch("https://api.minecraftservices.com/minecraft/profile", {
|
|
@@ -330,15 +407,16 @@ export class AuthClient {
|
|
|
330
407
|
},
|
|
331
408
|
method: "GET"
|
|
332
409
|
});
|
|
333
|
-
const mcProfileResponse = await mcProfileRequest
|
|
410
|
+
const mcProfileResponse = await getJson(mcProfileRequest, "minecraft profile request") as Record<string, any>;
|
|
334
411
|
if ("error" in mcProfileResponse) {
|
|
335
412
|
if (mcProfileResponse["error"] === "NOT_FOUND")
|
|
336
413
|
throw new ProfileNotFound();
|
|
337
414
|
else throw new ProfileError(mcProfileResponse["error"], mcProfileResponse["errorMessage"]);
|
|
338
415
|
}
|
|
339
416
|
|
|
417
|
+
this.accessToken = minecraftToken;
|
|
418
|
+
this.uuid = mcProfileResponse["id"];
|
|
340
419
|
return {
|
|
341
|
-
token: minecraftToken,
|
|
342
420
|
uuid: mcProfileResponse["id"],
|
|
343
421
|
name: mcProfileResponse["name"]
|
|
344
422
|
};
|
package/src/client/client.ts
CHANGED
|
@@ -1,43 +1,89 @@
|
|
|
1
|
-
import { EventEmitter } from "node:
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
2
|
import { BlockManager, BlockState } from "../world/block";
|
|
3
3
|
import { EntitiesManager } from "../world/entity";
|
|
4
|
-
import {
|
|
4
|
+
import { Input, Player } from "../physics/player";
|
|
5
5
|
import { TickLoop } from "../world/tick";
|
|
6
6
|
import { Message, TCPClient, TCPClientOption } from "./tcp";
|
|
7
7
|
import { TypedEmmiter as TypedEmitter } from "../base/event";
|
|
8
8
|
import { BlockRegistry, EntityRegistry, PacketRegistry, ProtocolVersionMapping } from "../version/registry";
|
|
9
9
|
import { BaseVec3, Vec3 } from "../physics/direction";
|
|
10
10
|
import { TextComponent } from "../base/typing";
|
|
11
|
-
import { EmittedEvent as EmittedEvent, Entity, SharedState } from "../world/state";
|
|
11
|
+
import { ClientStatus, ConnectionState, EmittedEvent as EmittedEvent, Entity, SharedState } from "../world/state";
|
|
12
12
|
import { Dispatcher } from "../packet/dispatcher";
|
|
13
13
|
import { VersionCodec } from "../version/codec";
|
|
14
14
|
import { AuthClient } from "./auth";
|
|
15
15
|
import { computeUUID } from "../base/math";
|
|
16
|
+
import { HaveSignatureButNotIndex } from "../base/error";
|
|
16
17
|
|
|
17
|
-
export type ClientOption = Omit<TCPClientOption, "protocolVersion"> & {
|
|
18
|
+
export type ClientOption = Omit<TCPClientOption, "protocolVersion"> & {
|
|
19
|
+
version: string,
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Whenever should the client verify the message against the signature, if any
|
|
23
|
+
*
|
|
24
|
+
* When set this option to true, you HAVE TO set `shouldVerifyMessageOrder` to true
|
|
25
|
+
*/
|
|
26
|
+
shouldVerifyMessageSignature?: boolean
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Should verify the message orders to keep the sync with server.
|
|
30
|
+
*/
|
|
31
|
+
shouldVerifyMessageOrder?: boolean
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
export interface ClientEvents {
|
|
35
|
+
// TCP Events
|
|
36
|
+
/** When the TCP socket connect is initted sucessfully */
|
|
20
37
|
connect: [],
|
|
38
|
+
/** When the TCP socket connection is destroyed */
|
|
21
39
|
destroy: [],
|
|
22
40
|
|
|
41
|
+
/**
|
|
42
|
+
* When the client is ready.
|
|
43
|
+
*
|
|
44
|
+
* Specifically, when the packet `Login (play)` is received
|
|
45
|
+
*/
|
|
23
46
|
ready: [],
|
|
47
|
+
/**
|
|
48
|
+
* When the client got disconnected from sevrer
|
|
49
|
+
*/
|
|
24
50
|
disconnect: [reason: string],
|
|
51
|
+
/**
|
|
52
|
+
* When the disconection come with the raw Text Component
|
|
53
|
+
*/
|
|
25
54
|
disconnectRaw: [textComponent: TextComponent],
|
|
26
55
|
|
|
56
|
+
/**
|
|
57
|
+
* When server send `Player Position Synchronization` packet
|
|
58
|
+
*
|
|
59
|
+
* Note: this event is not fired when the client is moved by holding the input
|
|
60
|
+
*/
|
|
27
61
|
playerPosition: [position: BaseVec3],
|
|
28
62
|
|
|
63
|
+
/** When the chunk is loaded */
|
|
29
64
|
loadChunk: [chunkX: number, sectionY: number, chunkZ: number],
|
|
65
|
+
/** When the chunk got deleted by the server */
|
|
30
66
|
unloadChunk: [chunkX: number, chunkZ: number],
|
|
31
67
|
|
|
68
|
+
/** When an entity is spawned or appeared in the client view distance */
|
|
32
69
|
spawnEntity: [entity: Entity],
|
|
70
|
+
/** When an entity is updated, including position, rotation, etc... */
|
|
33
71
|
updateEntity: [entityId: number],
|
|
72
|
+
/** When an entity go out of client view distance or despawned */
|
|
34
73
|
removeEntity: [entityId: number],
|
|
35
74
|
|
|
36
75
|
// Chat
|
|
76
|
+
/** When a chat message got broadcasted */
|
|
37
77
|
message: [message: Message],
|
|
78
|
+
/** When a message is failed to be verified against the signature sent with it */
|
|
79
|
+
failedMessage: [message: Message],
|
|
80
|
+
/** The system message */
|
|
38
81
|
systemMessage: [message: string],
|
|
82
|
+
/** The raw system message */
|
|
39
83
|
systemMessageRaw: [textComponent: TextComponent],
|
|
84
|
+
/** The action bar */
|
|
40
85
|
actionBar: [message: string],
|
|
86
|
+
/** The raw action bar */
|
|
41
87
|
actionBarRaw: [textComponent: TextComponent],
|
|
42
88
|
}
|
|
43
89
|
|
|
@@ -52,16 +98,18 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
52
98
|
private blocks: BlockManager;
|
|
53
99
|
private entities: EntitiesManager;
|
|
54
100
|
private player: Player;
|
|
55
|
-
private uuid: Buffer = Buffer.alloc(0);
|
|
56
101
|
|
|
57
102
|
private tcp: TCPClient;
|
|
58
103
|
private state: SharedState<IsReady>;
|
|
59
104
|
|
|
60
105
|
constructor(private options: ClientOption) {
|
|
106
|
+
if (options.shouldVerifyMessageSignature === true && options.shouldVerifyMessageOrder !== true)
|
|
107
|
+
throw new HaveSignatureButNotIndex();
|
|
108
|
+
|
|
61
109
|
super();
|
|
62
110
|
|
|
63
111
|
// For managing shared data
|
|
64
|
-
this.state = new SharedState(options
|
|
112
|
+
this.state = new SharedState(options);
|
|
65
113
|
// For handling packet
|
|
66
114
|
const dispatcher = new Dispatcher(this.state, this.emit.bind(this));
|
|
67
115
|
// For reading packet
|
|
@@ -82,7 +130,7 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
82
130
|
dispatcher.sendLoginStart();
|
|
83
131
|
};
|
|
84
132
|
dispatcher.sendPacket = this.tcp.sendPacket.bind(this.tcp);
|
|
85
|
-
dispatcher.
|
|
133
|
+
dispatcher.disconnect = this.disconnect.bind(this);
|
|
86
134
|
versionCodec.consumePacket = dispatcher.handlePacket.bind(dispatcher);
|
|
87
135
|
|
|
88
136
|
// For querying data
|
|
@@ -95,6 +143,10 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
95
143
|
this.entities,
|
|
96
144
|
this.blocks,
|
|
97
145
|
);
|
|
146
|
+
this.player.sendPlayerPos = dispatcher.sendPlayerPos;
|
|
147
|
+
this.player.sendPlayerPosRot = dispatcher.sendPlayerPosRot;
|
|
148
|
+
this.player.sendPlayerRot = dispatcher.sendPlayerRot;
|
|
149
|
+
this.player.sendPlayerStatus = dispatcher.sendPlayerStatus;
|
|
98
150
|
|
|
99
151
|
// For tick-tock tick-tock
|
|
100
152
|
this.tickLoop = new TickLoop(() => {
|
|
@@ -105,6 +157,10 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
105
157
|
.forEach((packet) => this.tcp.sendPacket(packet.id, packet.data));
|
|
106
158
|
this.player.tick();
|
|
107
159
|
});
|
|
160
|
+
|
|
161
|
+
// Forward TCP events
|
|
162
|
+
this.tcp.on("connect", () => this.emit("connect"));
|
|
163
|
+
this.tcp.on("destroy", () => this.emit("destroy"));
|
|
108
164
|
}
|
|
109
165
|
|
|
110
166
|
private emitEvent(event: EmittedEvent) {
|
|
@@ -126,14 +182,14 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
126
182
|
*/
|
|
127
183
|
public async connect() {
|
|
128
184
|
await this.loadRegistries();
|
|
129
|
-
|
|
130
|
-
if (!this.uuid.length) {
|
|
185
|
+
if (!this.state.playerUUID) {
|
|
131
186
|
if (this.options.auth) {
|
|
132
|
-
|
|
133
|
-
const { uuid } = await authClient.
|
|
134
|
-
this.
|
|
187
|
+
this.state.authClient = new AuthClient(this.options.auth);
|
|
188
|
+
const { uuid } = await this.state.authClient.getUUID();
|
|
189
|
+
this.state.playerUUID = uuid.replaceAll("-", "");
|
|
190
|
+
await this.state.startSignatureLoop();
|
|
135
191
|
} else
|
|
136
|
-
this.
|
|
192
|
+
this.state.playerUUID = computeUUID(this.options.playerName);
|
|
137
193
|
}
|
|
138
194
|
|
|
139
195
|
this.tcp.connect();
|
|
@@ -143,30 +199,40 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
143
199
|
this.player.setInitialVal();
|
|
144
200
|
this.tickLoop.start();
|
|
145
201
|
});
|
|
146
|
-
this.once("
|
|
147
|
-
this.
|
|
148
|
-
this.
|
|
202
|
+
this.tcp.once("destroy", () => {
|
|
203
|
+
if (this.state.state === ConnectionState.Disconnected) return;
|
|
204
|
+
this.disconnect("socket close");
|
|
149
205
|
});
|
|
150
206
|
}
|
|
151
207
|
/**
|
|
152
208
|
* Destroy the connection
|
|
153
209
|
*/
|
|
154
|
-
public disconnect() {
|
|
210
|
+
public disconnect(reason: string = "Disconnect", raw?: TextComponent) {
|
|
211
|
+
this.state.state = ConnectionState.Disconnected;
|
|
212
|
+
this.state.status = ClientStatus.Disconnected;
|
|
213
|
+
this.tickLoop.stop();
|
|
214
|
+
this.player.pruneInitialVal();
|
|
215
|
+
this.state.pruneStates();
|
|
216
|
+
this.emit("disconnect", reason);
|
|
217
|
+
if (raw)
|
|
218
|
+
this.emit("disconnectRaw", raw);
|
|
155
219
|
this.tcp.disconnect();
|
|
220
|
+
if (this.options.auth)
|
|
221
|
+
this.state.stopSignatureLoop();
|
|
156
222
|
}
|
|
157
223
|
|
|
158
224
|
// Inputs
|
|
159
225
|
/**
|
|
160
226
|
* Press and hold a key
|
|
161
227
|
*/
|
|
162
|
-
public hold(
|
|
163
|
-
this.player.input(
|
|
228
|
+
public hold(inout: Input) {
|
|
229
|
+
this.player.input(inout);
|
|
164
230
|
}
|
|
165
231
|
/**
|
|
166
232
|
* Release a key
|
|
167
233
|
*/
|
|
168
|
-
public release(
|
|
169
|
-
this.player.releaseInput(
|
|
234
|
+
public release(input: Input) {
|
|
235
|
+
this.player.releaseInput(input);
|
|
170
236
|
}
|
|
171
237
|
/**
|
|
172
238
|
* Release all key
|
|
@@ -185,4 +251,13 @@ export class Client<IsReady extends boolean = boolean> extends (EventEmitter as
|
|
|
185
251
|
const vec3 = Vec3.loadArgs(a, b, c);
|
|
186
252
|
return this.blocks.at(vec3);
|
|
187
253
|
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Rotate your face
|
|
257
|
+
* @param yaw In degree
|
|
258
|
+
* @param pitch In degree
|
|
259
|
+
*/
|
|
260
|
+
public lookAt(yaw: number, pitch: number) {
|
|
261
|
+
this.player.setAngle(yaw, pitch);
|
|
262
|
+
}
|
|
188
263
|
}
|
package/src/client/tcp.ts
CHANGED
|
@@ -6,10 +6,10 @@ import { TypedEmmiter } from "../base/event";
|
|
|
6
6
|
import { BinaryDecoder } from "../packet/decoder";
|
|
7
7
|
import { BinaryEncoder } from "../packet/encoder";
|
|
8
8
|
import { AuthOption } from "./auth";
|
|
9
|
-
import { ClientStatus,
|
|
9
|
+
import { ClientStatus, SharedState } from "../world/state";
|
|
10
10
|
import { EncodeResult } from "../version/codec";
|
|
11
11
|
import { TextComponent } from "../base/typing";
|
|
12
|
-
import {
|
|
12
|
+
import { NotImplemented, SockerIsNotWritable, UnexpectedValue } from "../base/error";
|
|
13
13
|
|
|
14
14
|
// Minecraft related typing
|
|
15
15
|
|
|
@@ -31,11 +31,6 @@ export interface TCPClientOption {
|
|
|
31
31
|
protocolVersion: number,
|
|
32
32
|
/** Player name */
|
|
33
33
|
playerName: string,
|
|
34
|
-
/**
|
|
35
|
-
* Set to true only if you are playing in offline server or crack server
|
|
36
|
-
*/
|
|
37
|
-
isOffline?: boolean,
|
|
38
|
-
|
|
39
34
|
/**
|
|
40
35
|
* Send empty `Known Packet` to let server send all Registry data, which may consume a lot of bandwith
|
|
41
36
|
*/
|
|
@@ -99,19 +94,16 @@ export class TCPClient extends (EventEmitter as new () => TypedEmmiter<TCPClient
|
|
|
99
94
|
) {
|
|
100
95
|
super();
|
|
101
96
|
this.socket = new Socket(socketOption);
|
|
102
|
-
|
|
103
|
-
if (!option.isOffline && !option.auth)
|
|
104
|
-
throw new MissingAuthOption();
|
|
105
97
|
}
|
|
106
98
|
|
|
107
|
-
public sendInitPacket: () => void = () => { throw new
|
|
108
|
-
public forwardPacket: (packetId: number, decoder: BinaryDecoder) => void = (
|
|
109
|
-
public parsePacket: (packetId: string | number, data: object) => EncodeResult = (
|
|
99
|
+
public sendInitPacket: () => void = () => { throw new NotImplemented(); };
|
|
100
|
+
public forwardPacket: (packetId: number, decoder: BinaryDecoder) => void = () => { throw new NotImplemented(); };
|
|
101
|
+
public parsePacket: (packetId: string | number, data: object) => EncodeResult = () => { throw new NotImplemented(); };
|
|
110
102
|
|
|
111
103
|
/**
|
|
112
104
|
* Connect to server
|
|
113
105
|
*/
|
|
114
|
-
public
|
|
106
|
+
public connect() {
|
|
115
107
|
this.state.status = ClientStatus.Connecting;
|
|
116
108
|
const connection = this.socket.connect({
|
|
117
109
|
host: this.option.host,
|
|
@@ -124,26 +116,17 @@ export class TCPClient extends (EventEmitter as new () => TypedEmmiter<TCPClient
|
|
|
124
116
|
});
|
|
125
117
|
connection.on("data", (data) => {
|
|
126
118
|
let buf = Buffer.from(data);
|
|
127
|
-
if (this.state.decipher)
|
|
119
|
+
if (this.state.decipher)
|
|
128
120
|
buf = this.state.decipher.update(buf);
|
|
129
|
-
}
|
|
130
121
|
this.emit("raw", Buffer.from(buf));
|
|
131
122
|
this.bufferPool = Buffer.concat([this.bufferPool, buf]);
|
|
132
123
|
this.handlePacket();
|
|
133
124
|
});
|
|
134
125
|
connection.once("end", () => {
|
|
135
126
|
this.emit("destroy");
|
|
136
|
-
if (this.state.status === ClientStatus.Disconnected) return;
|
|
137
|
-
this.state.status = ClientStatus.Disconnected;
|
|
138
|
-
this.state.state = ConnectionState.Disconnected;
|
|
139
|
-
// this.emit("disconnect", "socket close");
|
|
140
127
|
});
|
|
141
128
|
connection.once("close", () => {
|
|
142
129
|
this.emit("destroy");
|
|
143
|
-
if (this.state.status === ClientStatus.Disconnected) return;
|
|
144
|
-
this.state.status = ClientStatus.Disconnected;
|
|
145
|
-
this.state.state = ConnectionState.Disconnected;
|
|
146
|
-
// this.emit("disconnect", "socket close");
|
|
147
130
|
});
|
|
148
131
|
}
|
|
149
132
|
|
|
@@ -157,9 +140,7 @@ export class TCPClient extends (EventEmitter as new () => TypedEmmiter<TCPClient
|
|
|
157
140
|
*/
|
|
158
141
|
|
|
159
142
|
private handlePacket() {
|
|
160
|
-
while (
|
|
161
|
-
if (this.bufferPool.length === 0) break;
|
|
162
|
-
|
|
143
|
+
while (this.bufferPool.length > 0) {
|
|
163
144
|
let decoder = new BinaryDecoder(this.bufferPool);
|
|
164
145
|
let packetLength: number;
|
|
165
146
|
try {
|
|
@@ -184,6 +165,8 @@ export class TCPClient extends (EventEmitter as new () => TypedEmmiter<TCPClient
|
|
|
184
165
|
} else {
|
|
185
166
|
const dataBuffer = this.bufferPool.subarray(decoder.offset, expectedPacketEnd);
|
|
186
167
|
const decompressed = inflateSync(dataBuffer);
|
|
168
|
+
if (decompressed.length !== dataLength)
|
|
169
|
+
throw new UnexpectedValue("decompressed packet data", decompressed.length.toString(), dataLength.toString());
|
|
187
170
|
decoder = new BinaryDecoder(decompressed);
|
|
188
171
|
packetID = decoder.readVarInt();
|
|
189
172
|
}
|