@lpsmods/minecraft-server-mock 0.0.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 ADDED
@@ -0,0 +1,36 @@
1
+ # @lpsmods/minecraft-server-mock
2
+
3
+ Emulates the [@minecraft/server](https://learn.microsoft.com/en-us/minecraft/creator/scriptapi/minecraft/server/minecraft-server) package for testing.
4
+
5
+ ## What is this?
6
+
7
+ This package provides a [mocked](https://en.wikipedia.org/wiki/Mock_object) version of `@minecraft/server` for testing. It helps you run unit tests without needing the real Minecraft Bedrock scripting environment.
8
+
9
+ ## Using with Vitest
10
+
11
+ Install the mock package as a dev dependency:
12
+
13
+ ```sh
14
+ npm install -D @lpsmods/minecraft-server-mock
15
+ ```
16
+
17
+ Then alias `@minecraft/server` in your `vitest.config.ts`:
18
+
19
+ ```ts
20
+ import { defineConfig } from "vitest/config";
21
+
22
+ export default defineConfig({
23
+ test: {
24
+ globals: true,
25
+ },
26
+ resolve: {
27
+ alias: {
28
+ "@minecraft/server": "@lpsmods/minecraft-server-mock",
29
+ },
30
+ },
31
+ });
32
+ ```
33
+
34
+ Now your source code can keep importing from `@minecraft/server`, and when running tests, Vitest will automatically redirect those imports to this mock package.
35
+
36
+ > Not associated with or approved by Mojang Studios or Microsoft
@@ -0,0 +1,24 @@
1
+ import minecraftLinting from "eslint-plugin-minecraft-linting";
2
+ import unusedImports from "eslint-plugin-unused-imports";
3
+ import tsParser from "@typescript-eslint/parser";
4
+ import ts from "@typescript-eslint/eslint-plugin";
5
+
6
+ export default [
7
+ {
8
+ files: ["src/**/*.ts"],
9
+ languageOptions: {
10
+ parser: tsParser,
11
+ ecmaVersion: "latest",
12
+ },
13
+ plugins: {
14
+ ts,
15
+ "minecraft-linting": minecraftLinting,
16
+ "unused-imports": unusedImports,
17
+ },
18
+ rules: {
19
+ "minecraft-linting/avoid-unnecessary-command": "error",
20
+ "no-duplicate-imports": "error",
21
+ "unused-imports/no-unused-imports": "error",
22
+ },
23
+ },
24
+ ];
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@lpsmods/minecraft-server-mock",
3
+ "version": "0.0.1",
4
+ "description": "@minecraft/server mocks for testing.",
5
+ "license": "MIT",
6
+ "author": "legopitstop",
7
+ "type": "module",
8
+ "main": "src/index.ts",
9
+ "homepage": "https://github.com/lpsmods/minecraft-mock",
10
+ "funding": "https://github.com/sponsors/legopitstop",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/lpsmods/minecraft-mock"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/lpsmods/minecraft-mock/issues"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "test": "npx vitest run",
23
+ "lint": "eslint ."
24
+ },
25
+ "peerDependencies": {
26
+ "@minecraft/server": "^2.6.0"
27
+ },
28
+ "devDependencies": {
29
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
30
+ "eslint": "^10.1.0",
31
+ "eslint-plugin-minecraft-linting": "^2.0.12",
32
+ "eslint-plugin-unused-imports": "^4.4.1",
33
+ "path": "^0.12.7",
34
+ "typescript": "^5.9.3",
35
+ "vitest": "^4.1.1"
36
+ },
37
+ "dependencies": {
38
+ "@minecraft/math": "^2.4.0"
39
+ }
40
+ }
@@ -0,0 +1,8 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {} from "./index";
3
+
4
+ describe("@minecraft/server", () => {
5
+ it("should pass", () => {
6
+ expect(true).toBe(true);
7
+ });
8
+ });
package/src/index.ts ADDED
@@ -0,0 +1,489 @@
1
+ import { VECTOR3_ZERO } from "@minecraft/math";
2
+ import { Vector3, RawMessage } from "@minecraft/server";
3
+ import { vi } from "vitest";
4
+
5
+ const eventSignal = { subscribe: vi.fn((cb: () => void) => 1) };
6
+
7
+ // Internal
8
+ function idHelper(id: string): string {
9
+ const parts = id.split(":");
10
+ if (parts.length == 1) return `minecraft:${parts[0]}`;
11
+ return id;
12
+ }
13
+
14
+ export class World {
15
+ getPlayers() {
16
+ return [new Player()];
17
+ }
18
+
19
+ sendMessage(msg: RawMessage | string) {
20
+ console.log("[Mock world]", msg);
21
+ }
22
+
23
+ getDimension(dimensionId: string) {
24
+ return new Dimension(dimensionId);
25
+ }
26
+
27
+ getDay() {
28
+ return 256;
29
+ }
30
+
31
+ getTimeOfDay() {
32
+ return 1;
33
+ }
34
+
35
+ getMoonPhase() {
36
+ return MoonPhase.FullMoon;
37
+ }
38
+
39
+ getEntity() {
40
+ return new Entity();
41
+ }
42
+
43
+ readonly beforeEvents = {
44
+ effectAdd: eventSignal,
45
+ entityRemove: eventSignal,
46
+ explosion: eventSignal,
47
+ itemUse: eventSignal,
48
+ playerBreakBlock: eventSignal,
49
+ playerGameModeChange: eventSignal,
50
+ playerInteractWithBlock: eventSignal,
51
+ playerInteractWithEntity: eventSignal,
52
+ playerLeave: eventSignal,
53
+ weatherChange: eventSignal,
54
+ };
55
+ readonly afterEvents = {
56
+ blockExplode: eventSignal,
57
+ buttonPush: eventSignal,
58
+ dataDrivenEntityTrigger: eventSignal,
59
+ effectAdd: eventSignal,
60
+ entityDie: eventSignal,
61
+ entityHealthChanged: eventSignal,
62
+ entityHitBlock: eventSignal,
63
+ entityHitEntity: eventSignal,
64
+ entityHurt: eventSignal,
65
+ entityLoad: eventSignal,
66
+ entityRemove: eventSignal,
67
+ entitySpawn: eventSignal,
68
+ explosion: eventSignal,
69
+ gameRuleChange: eventSignal,
70
+ itemCompleteUse: eventSignal,
71
+ itemReleaseUse: eventSignal,
72
+ itemStartUse: eventSignal,
73
+ itemStartUseOn: eventSignal,
74
+ itemStopUse: eventSignal,
75
+ itemStopUseOn: eventSignal,
76
+ itemUse: eventSignal,
77
+ leverAction: eventSignal,
78
+ pistonActivate: eventSignal,
79
+ playerBreakBlock: eventSignal,
80
+ playerButtonInput: eventSignal,
81
+ playerDimensionChange: eventSignal,
82
+ playerEmote: eventSignal,
83
+ playerGameModeChange: eventSignal,
84
+ playerHotbarSelectedSlotChange: eventSignal,
85
+ playerInputModeChange: eventSignal,
86
+ playerInputPermissionCategoryChange: eventSignal,
87
+ playerInteractWithBlock: eventSignal,
88
+ playerInteractWithEntity: eventSignal,
89
+ playerInventoryItemChange: eventSignal,
90
+ playerJoin: eventSignal,
91
+ playerLeave: eventSignal,
92
+ playerPlaceBlock: eventSignal,
93
+ playerSpawn: eventSignal,
94
+ pressurePlatePop: eventSignal,
95
+ pressurePlatePush: eventSignal,
96
+ projectileHitBlock: eventSignal,
97
+ projectileHitEntity: eventSignal,
98
+ targetBlockHit: eventSignal,
99
+ tripWireTrip: eventSignal,
100
+ weatherChange: eventSignal,
101
+ worldLoad: eventSignal,
102
+ };
103
+ }
104
+
105
+ export const world = new World();
106
+
107
+ export enum ItemLockMode {
108
+ inventory = "inventory",
109
+ none = "none",
110
+ slot = "slot",
111
+ }
112
+
113
+ export enum Direction {
114
+ North = "North",
115
+ East = "East",
116
+ South = "South",
117
+ West = "West",
118
+ Up = "Up",
119
+ Down = "Down",
120
+ }
121
+
122
+ export enum MemoryTier {
123
+ SuperLow = 0,
124
+ Low = 1,
125
+ Mid = 2,
126
+ High = 3,
127
+ SuperHigh = 4,
128
+ }
129
+
130
+ export enum MoonPhase {
131
+ FullMoon = 0,
132
+ WaningGibbous = 1,
133
+ FirstQuarter = 2,
134
+ WaningCrescent = 3,
135
+ NewMoon = 4,
136
+ WaxingCrescent = 5,
137
+ LastQuarter = 6,
138
+ WaxingGibbous = 7,
139
+ }
140
+
141
+ export const system = {
142
+ run: (cb: Function) => cb(),
143
+ runTimeout: (cb: Function, _ticks: number) => 1, // Don't call callback in test
144
+ runInterval: (cb: Function, _ticks: number) => 1, // Don't call callback in test
145
+ clearRun: vi.fn(),
146
+ clearTimeout: vi.fn(),
147
+ runJob: vi.fn(),
148
+ currentTick: 0,
149
+ afterEvents: {
150
+ scriptEventReceive: {
151
+ subscribe: vi.fn(),
152
+ unsubscribe: vi.fn(),
153
+ },
154
+ },
155
+ beforeEvents: {
156
+ scriptEventReceive: {
157
+ subscribe: vi.fn(),
158
+ unsubscribe: vi.fn(),
159
+ },
160
+ },
161
+ serverSystemInfo: {
162
+ memoryTier: MemoryTier.Mid,
163
+ },
164
+ };
165
+
166
+ export class Entity {
167
+ readonly dimension = new Dimension();
168
+ readonly id = "12345";
169
+ readonly isClimbing = false;
170
+ readonly isFalling = false;
171
+ readonly isInWater = false;
172
+ readonly isOnGround = false;
173
+ readonly isSleeping = false;
174
+ readonly isSneaking = false;
175
+ readonly isSprinting = false;
176
+ readonly isSwimming = false;
177
+ readonly isValid = true;
178
+ readonly localizationKey = "entity.creeper.name";
179
+ readonly location = VECTOR3_ZERO;
180
+ readonly typeId = "minecraft:creeper";
181
+ nameTag = "";
182
+
183
+ addEffect = vi.fn();
184
+ addTag = vi.fn();
185
+ applyDamage = vi.fn();
186
+ applyImpulse = vi.fn();
187
+ applyKnockback = vi.fn();
188
+ clearDynamicProperties = vi.fn();
189
+ clearVelocity = vi.fn();
190
+ extinguishFire = vi.fn();
191
+ getBlockFromViewDirection = vi.fn();
192
+ getComponent = vi.fn((componentId) => {});
193
+ getComponents = vi.fn();
194
+ getDynamicProperty = vi.fn();
195
+ getDynamicPropertyIds = vi.fn();
196
+ getDynamicPropertyTotalByteCount = vi.fn();
197
+ getEffect = vi.fn();
198
+ getEffects = vi.fn();
199
+ getEntitiesFromViewDirection = vi.fn();
200
+ getHeadLocation = vi.fn();
201
+ getProperty = vi.fn();
202
+ getRotation = vi.fn(() => {
203
+ return { x: 0, y: 0 };
204
+ });
205
+ getTags = vi.fn();
206
+ getVelocity = vi.fn();
207
+ getViewDirection = vi.fn();
208
+ hasComponent = vi.fn();
209
+ hasTag = vi.fn();
210
+ kill = vi.fn();
211
+ lookAt = vi.fn();
212
+ matches = vi.fn();
213
+ playAnimation = vi.fn();
214
+ remove = vi.fn();
215
+ removeEffect = vi.fn();
216
+ removeTag = vi.fn();
217
+ resetProperty = vi.fn();
218
+ runCommand = vi.fn();
219
+ setDynamicProperties = vi.fn();
220
+ setDynamicProperty = vi.fn();
221
+ setOnFire = vi.fn();
222
+ setProperty = vi.fn();
223
+ setRotation = vi.fn();
224
+ teleport = vi.fn();
225
+ triggerEvent = vi.fn();
226
+ tryTeleport = vi.fn();
227
+ }
228
+
229
+ export class BlockVolumeBase {}
230
+
231
+ export class BlockVolume extends BlockVolumeBase {
232
+ constructor(
233
+ public from: Vector3,
234
+ public to: Vector3,
235
+ ) {
236
+ super();
237
+ }
238
+
239
+ isInside(v: Vector3): boolean {
240
+ return (
241
+ v.x >= Math.min(this.from.x, this.to.x) &&
242
+ v.x <= Math.max(this.from.x, this.to.x) &&
243
+ v.y >= Math.min(this.from.y, this.to.y) &&
244
+ v.y <= Math.max(this.from.y, this.to.y) &&
245
+ v.z >= Math.min(this.from.z, this.to.z) &&
246
+ v.z <= Math.max(this.from.z, this.to.z)
247
+ );
248
+ }
249
+ }
250
+
251
+ export class Player extends Entity {
252
+ readonly name = "Steve";
253
+ }
254
+
255
+ export class Dimension {
256
+ heightRange = { max: 256, min: -64 };
257
+ readonly id: string;
258
+ readonly localizationKey = "dimension.dimensionName0";
259
+
260
+ constructor(typeId?: string) {
261
+ this.id = typeId ?? "minecraft:overworld";
262
+ }
263
+
264
+ containsBlock() {}
265
+ createExplosion() {}
266
+ fillBlocks() {}
267
+ getBiome(location: Vector3) {
268
+ return new BiomeType("minecraft:plains");
269
+ }
270
+ getBlock(location: Vector3) {
271
+ return new Block();
272
+ }
273
+ getBlockAbove() {}
274
+ getBlockBelow() {}
275
+ getBlockFromRay() {}
276
+ getBlocks() {}
277
+ getEntities() {
278
+ return [new Entity()];
279
+ }
280
+ getEntitiesAtBlockLocation() {}
281
+ getEntitiesFromRay() {}
282
+ getLightLevel() {}
283
+ getPlayers() {}
284
+ getSkyLightLevel() {}
285
+ getTopmostBlock() {}
286
+ isChunkLoaded() {}
287
+ placeFeature() {}
288
+ placeFeatureRule() {}
289
+ playSound() {}
290
+ runCommand() {}
291
+ setBlockPermutation() {}
292
+ setBlockType() {}
293
+ setWeather() {}
294
+ spawnEntity() {}
295
+ spawnItem() {}
296
+ spawnParticle() {}
297
+ }
298
+
299
+ export const ItemStack = vi.fn(
300
+ class {
301
+ constructor(public typeId: string) {}
302
+ readonly isStackable = false;
303
+ readonly maxAmount = 64;
304
+ readonly weight = 1;
305
+ amount = 0;
306
+ keepOnDeath = false;
307
+ localizationKey = "item.paper";
308
+ lockMode = ItemLockMode.none;
309
+ nameTag = "Custom Name";
310
+ type = new ItemType("minecraft:paper");
311
+
312
+ clearDynamicProperties = vi.fn();
313
+ clone = vi.fn();
314
+ getCanDestroy = vi.fn();
315
+ getCanPlaceOn = vi.fn();
316
+ getComponent = vi.fn();
317
+ getComponents = vi.fn();
318
+ getDynamicProperty = vi.fn();
319
+ getDynamicPropertyIds = vi.fn();
320
+ getDynamicPropertyTotalByteCount = vi.fn();
321
+ getLore = vi.fn();
322
+ getRawLore = vi.fn();
323
+ getTags = vi.fn(() => []);
324
+ hasComponent = vi.fn();
325
+ hasTag = vi.fn((tag) => true);
326
+ isStackableWith = vi.fn();
327
+ matches = vi.fn((name) => true);
328
+ setCanDestroy = vi.fn();
329
+ setCanPlaceOn = vi.fn();
330
+ setDynamicProperties = vi.fn();
331
+ setDynamicProperty = vi.fn();
332
+ setLore = vi.fn();
333
+ },
334
+ );
335
+
336
+ export const BlockPermutation = vi.fn(class {});
337
+
338
+ export const Block = vi.fn(
339
+ class {
340
+ readonly typeId = "minecraft:stone";
341
+ readonly type = new BlockType("minecraft:stone");
342
+ readonly x = 0;
343
+ readonly y = 0;
344
+ readonly z = 0;
345
+ readonly dimension = new Dimension();
346
+ readonly isAir = false;
347
+ readonly isLiquid = false;
348
+ readonly isValid = true;
349
+ readonly isWaterlogged = false;
350
+ readonly localizationKey = "block.stone.name";
351
+ readonly location = VECTOR3_ZERO;
352
+ readonly permutation = new BlockPermutation();
353
+
354
+ above = vi.fn();
355
+ below = vi.fn();
356
+ bottomCenter = vi.fn();
357
+ canBeDestroyedByLiquidSpread = vi.fn();
358
+ canContainLiquid = vi.fn();
359
+ center = vi.fn();
360
+ east = vi.fn();
361
+ getComponent = vi.fn();
362
+ getItemStack = vi.fn();
363
+ getLightLevel = vi.fn();
364
+ getRedstonePower = vi.fn();
365
+ getSkyLightLevel = vi.fn();
366
+ getTags = vi.fn();
367
+ hasTag = vi.fn();
368
+ isLiquidBlocking = vi.fn();
369
+ liquidCanFlowFromDirection = vi.fn();
370
+ liquidSpreadCausesSpawn = vi.fn();
371
+ matches = vi.fn((cb: (name: string, states?: {}) => void) => true);
372
+ north = vi.fn();
373
+ offset = vi.fn();
374
+ setPermutation = vi.fn();
375
+ setType = vi.fn();
376
+ setWaterlogged = vi.fn();
377
+ south = vi.fn();
378
+ west = vi.fn();
379
+ },
380
+ );
381
+
382
+ export class BiomeType {
383
+ readonly id: string;
384
+ constructor(id: string) {
385
+ this.id = id;
386
+ }
387
+ }
388
+
389
+ export const BiomeTypes = {
390
+ getAll: vi.fn(() => [new BiomeType("minecraft:plains")]),
391
+ get: vi.fn((id: string) =>
392
+ BiomeTypes.getAll().find((biome) => biome.id === idHelper(id)),
393
+ ),
394
+ };
395
+
396
+ export class BlockType {
397
+ readonly id: string;
398
+ constructor(id: string) {
399
+ this.id = id;
400
+ }
401
+ }
402
+
403
+ export const BlockTypes = {
404
+ getAll: vi.fn(() => [new BlockType("minecraft:air")]),
405
+ get: vi.fn((id: string) =>
406
+ BlockTypes.getAll().find((block) => block.id === idHelper(id)),
407
+ ),
408
+ };
409
+
410
+ export class EffectType {
411
+ private id: string;
412
+
413
+ constructor(id: string) {
414
+ this.id = id;
415
+ }
416
+
417
+ getName(): string {
418
+ return this.id;
419
+ }
420
+ }
421
+
422
+ export const EffectTypes = {
423
+ getAll: vi.fn(() => [new EffectType("minecraft:haste")]),
424
+ get: vi.fn((id: string) =>
425
+ EffectTypes.getAll().find((effect) => effect.getName() === idHelper(id)),
426
+ ),
427
+ };
428
+
429
+ export class EnchantmentType {
430
+ readonly id: string;
431
+ constructor(id: string) {
432
+ this.id = id;
433
+ }
434
+ }
435
+
436
+ export const EnchantmentTypes = {
437
+ getAll: vi.fn(() => [new EnchantmentType("minecraft:mending")]),
438
+ get: vi.fn((id: string) =>
439
+ EnchantmentTypes.getAll().find((enchant) => enchant.id === idHelper(id)),
440
+ ),
441
+ };
442
+
443
+ export class EntityType {
444
+ readonly id: string;
445
+ constructor(id: string) {
446
+ this.id = id;
447
+ }
448
+ }
449
+
450
+ export const EntityTypes = {
451
+ getAll: vi.fn(() => [new EntityType("minecraft:creeper")]),
452
+ get: vi.fn((id: string) =>
453
+ EntityTypes.getAll().find((entity) => entity.id === idHelper(id)),
454
+ ),
455
+ };
456
+
457
+ export class ItemType {
458
+ readonly id: string;
459
+ constructor(id: string) {
460
+ this.id = id;
461
+ }
462
+ }
463
+
464
+ export const ItemTypes = {
465
+ getAll: vi.fn(() => [new ItemType("minecraft:paper")]),
466
+ get: vi.fn((id: string) =>
467
+ ItemTypes.getAll().find((item) => item.id === idHelper(id)),
468
+ ),
469
+ };
470
+
471
+ export class DimensionType {
472
+ readonly typeId: string;
473
+ constructor(typeId: string) {
474
+ this.typeId = typeId;
475
+ }
476
+ }
477
+
478
+ export const DimensionTypes = {
479
+ getAll: vi.fn(() => [
480
+ new DimensionType("minecraft:overworld"),
481
+ new DimensionType("minecraft:nether"),
482
+ new DimensionType("minecraft:the_end"),
483
+ ]),
484
+ get: vi.fn((id: string) =>
485
+ DimensionTypes.getAll().find((dimension) => dimension.typeId === id),
486
+ ),
487
+ };
488
+
489
+ export class InvalidEntityError {}
@@ -0,0 +1,18 @@
1
+ import path from "path";
2
+ import { defineConfig } from "vitest/config";
3
+
4
+ export default defineConfig({
5
+ test: {
6
+ environment: "node",
7
+ server: {
8
+ deps: {
9
+ inline: ["@minecraft/math"],
10
+ },
11
+ },
12
+ },
13
+ resolve: {
14
+ alias: {
15
+ "@minecraft/server": path.resolve(__dirname, "src/index.ts"),
16
+ },
17
+ },
18
+ });