@pilaf/backends 1.2.0 → 1.2.2

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
@@ -937,6 +937,208 @@ describe('Elemental Dragon Tests', () => {
937
937
 
938
938
  ---
939
939
 
940
+ ## 🎯 Using QueryHelper with createTestContext
941
+
942
+ When using `@pilaf/framework`'s `createTestContext()` helper, you can access QueryHelper through either the separate RCON backend or via the MineflayerBackend:
943
+
944
+ ```javascript
945
+ const { createTestContext, cleanupTestContext } = require('@pilaf/framework');
946
+
947
+ describe('Plugin Tests with QueryHelper', () => {
948
+ let context;
949
+
950
+ beforeAll(async () => {
951
+ context = await createTestContext({
952
+ username: 'TestPlayer',
953
+ rconPassword: 'dragon123'
954
+ });
955
+ });
956
+
957
+ afterAll(async () => {
958
+ await cleanupTestContext(context);
959
+ });
960
+
961
+ it('should verify player position using QueryHelper', async () => {
962
+ // Get player info BEFORE using RCON backend directly
963
+ const beforeInfo = await context.rcon.send('data get entity TestPlayer Pos');
964
+
965
+ // Execute ability
966
+ context.bot.chat('/myplugin teleport 100 64 100');
967
+ await new Promise(resolve => setTimeout(resolve, 1000));
968
+
969
+ // Get player info AFTER
970
+ const afterInfo = await context.rcon.send('data get entity TestPlayer Pos');
971
+
972
+ // Or use MineflayerBackend's QueryHelper (if available)
973
+ // const info = await context.backend.getPlayerInfo('TestPlayer');
974
+ // expect(info.position.x).toBeCloseTo(100);
975
+ });
976
+
977
+ it('should verify server TPS', async () => {
978
+ // Use QueryHelper methods via backend
979
+ const { tps } = await context.backend.getTPS();
980
+ expect(tps.tps).toBeGreaterThan(15);
981
+ });
982
+
983
+ it('should list online players', async () => {
984
+ const players = await context.backend.listPlayers();
985
+ expect(players.players).toContain('TestPlayer');
986
+ });
987
+ });
988
+ ```
989
+
990
+ ### Why Both Backends?
991
+
992
+ - **context.rcon** - For raw RCON commands that return responses (e.g., `/data get`)
993
+ - **context.backend** - For QueryHelper methods and bot control
994
+ - **context.bot** - For bot player actions (chat, movement)
995
+
996
+ This dual-backend approach is necessary because `MineflayerBackend.sendCommand()` sends via bot chat and returns empty responses.
997
+
998
+ ---
999
+
1000
+ ## ⚙️ Jest Configuration Recommendations
1001
+
1002
+ ### maxWorkers: 1 for Bot Player Tests
1003
+
1004
+ When testing with multiple bot players, set `maxWorkers: 1` to avoid connection throttling:
1005
+
1006
+ ```javascript
1007
+ // jest.config.js
1008
+ module.exports = {
1009
+ testMatch: ['**/*.pilaf.test.js'],
1010
+ testTimeout: 300000,
1011
+ maxWorkers: 1, // ← Important: Prevents connection throttling
1012
+ reporters: ['default']
1013
+ };
1014
+ ```
1015
+
1016
+ **Why?** Each Jest worker creates separate bot connections. Multiple workers connecting simultaneously can trigger server connection throttling or race conditions.
1017
+
1018
+ ### testTimeout: 300000
1019
+
1020
+ Server operations (bot spawning, command execution) are slower than unit tests. Set a higher timeout:
1021
+
1022
+ ```javascript
1023
+ testTimeout: 300000, // 5 minutes
1024
+ ```
1025
+
1026
+ ### Module Resolution for ES Modules
1027
+
1028
+ If using ES modules (`"type": "module"`), configure Jest accordingly:
1029
+
1030
+ ```javascript
1031
+ module.exports = {
1032
+ extension: ['.js', '.cjs'],
1033
+ transform: {},
1034
+ testTimeout: 300000,
1035
+ maxWorkers: 1
1036
+ };
1037
+ ```
1038
+
1039
+ ---
1040
+
1041
+ ## 📊 QueryHelper vs Raw RCON Comparison
1042
+
1043
+ ### Before: Raw RCON with Manual Parsing
1044
+
1045
+ ```javascript
1046
+ // ❌ Verbose and error-prone
1047
+ const result = await context.rcon.send('data get entity TestPlayer Pos');
1048
+
1049
+ // Manual regex parsing
1050
+ const match = result.raw.match(/Pos.*?\[.*?d/);
1051
+ if (!match) {
1052
+ throw new Error('Could not parse position');
1053
+ }
1054
+
1055
+ const coords = match[0].split(', ').map(s => {
1056
+ const num = s.match(/-?\d+\.\d+/);
1057
+ return num ? parseFloat(num[0]) : 0;
1058
+ });
1059
+
1060
+ const x = coords[0];
1061
+ const y = coords[1];
1062
+ const z = coords[2];
1063
+
1064
+ expect(x).toBeCloseTo(100);
1065
+ ```
1066
+
1067
+ ### After: QueryHelper (Clean & Type-Safe)
1068
+
1069
+ ```javascript
1070
+ // ✅ Clean and structured
1071
+ const info = await context.backend.getPlayerInfo('TestPlayer');
1072
+
1073
+ expect(info.position.x).toBeCloseTo(100);
1074
+ expect(info.position.y).toBeDefined();
1075
+ expect(info.health).toBeGreaterThan(0);
1076
+ ```
1077
+
1078
+ ### Benefits Summary
1079
+
1080
+ | Feature | Raw RCON | QueryHelper |
1081
+ |---------|----------|-------------|
1082
+ | **Code length** | 20+ lines | 1-2 lines |
1083
+ | **Error handling** | Manual regex | Built-in parsing |
1084
+ | **Type safety** | Strings | Structured objects |
1085
+ | **Maintainability** | Brittle (breaks on format changes) | Resilient |
1086
+
1087
+ ---
1088
+
1089
+ ## 🤖 Bot Player Limitations
1090
+
1091
+ ### Velocity-Based Abilities Don't Work on Bot Players
1092
+
1093
+ Bot players (Mineflayer) do not respond to server velocity the same way as real players:
1094
+
1095
+ ```javascript
1096
+ // ❌ This won't work as expected with bot players
1097
+ context.bot.chat('/agile 1'); // Dash ability that applies velocity
1098
+
1099
+ await new Promise(resolve => setTimeout(resolve, 1000));
1100
+
1101
+ // Bot's client-side position won't reflect the server-side movement
1102
+ const botPos = context.bot.entity.position;
1103
+ console.log(botPos); // Position unchanged (bot doesn't move)
1104
+ ```
1105
+
1106
+ **Workaround**: Test ability activation instead of actual movement:
1107
+
1108
+ ```javascript
1109
+ // ✅ Test that ability activates (not actual movement)
1110
+ context.bot.chat('/agile 1');
1111
+ await new Promise(resolve => setTimeout(resolve, 500));
1112
+
1113
+ // Verify cooldown was applied (ability was used)
1114
+ const quickResult = await context.rcon.send('execute as TestPlayer run agile 1');
1115
+ expect(quickResult.raw).toContain('cooldown'); // Ability is on cooldown
1116
+ ```
1117
+
1118
+ ### Entity Position Tracking
1119
+
1120
+ Use server-side queries instead of bot position:
1121
+
1122
+ ```javascript
1123
+ // ❌ Don't use bot position for velocity-based abilities
1124
+ const botPos = context.bot.entity.position; // Not updated by server velocity
1125
+
1126
+ // ✅ Use RCON to get server-side entity position
1127
+ const result = await context.rcon.send('data get entity Pig Pos');
1128
+ // Parse the result to verify entity moved
1129
+ ```
1130
+
1131
+ ### Chat and Commands Work Fine
1132
+
1133
+ Bot players correctly handle:
1134
+ - ✅ Chat messages
1135
+ - ✅ Command execution (`/command`)
1136
+ - ✅ Inventory manipulation
1137
+ - ✅ Block interaction
1138
+ - ✅ Entity detection (`bot.entities`)
1139
+
1140
+ ---
1141
+
940
1142
  ## 🔧 Core Components
941
1143
 
942
1144
  ### ConnectionState
@@ -1136,6 +1338,21 @@ const {
1136
1338
 
1137
1339
  ## 🐛 Troubleshooting
1138
1340
 
1341
+ ### Issue: Cannot find module './lib/features'
1342
+
1343
+ **Cause**: prismarine-physics package has a bug where it imports `./lib/features` but the file is actually `./lib/features.json`.
1344
+
1345
+ **Solution**: This is automatically fixed by the postinstall script in `@pilaf/backends`. If you're not using the postinstall script, you can create a symlink manually:
1346
+
1347
+ ```bash
1348
+ # Navigate to node_modules/prismarine-physics/lib
1349
+ cd node_modules/prismarine-physics/lib
1350
+ # Create symlink
1351
+ ln -s features.json features
1352
+ ```
1353
+
1354
+ **Note**: This fix runs automatically when `@pilaf/backends` is installed via npm/pnpm.
1355
+
1139
1356
  ### Issue: Events not being captured
1140
1357
 
1141
1358
  **Cause**: Not observing before triggering events
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pilaf/backends",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "main": "lib/index.js",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,6 +9,7 @@
9
9
  "files": [
10
10
  "lib/*.js",
11
11
  "lib/**/*.js",
12
+ "scripts/**/*.cjs",
12
13
  "!lib/**/*.spec.js",
13
14
  "!lib/**/*.test.js",
14
15
  "README.md",
@@ -21,5 +22,8 @@
21
22
  "dockerode": "^4.0.0",
22
23
  "mineflayer": "^4.20.1",
23
24
  "rcon-client": "^4.2.5"
25
+ },
26
+ "scripts": {
27
+ "postinstall": "node scripts/fix-prismarine-physics.cjs"
24
28
  }
25
- }
29
+ }
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Fix prismarine-physics dependency issue
5
+ *
6
+ * The prismarine-physics package imports './lib/features' but the file
7
+ * is actually './lib/features.json'. This script creates a symlink to fix the issue.
8
+ *
9
+ * This script supports both npm and pnpm package managers.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ function fixPrismarinePhysics() {
16
+ let fixedCount = 0;
17
+
18
+ // Handle pnpm-style node_modules (.pnpm directory)
19
+ const pnpmPath = path.join(__dirname, '..', '..', '.pnpm');
20
+
21
+ if (fs.existsSync(pnpmPath)) {
22
+ const findAndFix = (dir) => {
23
+ try {
24
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
25
+
26
+ for (const entry of entries) {
27
+ if (!entry.isDirectory()) continue;
28
+
29
+ const fullPath = path.join(dir, entry.name);
30
+
31
+ if (entry.name.startsWith('prismarine-physics@')) {
32
+ const libPath = path.join(fullPath, 'node_modules', 'prismarine-physics', 'lib');
33
+
34
+ if (fs.existsSync(libPath)) {
35
+ const featuresJson = path.join(libPath, 'features.json');
36
+ const featuresLink = path.join(libPath, 'features');
37
+
38
+ if (fs.existsSync(featuresJson) && !fs.existsSync(featuresLink)) {
39
+ try {
40
+ fs.symlinkSync('features.json', featuresLink);
41
+ console.log(`✓ Fixed prismarine-physics: ${featuresLink}`);
42
+ fixedCount++;
43
+ } catch (err) {
44
+ // On Windows, try junction instead of symlink
45
+ if (process.platform === 'win32') {
46
+ try {
47
+ fs.symlinkSync(path.join(libPath, 'features.json'), featuresLink, 'junction');
48
+ console.log(`✓ Fixed prismarine-physics (junction): ${featuresLink}`);
49
+ fixedCount++;
50
+ } catch (err2) {
51
+ console.log(`✗ Failed to fix prismarine-physics: ${featuresLink}`);
52
+ }
53
+ } else {
54
+ console.log(`✗ Failed to fix prismarine-physics: ${featuresLink}`);
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
60
+ }
61
+ } catch (err) {
62
+ // Ignore errors from readdirSync
63
+ }
64
+ };
65
+
66
+ findAndFix(pnpmPath);
67
+ }
68
+
69
+ // Handle npm-style node_modules (direct path)
70
+ const npmLibPath = path.join(__dirname, '..', '..', 'node_modules', 'prismarine-physics', 'lib');
71
+ if (fs.existsSync(npmLibPath)) {
72
+ const featuresJson = path.join(npmLibPath, 'features.json');
73
+ const featuresLink = path.join(npmLibPath, 'features');
74
+
75
+ if (fs.existsSync(featuresJson) && !fs.existsSync(featuresLink)) {
76
+ try {
77
+ fs.symlinkSync('features.json', featuresLink);
78
+ console.log(`✓ Fixed prismarine-physics: ${featuresLink}`);
79
+ fixedCount++;
80
+ } catch (err) {
81
+ if (process.platform === 'win32') {
82
+ try {
83
+ fs.symlinkSync(path.join(npmLibPath, 'features.json'), featuresLink, 'junction');
84
+ console.log(`✓ Fixed prismarine-physics (junction): ${featuresLink}`);
85
+ fixedCount++;
86
+ } catch (err2) {
87
+ console.log(`✗ Failed to fix prismarine-physics: ${featuresLink}`);
88
+ }
89
+ } else {
90
+ console.log(`✗ Failed to fix prismarine-physics: ${featuresLink}`);
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ if (fixedCount > 0) {
97
+ console.log(`\nFixed ${fixedCount} prismarine-physics package(s)`);
98
+ } else {
99
+ // Silent if already fixed or not found (common during development)
100
+ }
101
+ }
102
+
103
+ fixPrismarinePhysics();