@vaitosoi/minejs 0.3.1 → 0.4.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.
Files changed (78) hide show
  1. package/README.md +1 -1
  2. package/assets/minecraft/26.2/packets.json +27 -10
  3. package/dist/base/buffer.d.ts +2 -0
  4. package/dist/base/buffer.d.ts.map +1 -0
  5. package/dist/base/buffer.js +12 -0
  6. package/dist/base/buffer.js.map +1 -0
  7. package/dist/base/error.d.ts +33 -6
  8. package/dist/base/error.d.ts.map +1 -1
  9. package/dist/base/error.js +74 -11
  10. package/dist/base/error.js.map +1 -1
  11. package/dist/base/http.d.ts +2 -0
  12. package/dist/base/http.d.ts.map +1 -0
  13. package/dist/base/http.js +14 -0
  14. package/dist/base/http.js.map +1 -0
  15. package/dist/base/math.d.ts +22 -1
  16. package/dist/base/math.d.ts.map +1 -1
  17. package/dist/base/math.js +49 -2
  18. package/dist/base/math.js.map +1 -1
  19. package/dist/client/auth.d.ts +41 -7
  20. package/dist/client/auth.d.ts.map +1 -1
  21. package/dist/client/auth.js +96 -39
  22. package/dist/client/auth.js.map +1 -1
  23. package/dist/client/client.d.ts +50 -5
  24. package/dist/client/client.d.ts.map +1 -1
  25. package/dist/client/client.js +47 -18
  26. package/dist/client/client.js.map +1 -1
  27. package/dist/client/tcp.d.ts +1 -5
  28. package/dist/client/tcp.d.ts.map +1 -1
  29. package/dist/client/tcp.js +8 -21
  30. package/dist/client/tcp.js.map +1 -1
  31. package/dist/message/link.d.ts +10 -0
  32. package/dist/message/link.d.ts.map +1 -0
  33. package/dist/message/link.js +22 -0
  34. package/dist/message/link.js.map +1 -0
  35. package/dist/message/sigCache.d.ts +13 -0
  36. package/dist/message/sigCache.d.ts.map +1 -0
  37. package/dist/message/sigCache.js +26 -0
  38. package/dist/message/sigCache.js.map +1 -0
  39. package/dist/packet/decoder.d.ts +13 -0
  40. package/dist/packet/decoder.d.ts.map +1 -1
  41. package/dist/packet/decoder.js +21 -0
  42. package/dist/packet/decoder.js.map +1 -1
  43. package/dist/packet/dispatcher.d.ts +8 -1
  44. package/dist/packet/dispatcher.d.ts.map +1 -1
  45. package/dist/packet/dispatcher.js +142 -30
  46. package/dist/packet/dispatcher.js.map +1 -1
  47. package/dist/physics/player.d.ts +17 -23
  48. package/dist/physics/player.d.ts.map +1 -1
  49. package/dist/physics/player.js +59 -61
  50. package/dist/physics/player.js.map +1 -1
  51. package/dist/version/codec.d.ts.map +1 -1
  52. package/dist/version/codec.js +28 -7
  53. package/dist/version/codec.js.map +1 -1
  54. package/dist/version/registry.d.ts +1 -1
  55. package/dist/version/registry.d.ts.map +1 -1
  56. package/dist/version/registry.js +2 -7
  57. package/dist/version/registry.js.map +1 -1
  58. package/dist/world/state.d.ts +26 -2
  59. package/dist/world/state.d.ts.map +1 -1
  60. package/dist/world/state.js +58 -3
  61. package/dist/world/state.js.map +1 -1
  62. package/package.json +1 -1
  63. package/src/base/buffer.ts +12 -0
  64. package/src/base/error.ts +63 -10
  65. package/src/base/http.ts +10 -0
  66. package/src/base/math.ts +55 -3
  67. package/src/client/auth.ts +130 -52
  68. package/src/client/client.ts +96 -21
  69. package/src/client/tcp.ts +10 -27
  70. package/src/message/link.ts +17 -0
  71. package/src/message/sigCache.ts +25 -0
  72. package/src/packet/decoder.ts +21 -0
  73. package/src/packet/dispatcher.ts +169 -35
  74. package/src/physics/player.ts +83 -58
  75. package/src/version/codec.ts +32 -11
  76. package/src/version/registry.ts +2 -7
  77. package/src/world/state.ts +72 -4
  78. package/test/client/index.ts +27 -20
@@ -1,21 +1,37 @@
1
1
  import http from "node:http";
2
- import cryto from "node:crypto";
3
- import net from "node:net";
2
+ import crypto from "node:crypto";
4
3
  import { exec } from "node:child_process";
5
- import { AuthDenied, AuthError, AuthTokenExpired, CantGetMsAccessToken, ProfileError, ProfileNotFound, XboxError } from "../base/error";
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
- export interface AuthOption {
8
+ interface Loopback {
8
9
  /**
9
- * `"loopback"` - open a browser to login
10
- * `"device_code"` - provide a device code to login
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
- method: "loopback" | "device_code",
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 = cryto.randomBytes(32).toString("hex");
127
- const hashed = cryto.hash("sha256", codeVerifier, "base64url");
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
- public async auth() {
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 freePort = await getFreePort();
141
- const expectState = cryto.randomBytes(8).toString("hex");
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://127.0.0.1:${freePort}`);
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 (this.option.openBrowser === true)
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.end(html);
160
- server.close();
161
- return resolve(result);
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://127.0.0.1:${freePort}`);
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(freePort);
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("redirect_uri", `http://127.0.0.1:${freePort}`);
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.json() as Record<string, any>;
279
+ const msTokenResponse = await getJson(msTokenRequest, "microsoft token request") as Record<string, any>;
206
280
  if ("error" in msTokenResponse)
207
- throw new AuthError(msTokenResponse["error"]);
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/{tenant}/oauth2/v2.0/token");
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 AuthError(msTokenResponse["error"]);
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.json() as Record<string, any>;
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.json() as Record<string, any>;
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.json() as Record<string, any>;
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.json() as Record<string, any>;
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
  };
@@ -1,43 +1,89 @@
1
- import { EventEmitter } from "node:stream";
1
+ import { EventEmitter } from "node:events";
2
2
  import { BlockManager, BlockState } from "../world/block";
3
3
  import { EntitiesManager } from "../world/entity";
4
- import { MoveDirection, Player } from "../physics/player";
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"> & { version: string }
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, () => this.uuid);
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.tcpDisconnect = this.tcp.disconnect.bind(this.tcp);
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
- const authClient = new AuthClient(this.options.auth);
133
- const { uuid } = await authClient.auth();
134
- this.uuid = Buffer.from(uuid);
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.uuid = computeUUID(this.options.playerName);
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("disconnect", () => {
147
- this.tickLoop.stop();
148
- this.player.pruneInitialVal();
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(direction: MoveDirection) {
163
- this.player.input(direction);
228
+ public hold(inout: Input) {
229
+ this.player.input(inout);
164
230
  }
165
231
  /**
166
232
  * Release a key
167
233
  */
168
- public release(direction: MoveDirection) {
169
- this.player.releaseInput(direction);
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, ConnectionState, SharedState } from "../world/state";
9
+ import { ClientStatus, SharedState } from "../world/state";
10
10
  import { EncodeResult } from "../version/codec";
11
11
  import { TextComponent } from "../base/typing";
12
- import { MissingAuthOption, SockerIsNotWritable } from "../base/error";
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 Error("method not implemented"); };
108
- public forwardPacket: (packetId: number, decoder: BinaryDecoder) => void = (...args) => { throw new Error("method not implemented"); };
109
- public parsePacket: (packetId: string | number, data: object) => EncodeResult = (...args) => { throw new Error("method not implemented"); };
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 async connect() {
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 (true) {
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
  }