baltica 0.1.22 → 0.1.23

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 CHANGED
@@ -91,6 +91,7 @@ const client = new Client({
91
91
  userId: "proxy-username", // Optional
92
92
  password: "proxy-password", // Optional
93
93
  },
94
+ skinFile: `./skins/skin.png`, // If you want to load a skin from a png file
94
95
  });
95
96
 
96
97
  await client.connect();
@@ -57,6 +57,8 @@ class Client extends shared_1.Emitter {
57
57
  this.packetCompressor = new shared_1.PacketCompressor(this);
58
58
  this.handleGamePackets();
59
59
  this.raknet.on("encapsulated", this.handleEncapsulated.bind(this));
60
+ // Wait for authentication to complete before sending network settings request
61
+ await this.waitForSessionReady();
60
62
  const request = new protocol_1.RequestNetworkSettingsPacket();
61
63
  request.protocol = types_1.ProtocolList[types_1.CurrentVersionConst];
62
64
  this.send(request);
@@ -1,3 +1,4 @@
1
1
  export * from "./worker";
2
2
  export * from "./types";
3
3
  export * from "./client";
4
+ export * from "./skin-loader";
@@ -17,3 +17,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
17
17
  __exportStar(require("./worker"), exports);
18
18
  __exportStar(require("./types"), exports);
19
19
  __exportStar(require("./client"), exports);
20
+ __exportStar(require("./skin-loader"), exports);
@@ -0,0 +1,38 @@
1
+ import type { SkinData } from "./types/payload";
2
+ /**
3
+ * Load a Minecraft Bedrock skin from a PNG file
4
+ * @param pngPath - Path to the PNG skin file (64x64 or 64x32)
5
+ * @returns SkinData object ready to use in client options
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import { Client, loadSkinFromPNG } from 'baltica';
10
+ *
11
+ * const client = new Client({
12
+ * address: "play.example.com",
13
+ * port: 19132,
14
+ * skinData: loadSkinFromPNG('./my-skin.png')
15
+ * });
16
+ * ```
17
+ */
18
+ export declare function loadSkinFromPNG(pngPath: string): SkinData;
19
+ /**
20
+ * Load a Minecraft Bedrock skin from raw RGBA buffer
21
+ * @param buffer - Raw RGBA pixel data buffer
22
+ * @param width - Skin width (typically 64)
23
+ * @param height - Skin height (typically 64 or 32)
24
+ * @returns SkinData object ready to use in client options
25
+ *
26
+ * @example
27
+ * ```typescript
28
+ * import { Client, loadSkinFromBuffer } from 'baltica';
29
+ *
30
+ * const rgbaBuffer = Buffer.from([...]); // Your RGBA data
31
+ * const client = new Client({
32
+ * address: "play.example.com",
33
+ * port: 19132,
34
+ * skinData: loadSkinFromBuffer(rgbaBuffer, 64, 64)
35
+ * });
36
+ * ```
37
+ */
38
+ export declare function loadSkinFromBuffer(buffer: Buffer, width: number, height: number): SkinData;
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.loadSkinFromPNG = loadSkinFromPNG;
37
+ exports.loadSkinFromBuffer = loadSkinFromBuffer;
38
+ const fs = __importStar(require("node:fs"));
39
+ const pngjs_1 = require("pngjs");
40
+ /**
41
+ * Load a Minecraft Bedrock skin from a PNG file
42
+ * @param pngPath - Path to the PNG skin file (64x64 or 64x32)
43
+ * @returns SkinData object ready to use in client options
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { Client, loadSkinFromPNG } from 'baltica';
48
+ *
49
+ * const client = new Client({
50
+ * address: "play.example.com",
51
+ * port: 19132,
52
+ * skinData: loadSkinFromPNG('./my-skin.png')
53
+ * });
54
+ * ```
55
+ */
56
+ function loadSkinFromPNG(pngPath) {
57
+ // Read and parse PNG file
58
+ const pngData = fs.readFileSync(pngPath);
59
+ const png = pngjs_1.PNG.sync.read(pngData);
60
+ // Validate dimensions
61
+ if (png.width !== 64 || (png.height !== 64 && png.height !== 32)) {
62
+ throw new Error(`Invalid skin dimensions: ${png.width}x${png.height}. Expected 64x64 or 64x32`);
63
+ }
64
+ // Convert RGBA pixel data to base64
65
+ const base64Skin = png.data.toString("base64");
66
+ // Create skin resource patch for standard humanoid model
67
+ const resourcePatch = {
68
+ geometry: {
69
+ default: "geometry.humanoid.custom",
70
+ },
71
+ };
72
+ return {
73
+ AnimatedImageData: [],
74
+ ArmSize: "wide",
75
+ CapeData: "",
76
+ CapeId: "",
77
+ CapeImageHeight: 0,
78
+ CapeImageWidth: 0,
79
+ CapeOnClassicSkin: false,
80
+ PersonaPieces: [],
81
+ PersonaSkin: false,
82
+ PieceTintColors: [],
83
+ PremiumSkin: false,
84
+ SkinAnimationData: "",
85
+ SkinColor: "#0",
86
+ SkinData: base64Skin,
87
+ SkinGeometryData: "",
88
+ SkinGeometryDataEngineVersion: "",
89
+ SkinId: `custom_skin_${Date.now()}`,
90
+ SkinImageHeight: png.height,
91
+ SkinImageWidth: png.width,
92
+ SkinResourcePatch: Buffer.from(JSON.stringify(resourcePatch)).toString("base64"),
93
+ TrustedSkin: true,
94
+ };
95
+ }
96
+ /**
97
+ * Load a Minecraft Bedrock skin from raw RGBA buffer
98
+ * @param buffer - Raw RGBA pixel data buffer
99
+ * @param width - Skin width (typically 64)
100
+ * @param height - Skin height (typically 64 or 32)
101
+ * @returns SkinData object ready to use in client options
102
+ *
103
+ * @example
104
+ * ```typescript
105
+ * import { Client, loadSkinFromBuffer } from 'baltica';
106
+ *
107
+ * const rgbaBuffer = Buffer.from([...]); // Your RGBA data
108
+ * const client = new Client({
109
+ * address: "play.example.com",
110
+ * port: 19132,
111
+ * skinData: loadSkinFromBuffer(rgbaBuffer, 64, 64)
112
+ * });
113
+ * ```
114
+ */
115
+ function loadSkinFromBuffer(buffer, width, height) {
116
+ // Validate dimensions
117
+ if (width !== 64 || (height !== 64 && height !== 32)) {
118
+ throw new Error(`Invalid skin dimensions: ${width}x${height}. Expected 64x64 or 64x32`);
119
+ }
120
+ // Validate buffer size (RGBA = 4 bytes per pixel)
121
+ const expectedSize = width * height * 4;
122
+ if (buffer.length !== expectedSize) {
123
+ throw new Error(`Invalid buffer size: ${buffer.length} bytes. Expected ${expectedSize} bytes for ${width}x${height} RGBA`);
124
+ }
125
+ // Convert to base64
126
+ const base64Skin = buffer.toString("base64");
127
+ // Create skin resource patch
128
+ const resourcePatch = {
129
+ geometry: {
130
+ default: "geometry.humanoid.custom",
131
+ },
132
+ };
133
+ return {
134
+ AnimatedImageData: [],
135
+ ArmSize: "wide",
136
+ CapeData: "",
137
+ CapeId: "",
138
+ CapeImageHeight: 0,
139
+ CapeImageWidth: 0,
140
+ CapeOnClassicSkin: false,
141
+ PersonaPieces: [],
142
+ PersonaSkin: false,
143
+ PieceTintColors: [],
144
+ PremiumSkin: false,
145
+ SkinAnimationData: "",
146
+ SkinColor: "#0",
147
+ SkinData: base64Skin,
148
+ SkinGeometryData: "",
149
+ SkinGeometryDataEngineVersion: "",
150
+ SkinId: `custom_skin_${Date.now()}`,
151
+ SkinImageHeight: height,
152
+ SkinImageWidth: width,
153
+ SkinResourcePatch: Buffer.from(JSON.stringify(resourcePatch)).toString("base64"),
154
+ TrustedSkin: true,
155
+ };
156
+ }
@@ -42,6 +42,7 @@ const uuid_1345_1 = require("uuid-1345");
42
42
  const __1 = require("../");
43
43
  const types_1 = require("../../shared/types");
44
44
  const login_data_1 = require("./login-data");
45
+ const skin_loader_1 = require("../skin-loader");
45
46
  const PUBLIC_KEY = "MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAECRXueJeTDqNRRgJi/vlRufByu/2G0i2Ebt6YMar5QX/R0DIIyrJMcUpruK4QveTfJSTp3Shlq4Gk34cD/4GUWwkv0DVuzeuB+tXija7HBxii03NHDbPAD0AKnLr2wdAp";
46
47
  const algorithm = "ES384";
47
48
  class ClientData {
@@ -52,6 +53,10 @@ class ClientData {
52
53
  this.client = client;
53
54
  this.payload = (0, __1.createDefaultPayload)(client);
54
55
  this.loginData = (0, login_data_1.prepareLoginData)();
56
+ // Load skin from file if skinFile is provided
57
+ if (client.options.skinFile && !client.options.skinData) {
58
+ client.options.skinData = (0, skin_loader_1.loadSkinFromPNG)(client.options.skinFile);
59
+ }
55
60
  }
56
61
  createLoginPacket() {
57
62
  const loginPacket = new protocol_1.LoginPacket();
@@ -68,7 +73,7 @@ class ClientData {
68
73
  return loginPacket;
69
74
  }
70
75
  async createClientChain(mojangKey, offline) {
71
- const { clientX509, ecdhKeyPair } = this.loginData;
76
+ const { clientX509, ecdhKeyPair, sessionTokenData } = this.loginData;
72
77
  let payload;
73
78
  let header;
74
79
  if (offline) {
@@ -96,6 +101,19 @@ class ClientData {
96
101
  identityPublicKey: mojangKey || PUBLIC_KEY,
97
102
  certificateAuthority: true,
98
103
  };
104
+ // Add session token data for PocketMine 1.21.100+ compatibility
105
+ if (sessionTokenData) {
106
+ payload.ipt = sessionTokenData.ipt;
107
+ payload.tid = sessionTokenData.tid;
108
+ payload.mid = sessionTokenData.mid;
109
+ payload.xid = sessionTokenData.xid;
110
+ payload.cpk = sessionTokenData.cpk;
111
+ payload.xname = this.client.profile.name;
112
+ }
113
+ // Add pfcd if available (PlayFab ID)
114
+ if (this.payload.pfcd) {
115
+ payload.pfcd = this.payload.pfcd;
116
+ }
99
117
  header = {
100
118
  alg: algorithm,
101
119
  x5u: clientX509,
@@ -33,6 +33,8 @@ export type ClientOptions = {
33
33
  tokensFolder: string;
34
34
  /** Skin Data for custom skins (By default we parse it from json)*/
35
35
  skinData: SkinData | undefined;
36
+ /** Path to PNG skin file (alternative to skinData) */
37
+ skinFile: string | undefined;
36
38
  /** LoginPacket data Customization */
37
39
  loginOptions: LoginPacketOptions;
38
40
  /** The View Distance of the client. */
@@ -18,6 +18,8 @@ exports.defaultClientOptions = {
18
18
  tokensFolder: "tokens",
19
19
  /** Default Value: undefined */
20
20
  skinData: undefined,
21
+ /** Default Value: undefined */
22
+ skinFile: undefined,
21
23
  loginOptions: {
22
24
  /** Default Value: Unknown */
23
25
  currentInputMode: protocol_1.InputMode.Unknown,
@@ -6,6 +6,13 @@ type LoginData = {
6
6
  clientX509: string;
7
7
  clientIdentityChain: string;
8
8
  clientUserChain: string;
9
+ sessionTokenData?: {
10
+ ipt: string;
11
+ tid: string;
12
+ mid: string;
13
+ xid: string;
14
+ cpk: string;
15
+ };
9
16
  };
10
17
  export declare const prepareLoginData: () => LoginData;
11
18
  export type { LoginData };
@@ -29,6 +29,7 @@ export type Payload = {
29
29
  PlatformOnlineId: string;
30
30
  PlatformType: number;
31
31
  PlayFabId: string;
32
+ pfcd?: string;
32
33
  PremiumSkin: boolean;
33
34
  SelfSignedId: string;
34
35
  ServerAddress: string;
@@ -11,6 +11,9 @@ export interface BedrockTokens {
11
11
  xuid: string;
12
12
  gamertag: string;
13
13
  userHash: string;
14
+ xstsToken: string;
15
+ playfabXstsToken: string;
16
+ playfabUserHash: string;
14
17
  }
15
18
  export interface ProxyOptions {
16
19
  host: string;
@@ -48,6 +48,7 @@ const raknet_1 = require("@sanctumterra/raknet");
48
48
  const fetch_socks_1 = require("fetch-socks");
49
49
  const undici_1 = require("undici");
50
50
  const MINECRAFT_BEDROCK_RELYING_PARTY = "https://multiplayer.minecraft.net/";
51
+ const PLAYFAB_RELYING_PARTY = "https://b980a380.minecraft.playfabapi.com/";
51
52
  const XBOX_AUTH_CLIENT_ID = "00000000441cc96b";
52
53
  function hashString(str) {
53
54
  let hash = 0;
@@ -154,12 +155,23 @@ async function authenticateWithCredentials(options) {
154
155
  saveCache(cacheFile, userToken, userHash, userTokenResp.NotAfter);
155
156
  }
156
157
  }
158
+ // Get XSTS token for Minecraft Bedrock
157
159
  const xstsResp = await exchangeTokenForXSTSToken(userToken, MINECRAFT_BEDROCK_RELYING_PARTY, proxiedFetch);
158
160
  const xuid = xstsResp.DisplayClaims.xui[0].xid || "";
161
+ // Get XSTS token for Playfab (needed for session token)
162
+ const playfabXstsResp = await exchangeTokenForXSTSToken(userToken, PLAYFAB_RELYING_PARTY, proxiedFetch);
159
163
  const chains = await getMinecraftBedrockChains(xstsResp.Token, userHash, clientPublicKey, proxiedFetch);
160
164
  const gamertag = extractGamertagFromChains(chains);
161
165
  raknet_1.Logger.info(`Authenticated as: ${gamertag} (${xuid})`);
162
- return { chains, xuid, gamertag, userHash };
166
+ return {
167
+ chains,
168
+ xuid,
169
+ gamertag,
170
+ userHash,
171
+ xstsToken: xstsResp.Token,
172
+ playfabXstsToken: playfabXstsResp.Token,
173
+ playfabUserHash: playfabXstsResp.DisplayClaims.xui[0].uhs,
174
+ };
163
175
  }
164
176
  /**
165
177
  * Get Microsoft access token using email/password via OAuth flow
@@ -114,6 +114,35 @@ async function authenticateWithEmailPassword(client) {
114
114
  uuid: generateUUID(tokens.gamertag),
115
115
  xuid: Number(tokens.xuid) || 0,
116
116
  };
117
+ // Get Playfab session ticket
118
+ const playfabData = await getPlayfabSessionTicket(tokens.playfabUserHash, tokens.playfabXstsToken);
119
+ // Get the multiplayer session token
120
+ // First get the MC services token (mcToken)
121
+ const mcToken = await getMinecraftServicesTokenFromPlayfab(playfabData.sessionTicket);
122
+ // Then use mcToken to get the multiplayer session token
123
+ const sessionToken = await getMultiplayerSessionTokenFromMcToken(mcToken, client.data.loginData.clientX509);
124
+ client.data.loginToken = sessionToken;
125
+ // Extract pfcd from session token and store it
126
+ try {
127
+ const tokenParts = sessionToken.split(".");
128
+ if (tokenParts.length >= 2) {
129
+ const payload = JSON.parse(Buffer.from(tokenParts[1], "base64").toString());
130
+ if (payload.pfcd) {
131
+ client.data.payload.pfcd = payload.pfcd;
132
+ }
133
+ // Store session token data for client chain
134
+ client.data.loginData.sessionTokenData = {
135
+ ipt: payload.ipt,
136
+ tid: payload.prop ? JSON.parse(payload.prop).tid : undefined,
137
+ mid: payload.prop ? JSON.parse(payload.prop).mid : undefined,
138
+ xid: payload.xid,
139
+ cpk: payload.cpk,
140
+ };
141
+ }
142
+ }
143
+ catch (e) {
144
+ raknet_1.Logger.warn(`Failed to extract pfcd from session token: ${e instanceof Error ? e.message : String(e)}`);
145
+ }
117
146
  const endTime = Date.now();
118
147
  raknet_1.Logger.info(`Authentication with Xbox (email/password) took ${(endTime - startTime) / 1000}s.`);
119
148
  setupClientProfile(client, profile, tokens.chains);
@@ -155,6 +184,144 @@ async function getMultiplayerSessionToken(authflow, client) {
155
184
  throw error;
156
185
  }
157
186
  }
187
+ async function getPlayfabSessionTicket(playfabUserHash, playfabXstsToken) {
188
+ try {
189
+ const response = await fetch("https://20ca2.playfabapi.com/Client/LoginWithXbox", {
190
+ method: "POST",
191
+ headers: { "Content-Type": "application/json" },
192
+ body: JSON.stringify({
193
+ CreateAccount: true,
194
+ EncryptedRequest: null,
195
+ InfoRequestParameters: {
196
+ GetCharacterInventories: false,
197
+ GetCharacterList: false,
198
+ GetPlayerProfile: true,
199
+ GetPlayerStatistics: false,
200
+ GetTitleData: false,
201
+ GetUserAccountInfo: true,
202
+ GetUserData: false,
203
+ GetUserInventory: false,
204
+ GetUserReadOnlyData: false,
205
+ GetUserVirtualCurrency: false,
206
+ PlayerStatisticNames: null,
207
+ ProfileConstraints: null,
208
+ TitleDataKeys: null,
209
+ UserDataKeys: null,
210
+ UserReadOnlyDataKeys: null,
211
+ },
212
+ PlayerSecret: null,
213
+ TitleId: "20CA2",
214
+ XboxToken: `XBL3.0 x=${playfabUserHash};${playfabXstsToken}`,
215
+ }),
216
+ });
217
+ if (!response.ok) {
218
+ const text = await response.text();
219
+ throw new Error(`Playfab login failed: ${response.status} ${response.statusText} - ${text}`);
220
+ }
221
+ const json = (await response.json());
222
+ return {
223
+ sessionTicket: json.data.SessionTicket,
224
+ playFabId: json.data.PlayFabId,
225
+ };
226
+ }
227
+ catch (error) {
228
+ raknet_1.Logger.error(`Error while getting Playfab session ticket: ${error instanceof Error ? error.message : String(error)}`);
229
+ throw error;
230
+ }
231
+ }
232
+ async function getMinecraftServicesTokenFromPlayfab(sessionTicket) {
233
+ try {
234
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/session/start", {
235
+ method: "POST",
236
+ headers: { "Content-Type": "application/json" },
237
+ body: JSON.stringify({
238
+ device: {
239
+ applicationType: "MinecraftPE",
240
+ gameVersion: "1.21.130",
241
+ id: "c1681ad3-415e-30cd-abd3-3b8f51e771d1",
242
+ memory: String(8 * (1024 * 1024 * 1024)),
243
+ platform: "Windows10",
244
+ playFabTitleId: "20CA2",
245
+ storePlatform: "uwp.store",
246
+ type: "Windows10",
247
+ },
248
+ user: {
249
+ token: sessionTicket,
250
+ tokenType: "PlayFab",
251
+ },
252
+ }),
253
+ });
254
+ if (!response.ok) {
255
+ const text = await response.text();
256
+ throw new Error(`MC services token failed: ${response.status} ${response.statusText} - ${text}`);
257
+ }
258
+ const json = (await response.json());
259
+ return json.result.authorizationHeader;
260
+ }
261
+ catch (error) {
262
+ raknet_1.Logger.error(`Error while getting MC services token: ${error instanceof Error ? error.message : String(error)}`);
263
+ throw error;
264
+ }
265
+ }
266
+ async function getMultiplayerSessionTokenFromMcToken(mcToken, publicKey) {
267
+ try {
268
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/multiplayer/session/start", {
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ Authorization: mcToken,
273
+ "Accept-Encoding": "identity",
274
+ },
275
+ body: JSON.stringify({
276
+ publicKey: publicKey,
277
+ }),
278
+ });
279
+ if (!response.ok) {
280
+ const text = await response.text();
281
+ throw new Error(`Multiplayer session start failed: ${response.status} ${response.statusText} - ${text}`);
282
+ }
283
+ const json = (await response.json());
284
+ return json.result.signedToken;
285
+ }
286
+ catch (error) {
287
+ raknet_1.Logger.error(`Error while getting Multiplayer Session Token: ${error instanceof Error ? error.message : String(error)}`);
288
+ throw error;
289
+ }
290
+ }
291
+ async function getMultiplayerSessionTokenFromXsts(sessionTicket, publicKey) {
292
+ try {
293
+ const response = await fetch("https://authorization.franchise.minecraft-services.net/api/v1.0/session/start", {
294
+ method: "POST",
295
+ headers: { "Content-Type": "application/json" },
296
+ body: JSON.stringify({
297
+ device: {
298
+ applicationType: "MinecraftPE",
299
+ gameVersion: "1.21.130",
300
+ id: "c1681ad3-415e-30cd-abd3-3b8f51e771d1",
301
+ memory: String(8 * (1024 * 1024 * 1024)),
302
+ platform: "Windows10",
303
+ playFabTitleId: "20CA2",
304
+ storePlatform: "uwp.store",
305
+ type: "Windows10",
306
+ },
307
+ user: {
308
+ token: sessionTicket,
309
+ tokenType: "PlayFab",
310
+ },
311
+ }),
312
+ });
313
+ if (!response.ok) {
314
+ const text = await response.text();
315
+ throw new Error(`Multiplayer session start failed: ${response.status} ${response.statusText} - ${text}`);
316
+ }
317
+ const json = (await response.json());
318
+ return json.result.authorizationHeader;
319
+ }
320
+ catch (error) {
321
+ raknet_1.Logger.error(`Error while getting Multiplayer Session Token: ${error instanceof Error ? error.message : String(error)}`);
322
+ throw error;
323
+ }
324
+ }
158
325
  function extractProfile(jwt) {
159
326
  if (!jwt) {
160
327
  raknet_1.Logger.error("JWT is undefined or empty");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "baltica",
3
3
  "description": "Library for Minecraft Bedrock Edition community developers.",
4
- "version": "0.1.22",
4
+ "version": "0.1.23",
5
5
  "minecraft": "1.21.130",
6
6
  "main": "dist/index.js",
7
7
  "license": "MIT",
@@ -33,7 +33,9 @@
33
33
  "devDependencies": {
34
34
  "@biomejs/biome": "1.9.4",
35
35
  "@types/node": "^22.19.7",
36
+ "@types/pngjs": "^6.0.5",
36
37
  "@types/uuid-1345": "^0.99.25",
38
+ "pngjs": "^7.0.0",
37
39
  "typescript": "^5.9.3"
38
40
  }
39
41
  }